@aliyunrds/ctxdb-shared 0.0.7 → 0.0.8
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/README.md +15 -0
- package/dist/index.d.ts +513 -2
- package/dist/index.js +1657 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -483,8 +483,8 @@ function getMemoryImportance(memory) {
|
|
|
483
483
|
};
|
|
484
484
|
return defaults[cat] ?? 0.5;
|
|
485
485
|
}
|
|
486
|
-
function estimateTokens(
|
|
487
|
-
return Math.ceil(
|
|
486
|
+
function estimateTokens(text2) {
|
|
487
|
+
return Math.ceil(text2.length / CHARS_PER_TOKEN);
|
|
488
488
|
}
|
|
489
489
|
function rankMemories(memories, categoryOrder) {
|
|
490
490
|
const orderMap = new Map(categoryOrder.map((cat, i) => [cat, i]));
|
|
@@ -698,6 +698,7 @@ async function recall(provider, query, userId, config = {}, sessionId) {
|
|
|
698
698
|
|
|
699
699
|
// src/debug-policy.ts
|
|
700
700
|
var OFFICIAL_PRODUCTION_BASE_URLS = [
|
|
701
|
+
"https://api.cn-hangzhou.agentcontext.aliyuncs.com",
|
|
701
702
|
"https://context-database.aliyuncs.com"
|
|
702
703
|
];
|
|
703
704
|
function normalizeBaseUrl(value) {
|
|
@@ -809,9 +810,9 @@ function sanitizeRecallFailureReason(reason, secrets = []) {
|
|
|
809
810
|
}
|
|
810
811
|
function sanitizeTraceString(value, secrets) {
|
|
811
812
|
let sanitized = value.replace(/authorization\s*[:=]\s*[^\r\n]*/gi, "Authorization: [REDACTED]").replace(/(api[_-]?key|access[_-]?token)\s*[:=]\s*[^\s,;]+/gi, "$1=[REDACTED]");
|
|
812
|
-
for (const
|
|
813
|
-
if (!
|
|
814
|
-
sanitized = sanitized.split(
|
|
813
|
+
for (const secret2 of secrets) {
|
|
814
|
+
if (!secret2) continue;
|
|
815
|
+
sanitized = sanitized.split(secret2).join("[REDACTED]");
|
|
815
816
|
}
|
|
816
817
|
return sanitized;
|
|
817
818
|
}
|
|
@@ -946,6 +947,7 @@ function isRecallTraceRecord(value) {
|
|
|
946
947
|
const baseValid = record.schema_version === RECALL_TRACE_SCHEMA_VERSION && (record.type === "recall.start" || record.type === "recall.finish") && isIsoTimestamp(record.timestamp) && typeof record.recall_event_id === "string" && record.recall_event_id.length > 0 && (typeof record.session_id === "string" || record.session_id === null) && isRecallTraceAgent(record.agent) && (record.kind === "prompt" || record.kind === "warmup") && typeof record.query === "string" && isRecallTraceRequest(record.request);
|
|
947
948
|
if (!baseValid) return false;
|
|
948
949
|
if (record.type === "recall.start") return true;
|
|
950
|
+
if (record.type !== "recall.finish") return false;
|
|
949
951
|
const selection = record.selection;
|
|
950
952
|
return isNonNegativeFinite(record.duration_ms) && typeof record.outcome === "string" && TRACE_OUTCOMES.has(record.outcome) && (typeof record.failure_reason === "string" || record.failure_reason === null) && Array.isArray(record.returned_memories) && Array.isArray(record.returned_knowledge) && Boolean(selection) && Array.isArray(selection?.selected) && Array.isArray(selection?.excluded) && isNonNegativeFinite(selection?.token_estimate) && typeof record.recall_context === "string";
|
|
951
953
|
}
|
|
@@ -993,8 +995,1617 @@ function readRecallTraces(options = {}) {
|
|
|
993
995
|
});
|
|
994
996
|
return { records: records.map(({ record }) => record), malformedLines };
|
|
995
997
|
}
|
|
998
|
+
|
|
999
|
+
// src/device-flow.ts
|
|
1000
|
+
import { setTimeout as delay } from "timers/promises";
|
|
1001
|
+
var DeviceFlowError = class extends Error {
|
|
1002
|
+
code;
|
|
1003
|
+
constructor(code) {
|
|
1004
|
+
super(code === "service_unavailable" ? "ContextDB authentication service is unavailable. No automatic retry was performed; start a new login later if authorization could not be completed." : `ContextDB device authorization: ${code}. Start a new login if authorization could not be completed.`);
|
|
1005
|
+
this.name = "DeviceFlowError";
|
|
1006
|
+
this.code = code;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
var invalid = () => new DeviceFlowError("invalid_response");
|
|
1010
|
+
var scopes = ["read_only", "read_write", "default"];
|
|
1011
|
+
var clientId = "ctxdb-cli";
|
|
1012
|
+
var grantType = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1013
|
+
function normalizeCoreOrigin(value) {
|
|
1014
|
+
let url;
|
|
1015
|
+
try {
|
|
1016
|
+
url = new URL(value);
|
|
1017
|
+
} catch {
|
|
1018
|
+
throw new Error("Invalid Core server origin");
|
|
1019
|
+
}
|
|
1020
|
+
const local = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
1021
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.protocol === "https:" || local && url.protocol === "http:")) throw new Error("Invalid Core server origin");
|
|
1022
|
+
return url.origin;
|
|
1023
|
+
}
|
|
1024
|
+
var CoreDeviceClient = class {
|
|
1025
|
+
baseUrl;
|
|
1026
|
+
fetchImpl;
|
|
1027
|
+
now;
|
|
1028
|
+
requestTimeoutMs;
|
|
1029
|
+
sleep;
|
|
1030
|
+
constructor(baseUrl, options = {}) {
|
|
1031
|
+
this.baseUrl = normalizeCoreOrigin(baseUrl);
|
|
1032
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1033
|
+
this.now = options.now ?? Date.now;
|
|
1034
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
1035
|
+
this.sleep = options.sleep ?? ((ms, signal) => delay(ms, void 0, { signal }));
|
|
1036
|
+
}
|
|
1037
|
+
async login(options) {
|
|
1038
|
+
const started = this.now();
|
|
1039
|
+
const initial = await this.post("/v1/auth/device/authorize", {
|
|
1040
|
+
client_id: clientId,
|
|
1041
|
+
...options.initialAgent === void 0 ? {} : { initial_agent: options.initialAgent }
|
|
1042
|
+
}, options.signal);
|
|
1043
|
+
if (!initial.ok) throw new DeviceFlowError("request_rejected");
|
|
1044
|
+
const data = initial.data;
|
|
1045
|
+
const requestId = text(data.request_id, 32), userCode = text(data.user_code, 9), code = text(data.device_code, 43);
|
|
1046
|
+
if (!/^[0-9a-f]{32}$/.test(requestId) || !/^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$/.test(userCode) || !/^[A-Za-z0-9_-]{43}$/.test(code)) throw invalid();
|
|
1047
|
+
const expiresIn = integer(data.expires_in, 1, 3600);
|
|
1048
|
+
let interval = integer(data.interval, 1, 3600);
|
|
1049
|
+
const deadline = started + expiresIn * 1e3;
|
|
1050
|
+
options.onPrompt({ requestId, userCode, expiresIn, interval });
|
|
1051
|
+
while (true) {
|
|
1052
|
+
if (options.signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
1053
|
+
if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
|
|
1054
|
+
try {
|
|
1055
|
+
await this.sleep(Math.min(interval * 1e3, deadline - this.now()), options.signal);
|
|
1056
|
+
} catch {
|
|
1057
|
+
throw new DeviceFlowError("cancelled");
|
|
1058
|
+
}
|
|
1059
|
+
if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
|
|
1060
|
+
const issuedAt = this.now();
|
|
1061
|
+
const result = await this.post("/v1/auth/device/token", { grant_type: grantType, client_id: clientId, device_code: code }, options.signal);
|
|
1062
|
+
if (result.ok) return loginResult(result.data, this.baseUrl, issuedAt, this.now());
|
|
1063
|
+
const error = result.data.error;
|
|
1064
|
+
if (error === "authorization_pending") continue;
|
|
1065
|
+
if (error === "slow_down") {
|
|
1066
|
+
interval = Math.max(interval + 5, integer(result.data.interval, 1, 3605));
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (error === "access_denied" || error === "expired_token" || error === "invalid_grant") throw new DeviceFlowError(error);
|
|
1070
|
+
throw new DeviceFlowError("request_rejected");
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
async refresh(refreshToken, baseUrl) {
|
|
1074
|
+
this.requireSameOrigin(baseUrl);
|
|
1075
|
+
const result = await this.post("/v1/auth/token/refresh", { refresh_token: secret(refreshToken) });
|
|
1076
|
+
if (!result.ok) throw new DeviceFlowError("request_rejected");
|
|
1077
|
+
return credentialResult(result.data);
|
|
1078
|
+
}
|
|
1079
|
+
async revoke(refreshToken, baseUrl) {
|
|
1080
|
+
this.requireSameOrigin(baseUrl);
|
|
1081
|
+
const result = await this.post("/v1/auth/token/revoke", { refresh_token: secret(refreshToken) });
|
|
1082
|
+
if (!result.ok) throw new DeviceFlowError("request_rejected");
|
|
1083
|
+
}
|
|
1084
|
+
requireSameOrigin(baseUrl) {
|
|
1085
|
+
if (normalizeCoreOrigin(baseUrl) !== this.baseUrl) throw new Error("OAuth credential Core origin cannot change");
|
|
1086
|
+
}
|
|
1087
|
+
async post(path, body, signal) {
|
|
1088
|
+
if (signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
1089
|
+
const controller = new AbortController();
|
|
1090
|
+
const abort = () => controller.abort();
|
|
1091
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1092
|
+
const timer = setTimeout(abort, this.requestTimeoutMs);
|
|
1093
|
+
timer.unref?.();
|
|
1094
|
+
try {
|
|
1095
|
+
const response = await this.fetchImpl(this.baseUrl + path, {
|
|
1096
|
+
method: "POST",
|
|
1097
|
+
body: JSON.stringify(body),
|
|
1098
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1099
|
+
redirect: "error",
|
|
1100
|
+
credentials: "omit",
|
|
1101
|
+
cache: "no-store",
|
|
1102
|
+
signal: controller.signal
|
|
1103
|
+
});
|
|
1104
|
+
const box = await boundedJson(response);
|
|
1105
|
+
if (!Number.isInteger(box.errorCode)) throw invalid();
|
|
1106
|
+
const ok = response.ok && box.errorCode === 0;
|
|
1107
|
+
const data = (!ok || path === "/v1/auth/token/revoke") && box.data == null ? {} : object(box.data);
|
|
1108
|
+
if (!ok && response.status >= 500) throw new DeviceFlowError("service_unavailable");
|
|
1109
|
+
return { ok, data };
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
if (signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
1112
|
+
if (error instanceof DeviceFlowError) throw error;
|
|
1113
|
+
throw new DeviceFlowError("network_error");
|
|
1114
|
+
} finally {
|
|
1115
|
+
clearTimeout(timer);
|
|
1116
|
+
signal?.removeEventListener("abort", abort);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
function text(value, maximum) {
|
|
1121
|
+
if (typeof value !== "string" || !value || value.length > maximum || /[\x00-\x1f\x7f]/.test(value)) throw invalid();
|
|
1122
|
+
return value;
|
|
1123
|
+
}
|
|
1124
|
+
function integer(value, min, max) {
|
|
1125
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw invalid();
|
|
1126
|
+
return value;
|
|
1127
|
+
}
|
|
1128
|
+
function secret(value) {
|
|
1129
|
+
const token = text(value, 8192);
|
|
1130
|
+
if (/\s/.test(token)) throw invalid();
|
|
1131
|
+
return token;
|
|
1132
|
+
}
|
|
1133
|
+
function object(value) {
|
|
1134
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid();
|
|
1135
|
+
return value;
|
|
1136
|
+
}
|
|
1137
|
+
function credentialResult(data) {
|
|
1138
|
+
if (data.token_type !== "Bearer" || !scopes.includes(data.scope)) throw invalid();
|
|
1139
|
+
return {
|
|
1140
|
+
access_token: secret(data.access_token),
|
|
1141
|
+
refresh_token: secret(data.refresh_token),
|
|
1142
|
+
token_type: "Bearer",
|
|
1143
|
+
expires_in: integer(data.expires_in, 1, 3600),
|
|
1144
|
+
session_id: text(data.session_id, 64),
|
|
1145
|
+
scope: data.scope
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
function loginResult(data, baseUrl, issuedAt, now) {
|
|
1149
|
+
const tokens = credentialResult(data), scope = tokens.scope;
|
|
1150
|
+
if (data.client_id !== clientId || scope !== "default") throw invalid();
|
|
1151
|
+
const expiresAt = issuedAt + tokens.expires_in * 1e3;
|
|
1152
|
+
if (expiresAt <= now) throw invalid();
|
|
1153
|
+
return {
|
|
1154
|
+
baseUrl,
|
|
1155
|
+
workspaceId: text(data.workspace_id, 64),
|
|
1156
|
+
memberId: text(data.member_id, 64),
|
|
1157
|
+
scope,
|
|
1158
|
+
clientId,
|
|
1159
|
+
sessionId: tokens.session_id,
|
|
1160
|
+
accountDisplay: text(data.account_display, 256),
|
|
1161
|
+
issuedAt,
|
|
1162
|
+
expiresAt,
|
|
1163
|
+
accessToken: tokens.access_token,
|
|
1164
|
+
refreshToken: tokens.refresh_token
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
async function boundedJson(response) {
|
|
1168
|
+
if (!response.body || Number(response.headers.get("content-length")) > 65536) throw invalid();
|
|
1169
|
+
const reader = response.body.getReader();
|
|
1170
|
+
const chunks = [];
|
|
1171
|
+
let size = 0;
|
|
1172
|
+
try {
|
|
1173
|
+
while (true) {
|
|
1174
|
+
const { done, value } = await reader.read();
|
|
1175
|
+
if (done) break;
|
|
1176
|
+
size += value.byteLength;
|
|
1177
|
+
if (size > 65536) {
|
|
1178
|
+
await reader.cancel();
|
|
1179
|
+
throw invalid();
|
|
1180
|
+
}
|
|
1181
|
+
chunks.push(value);
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
return object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
1185
|
+
} catch {
|
|
1186
|
+
throw invalid();
|
|
1187
|
+
}
|
|
1188
|
+
} finally {
|
|
1189
|
+
reader.releaseLock();
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/oauth-credentials.ts
|
|
1194
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1195
|
+
|
|
1196
|
+
// src/credentials/local-credential-provider.ts
|
|
1197
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
1198
|
+
import { homedir as homedir2 } from "os";
|
|
1199
|
+
import { join as join3 } from "path";
|
|
1200
|
+
|
|
1201
|
+
// src/credentials/storage.ts
|
|
1202
|
+
import { constants, closeSync as closeSync2, existsSync as existsSync2, fstatSync, fsyncSync, lstatSync, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
|
|
1203
|
+
import { randomBytes, createHash } from "crypto";
|
|
1204
|
+
import { dirname as dirname2 } from "path";
|
|
1205
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
1206
|
+
|
|
1207
|
+
// src/windows-private-storage.ts
|
|
1208
|
+
import { execFileSync } from "child_process";
|
|
1209
|
+
import { join as join2 } from "path";
|
|
1210
|
+
var SCRIPT = `
|
|
1211
|
+
$ErrorActionPreference = 'Stop'
|
|
1212
|
+
$path = $env:CTXDB_PRIVATE_STORAGE_PATH
|
|
1213
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
1214
|
+
$item = Get-Item -LiteralPath $path -Force
|
|
1215
|
+
if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'reparse' }
|
|
1216
|
+
if ($env:CTXDB_PRIVATE_STORAGE_INITIALIZE -eq '1') {
|
|
1217
|
+
if (-not $item.PSIsContainer) { throw 'not directory' }
|
|
1218
|
+
$acl = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
1219
|
+
$acl.SetOwner($sid)
|
|
1220
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
1221
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
|
1222
|
+
$acl.AddAccessRule($rule)
|
|
1223
|
+
Set-Acl -LiteralPath $path -AclObject $acl
|
|
1224
|
+
}
|
|
1225
|
+
$acl = Get-Acl -LiteralPath $path
|
|
1226
|
+
if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'owner' }
|
|
1227
|
+
$self = $false
|
|
1228
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
|
1229
|
+
if ($rule.AccessControlType -eq 'Allow') {
|
|
1230
|
+
if ($rule.IdentityReference.Value -ne $sid.Value) { throw 'other principal' }
|
|
1231
|
+
if (($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl) { $self = $true }
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (-not $self) { throw 'missing owner access' }
|
|
1235
|
+
Write-Output 'private'
|
|
1236
|
+
`;
|
|
1237
|
+
function checkWindowsPrivateStorage(path, initialize, run = execFileSync) {
|
|
1238
|
+
const systemRoot = process.env.SystemRoot;
|
|
1239
|
+
if (!systemRoot) throw new Error("Private Windows credential storage could not be verified.");
|
|
1240
|
+
try {
|
|
1241
|
+
const result = run(
|
|
1242
|
+
join2(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
1243
|
+
["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(SCRIPT, "utf16le").toString("base64")],
|
|
1244
|
+
{
|
|
1245
|
+
env: { ...process.env, CTXDB_PRIVATE_STORAGE_PATH: path, CTXDB_PRIVATE_STORAGE_INITIALIZE: initialize ? "1" : "0" },
|
|
1246
|
+
encoding: "utf8",
|
|
1247
|
+
windowsHide: true,
|
|
1248
|
+
timeout: 1e4,
|
|
1249
|
+
maxBuffer: 4096,
|
|
1250
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1251
|
+
}
|
|
1252
|
+
);
|
|
1253
|
+
if (String(result).trim() !== "private") throw new Error();
|
|
1254
|
+
} catch {
|
|
1255
|
+
throw new Error("Private Windows credential storage could not be verified.");
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/credentials/storage.ts
|
|
1260
|
+
function privatePath(path, directory = false) {
|
|
1261
|
+
const s = lstatSync(path);
|
|
1262
|
+
if (s.isSymbolicLink() || (directory ? !s.isDirectory() : !s.isFile())) throw new Error("credentials: storage is not private or is invalid");
|
|
1263
|
+
if (process.platform === "win32") {
|
|
1264
|
+
checkWindowsPrivateStorage(path, false);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (s.mode & 63 || process.getuid && s.uid !== process.getuid()) throw new Error("credentials: storage must be private and owner-only (run chmod 600 on credentials.json)");
|
|
1268
|
+
}
|
|
1269
|
+
function prepareStorage(path) {
|
|
1270
|
+
const dir = dirname2(path);
|
|
1271
|
+
if (!existsSync2(dir)) {
|
|
1272
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
1273
|
+
if (process.platform === "win32") checkWindowsPrivateStorage(dir, true);
|
|
1274
|
+
}
|
|
1275
|
+
privatePath(dir, true);
|
|
1276
|
+
}
|
|
1277
|
+
function readPrivate(path) {
|
|
1278
|
+
if (!existsSync2(path)) return void 0;
|
|
1279
|
+
privatePath(dirname2(path), true);
|
|
1280
|
+
privatePath(path);
|
|
1281
|
+
const fd = openSync2(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1282
|
+
try {
|
|
1283
|
+
const stat = fstatSync(fd);
|
|
1284
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new Error("credentials: invalid storage");
|
|
1285
|
+
return readFileSync2(fd, "utf8");
|
|
1286
|
+
} finally {
|
|
1287
|
+
closeSync2(fd);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function atomicCredentialWrite(path, value) {
|
|
1291
|
+
prepareStorage(path);
|
|
1292
|
+
if (existsSync2(path)) privatePath(path);
|
|
1293
|
+
const temp = path + "." + randomBytes(12).toString("hex") + ".tmp";
|
|
1294
|
+
let fd;
|
|
1295
|
+
try {
|
|
1296
|
+
fd = openSync2(temp, "wx", 384);
|
|
1297
|
+
writeFileSync(fd, JSON.stringify(value, null, 2) + "\n");
|
|
1298
|
+
fsyncSync(fd);
|
|
1299
|
+
closeSync2(fd);
|
|
1300
|
+
fd = void 0;
|
|
1301
|
+
renameSync2(temp, path);
|
|
1302
|
+
if (process.platform !== "win32") {
|
|
1303
|
+
try {
|
|
1304
|
+
const dir = openSync2(dirname2(path), "r");
|
|
1305
|
+
try {
|
|
1306
|
+
fsyncSync(dir);
|
|
1307
|
+
} finally {
|
|
1308
|
+
closeSync2(dir);
|
|
1309
|
+
}
|
|
1310
|
+
} catch {
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
} finally {
|
|
1314
|
+
if (fd !== void 0) closeSync2(fd);
|
|
1315
|
+
if (existsSync2(temp)) unlinkSync(temp);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function withCredentialWriteLock(path, operation) {
|
|
1319
|
+
prepareStorage(path);
|
|
1320
|
+
const lock = path + ".lock", deadline = Date.now() + 1e4;
|
|
1321
|
+
let fd;
|
|
1322
|
+
while (true) {
|
|
1323
|
+
try {
|
|
1324
|
+
fd = openSync2(lock, "wx", 384);
|
|
1325
|
+
break;
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
if (error.code !== "EEXIST") throw error;
|
|
1328
|
+
if (Date.now() >= deadline) throw new Error("credentials: writer lock is busy; check the previous CLI process");
|
|
1329
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
try {
|
|
1333
|
+
return operation();
|
|
1334
|
+
} finally {
|
|
1335
|
+
closeSync2(fd);
|
|
1336
|
+
unlinkSync(lock);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
function operationLockPath(path, key) {
|
|
1340
|
+
return path + "." + createHash("sha256").update(key).digest("hex") + ".lock";
|
|
1341
|
+
}
|
|
1342
|
+
async function withCredentialOperation(path, key, operation, options = {}) {
|
|
1343
|
+
prepareStorage(path);
|
|
1344
|
+
const lock = operationLockPath(path, key), deadline = Date.now() + (options.lockWaitMs ?? 3e4);
|
|
1345
|
+
while (true) {
|
|
1346
|
+
options.signal?.throwIfAborted();
|
|
1347
|
+
let fd;
|
|
1348
|
+
try {
|
|
1349
|
+
fd = openSync2(lock, "wx", 384);
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
if (error.code !== "EEXIST") throw error;
|
|
1352
|
+
if (Date.now() >= deadline) throw new Error("credentials: credential_busy");
|
|
1353
|
+
await delay2(25, void 0, { signal: options.signal });
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
try {
|
|
1357
|
+
writeFileSync(fd, String(process.pid));
|
|
1358
|
+
} finally {
|
|
1359
|
+
closeSync2(fd);
|
|
1360
|
+
}
|
|
1361
|
+
try {
|
|
1362
|
+
return await operation();
|
|
1363
|
+
} finally {
|
|
1364
|
+
unlinkSync(lock);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/credentials/types.ts
|
|
1370
|
+
var ACTIVE_CONTEXTDB_CREDENTIAL = "contextdb/active";
|
|
1371
|
+
|
|
1372
|
+
// src/credentials/local-credential-provider.ts
|
|
1373
|
+
function defaultCredentialsPath() {
|
|
1374
|
+
return join3(homedir2(), ".ctxdb", "credentials.json");
|
|
1375
|
+
}
|
|
1376
|
+
var object2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1377
|
+
var fail = (message) => {
|
|
1378
|
+
throw new Error("credentials: " + message);
|
|
1379
|
+
};
|
|
1380
|
+
function origin(value) {
|
|
1381
|
+
if (typeof value !== "string" || !value) return fail("invalid service URL");
|
|
1382
|
+
let u;
|
|
1383
|
+
try {
|
|
1384
|
+
u = new URL(value);
|
|
1385
|
+
} catch {
|
|
1386
|
+
return fail("invalid service URL");
|
|
1387
|
+
}
|
|
1388
|
+
if (!["http:", "https:"].includes(u.protocol) || u.username || u.password || u.search || u.hash) return fail("invalid service URL");
|
|
1389
|
+
return u.toString().replace(/\/$/, "");
|
|
1390
|
+
}
|
|
1391
|
+
function validateApi(raw, internal, v1 = false) {
|
|
1392
|
+
if (!object2(raw.payload) || typeof raw.payload.api_key !== "string" || !raw.payload.api_key) fail("invalid api-key payload");
|
|
1393
|
+
const metadata = v1 ? raw.payload : raw.metadata;
|
|
1394
|
+
origin(metadata.base_url);
|
|
1395
|
+
if (internal) {
|
|
1396
|
+
origin(metadata.login_server);
|
|
1397
|
+
if (!object2(raw.metadata) || !["browser-loopback", "device-code"].includes(raw.metadata.authorization_method) || typeof raw.metadata.issued_at !== "string" || !Number.isFinite(Date.parse(raw.metadata.issued_at))) fail("invalid api-key metadata");
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function validate(key, raw) {
|
|
1401
|
+
if (!object2(raw) || !["api-key", "oauth-session"].includes(raw.kind) || !object2(raw.payload) || !object2(raw.metadata) || !object2(raw.metadata.owner) || !["ready", "refreshing", "reauth_required", "logged_out"].includes(raw.metadata.state) || typeof raw.metadata.generation !== "string" || !/^[a-f0-9]{32}$/.test(raw.metadata.generation) || !Number.isSafeInteger(raw.metadata.revision) || raw.metadata.revision < 1) fail("invalid credential record");
|
|
1402
|
+
if (key === ACTIVE_CONTEXTDB_CREDENTIAL) {
|
|
1403
|
+
if (raw.kind !== "api-key" || raw.metadata.owner.distribution !== "internal" || raw.metadata.state !== "ready") fail("invalid contextdb/active record");
|
|
1404
|
+
} else if (!/^[a-f0-9]{32}$/.test(key) || raw.metadata.owner.distribution !== "public" || typeof raw.metadata.owner.agent !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(raw.metadata.owner.agent) || raw.metadata.owner.loginId !== void 0 && (typeof raw.metadata.owner.loginId !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(raw.metadata.owner.loginId))) fail("invalid credential ownership");
|
|
1405
|
+
const fields = raw.kind === "api-key" ? ["api_key"] : ["ready", "refreshing"].includes(raw.metadata.state) ? ["accessToken", "refreshToken"] : [];
|
|
1406
|
+
if (Object.keys(raw.payload).length !== fields.length || fields.some((field) => typeof raw.payload[field] !== "string" || !raw.payload[field])) fail("invalid credential payload; only credential values are allowed");
|
|
1407
|
+
if (raw.kind === "api-key") validateApi(raw, key === ACTIVE_CONTEXTDB_CREDENTIAL);
|
|
1408
|
+
}
|
|
1409
|
+
function readDocument(path) {
|
|
1410
|
+
const bytes = readPrivate(path);
|
|
1411
|
+
if (bytes === void 0) return { version: 2, records: {} };
|
|
1412
|
+
let doc;
|
|
1413
|
+
try {
|
|
1414
|
+
doc = JSON.parse(bytes);
|
|
1415
|
+
} catch {
|
|
1416
|
+
return fail("credentials.json is not valid JSON");
|
|
1417
|
+
}
|
|
1418
|
+
if (!object2(doc) || ![1, 2].includes(doc.version) || !object2(doc.records)) return fail("unsupported credentials.json schema/version");
|
|
1419
|
+
if (doc.version === 1) {
|
|
1420
|
+
if (Object.keys(doc.records).some((k) => k !== ACTIVE_CONTEXTDB_CREDENTIAL)) fail("unsupported record key in v1 credentials");
|
|
1421
|
+
const active = doc.records[ACTIVE_CONTEXTDB_CREDENTIAL];
|
|
1422
|
+
if (active !== void 0) {
|
|
1423
|
+
if (active.kind !== "api-key") fail("unsupported record kind");
|
|
1424
|
+
validateApi(active, true, true);
|
|
1425
|
+
}
|
|
1426
|
+
} else for (const [key, record] of Object.entries(doc.records)) validate(key, record);
|
|
1427
|
+
return doc;
|
|
1428
|
+
}
|
|
1429
|
+
function upgrade(doc) {
|
|
1430
|
+
if (doc.version === 2) return doc;
|
|
1431
|
+
const record = doc.records[ACTIVE_CONTEXTDB_CREDENTIAL];
|
|
1432
|
+
return { ...doc, version: 2, records: record ? { [ACTIVE_CONTEXTDB_CREDENTIAL]: legacy(record) } : {} };
|
|
1433
|
+
}
|
|
1434
|
+
function legacy(record) {
|
|
1435
|
+
const { api_key, ...details } = record.payload;
|
|
1436
|
+
return { kind: record.kind, payload: { api_key }, metadata: {
|
|
1437
|
+
...details,
|
|
1438
|
+
...record.metadata,
|
|
1439
|
+
base_url: record.payload.base_url,
|
|
1440
|
+
login_server: record.payload.login_server,
|
|
1441
|
+
owner: { distribution: "internal" },
|
|
1442
|
+
state: "ready",
|
|
1443
|
+
revision: 1,
|
|
1444
|
+
generation: createHash2("sha256").update(JSON.stringify(record)).digest("hex").slice(0, 32)
|
|
1445
|
+
} };
|
|
1446
|
+
}
|
|
1447
|
+
function snapshot(doc, key) {
|
|
1448
|
+
const raw = Object.hasOwn(doc.records, key) ? doc.records[key] : void 0;
|
|
1449
|
+
const record = raw && (doc.version === 1 ? legacy(raw) : raw);
|
|
1450
|
+
return { key, record, fingerprint: createHash2("sha256").update(JSON.stringify(record) ?? "missing").digest("hex") };
|
|
1451
|
+
}
|
|
1452
|
+
function apiRecord(record) {
|
|
1453
|
+
if (!record) return void 0;
|
|
1454
|
+
if (record.kind !== "api-key" || record.metadata.owner.distribution !== "internal") return fail("invalid contextdb/active record");
|
|
1455
|
+
return {
|
|
1456
|
+
kind: "api-key",
|
|
1457
|
+
payload: { apiKey: record.payload.api_key },
|
|
1458
|
+
metadata: {
|
|
1459
|
+
baseUrl: origin(record.metadata.base_url),
|
|
1460
|
+
loginServer: origin(record.metadata.login_server),
|
|
1461
|
+
authorizationMethod: record.metadata.authorization_method,
|
|
1462
|
+
issuedAt: record.metadata.issued_at
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function internalCandidate(record) {
|
|
1467
|
+
return {
|
|
1468
|
+
kind: "api-key",
|
|
1469
|
+
payload: { api_key: record.payload.apiKey },
|
|
1470
|
+
metadata: {
|
|
1471
|
+
base_url: record.metadata.baseUrl,
|
|
1472
|
+
login_server: record.metadata.loginServer,
|
|
1473
|
+
authorization_method: record.metadata.authorizationMethod,
|
|
1474
|
+
issued_at: record.metadata.issuedAt,
|
|
1475
|
+
owner: { distribution: "internal" },
|
|
1476
|
+
state: "ready"
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
var LocalCredentialProvider = class {
|
|
1481
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1482
|
+
path;
|
|
1483
|
+
constructor(path = defaultCredentialsPath()) {
|
|
1484
|
+
this.path = path;
|
|
1485
|
+
}
|
|
1486
|
+
snapshot(key) {
|
|
1487
|
+
return snapshot(readDocument(this.path), key);
|
|
1488
|
+
}
|
|
1489
|
+
readStoredRecord(key) {
|
|
1490
|
+
return this.snapshot(key).record;
|
|
1491
|
+
}
|
|
1492
|
+
async readRecord(key) {
|
|
1493
|
+
return apiRecord(this.readStoredRecord(key));
|
|
1494
|
+
}
|
|
1495
|
+
async describeRecord(key) {
|
|
1496
|
+
const r = this.readStoredRecord(key);
|
|
1497
|
+
return { configured: !!r && r.metadata.state === "ready", kind: r?.kind, writable: true };
|
|
1498
|
+
}
|
|
1499
|
+
onRecordUpdated(listener) {
|
|
1500
|
+
this.listeners.add(listener);
|
|
1501
|
+
return () => this.listeners.delete(listener);
|
|
1502
|
+
}
|
|
1503
|
+
/** Synchronous short commit; callbacks may acquire a config lock, never the reverse. */
|
|
1504
|
+
transaction(operation) {
|
|
1505
|
+
return withCredentialWriteLock(this.path, () => {
|
|
1506
|
+
let doc = readDocument(this.path);
|
|
1507
|
+
const read = (key) => snapshot(doc, key);
|
|
1508
|
+
const check = (expected) => {
|
|
1509
|
+
if (read(expected.key).fingerprint !== expected.fingerprint) fail("authentication changed; the newer credential was preserved, retry the command");
|
|
1510
|
+
};
|
|
1511
|
+
const save = () => {
|
|
1512
|
+
doc = upgrade(doc);
|
|
1513
|
+
atomicCredentialWrite(this.path, doc);
|
|
1514
|
+
};
|
|
1515
|
+
return operation({
|
|
1516
|
+
read,
|
|
1517
|
+
put: (key, candidate, expected) => {
|
|
1518
|
+
if (expected) {
|
|
1519
|
+
if (expected.key !== key) fail("invalid commit target");
|
|
1520
|
+
check(expected);
|
|
1521
|
+
}
|
|
1522
|
+
const before = read(key), previous = before.record;
|
|
1523
|
+
const next = { kind: candidate.kind, payload: candidate.payload, metadata: {
|
|
1524
|
+
...candidate.metadata,
|
|
1525
|
+
generation: previous?.metadata.generation ?? randomBytes2(16).toString("hex"),
|
|
1526
|
+
revision: (previous?.metadata.revision ?? 0) + 1
|
|
1527
|
+
} };
|
|
1528
|
+
validate(key, next);
|
|
1529
|
+
doc = upgrade(doc);
|
|
1530
|
+
doc.records[key] = next;
|
|
1531
|
+
save();
|
|
1532
|
+
return { before, after: read(key) };
|
|
1533
|
+
},
|
|
1534
|
+
remove: (expected) => {
|
|
1535
|
+
check(expected);
|
|
1536
|
+
if (!read(expected.key).record) return;
|
|
1537
|
+
doc = upgrade(doc);
|
|
1538
|
+
delete doc.records[expected.key];
|
|
1539
|
+
save();
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
commit(key, record, expected = this.snapshot(key)) {
|
|
1545
|
+
return this.transaction((tx) => tx.put(key, record, expected));
|
|
1546
|
+
}
|
|
1547
|
+
rollback(receipt) {
|
|
1548
|
+
return this.transaction((tx) => {
|
|
1549
|
+
if (tx.read(receipt.after.key).fingerprint !== receipt.after.fingerprint) return false;
|
|
1550
|
+
if (receipt.before.record) tx.put(receipt.after.key, receipt.before.record, receipt.after);
|
|
1551
|
+
else tx.remove(receipt.after);
|
|
1552
|
+
return true;
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
async modifyRecord(key, mutate, signal) {
|
|
1556
|
+
const before = this.snapshot(key), next = await mutate(apiRecord(before.record));
|
|
1557
|
+
signal?.throwIfAborted();
|
|
1558
|
+
if (next === void 0) return apiRecord(before.record);
|
|
1559
|
+
const receipt = this.commit(key, internalCandidate(next), before);
|
|
1560
|
+
for (const listener of this.listeners) {
|
|
1561
|
+
try {
|
|
1562
|
+
listener(key);
|
|
1563
|
+
} catch {
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return apiRecord(receipt.after.record);
|
|
1567
|
+
}
|
|
1568
|
+
async deleteRecord(key, signal) {
|
|
1569
|
+
signal?.throwIfAborted();
|
|
1570
|
+
const before = this.snapshot(key);
|
|
1571
|
+
this.transaction((tx) => tx.remove(before));
|
|
1572
|
+
for (const listener of this.listeners) {
|
|
1573
|
+
try {
|
|
1574
|
+
listener(key);
|
|
1575
|
+
} catch {
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
function readActiveCredentialSync(path = defaultCredentialsPath()) {
|
|
1581
|
+
return apiRecord(new LocalCredentialProvider(path).readStoredRecord(ACTIVE_CONTEXTDB_CREDENTIAL));
|
|
1582
|
+
}
|
|
1583
|
+
async function withAuthorizationLock(path, operation, signal) {
|
|
1584
|
+
return withCredentialOperation(path, ACTIVE_CONTEXTDB_CREDENTIAL, () => operation(signal), { signal });
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// src/oauth-credentials.ts
|
|
1588
|
+
var OAuthCredentialError = class extends Error {
|
|
1589
|
+
code;
|
|
1590
|
+
constructor(code) {
|
|
1591
|
+
const messages = {
|
|
1592
|
+
login_required: "OAuth session needs login; run ctxdb login for this profile.",
|
|
1593
|
+
credential_busy: "OAuth credential is busy. If its previous process crashed, run ctxdb login for a new credential.",
|
|
1594
|
+
invalid_profile: "OAuth credential does not belong to this profile.",
|
|
1595
|
+
unsafe_storage: "OAuth credential storage is not private or is invalid."
|
|
1596
|
+
};
|
|
1597
|
+
super(messages[code]);
|
|
1598
|
+
this.name = "OAuthCredentialError";
|
|
1599
|
+
this.code = code;
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
var loginRequired = () => new OAuthCredentialError("login_required");
|
|
1603
|
+
var unsafe = () => new OAuthCredentialError("unsafe_storage");
|
|
1604
|
+
var OAuthSessionStore = class {
|
|
1605
|
+
provider;
|
|
1606
|
+
now;
|
|
1607
|
+
lockWaitMs;
|
|
1608
|
+
path;
|
|
1609
|
+
constructor(path, options = {}) {
|
|
1610
|
+
this.path = path;
|
|
1611
|
+
this.provider = new LocalCredentialProvider(path);
|
|
1612
|
+
this.now = options.now ?? Date.now;
|
|
1613
|
+
this.lockWaitMs = options.lockWaitMs ?? 3e4;
|
|
1614
|
+
}
|
|
1615
|
+
saveLogin(profile, session, loginId) {
|
|
1616
|
+
requireProfile(profile);
|
|
1617
|
+
validateSession(session);
|
|
1618
|
+
if (loginId !== void 0) requireProfile(loginId);
|
|
1619
|
+
const ref = randomBytes3(16).toString("hex");
|
|
1620
|
+
this.write(ref, { profile, ...loginId ? { loginId } : {}, state: "ready", session });
|
|
1621
|
+
return ref;
|
|
1622
|
+
}
|
|
1623
|
+
status(ref, owner) {
|
|
1624
|
+
const record = this.read(ref, owner);
|
|
1625
|
+
if (!record) return { state: "missing" };
|
|
1626
|
+
const s = record.session;
|
|
1627
|
+
return {
|
|
1628
|
+
state: record.state,
|
|
1629
|
+
profile: record.profile,
|
|
1630
|
+
baseUrl: s.baseUrl,
|
|
1631
|
+
workspaceId: s.workspaceId,
|
|
1632
|
+
memberId: s.memberId,
|
|
1633
|
+
clientId: s.clientId,
|
|
1634
|
+
scope: s.scope,
|
|
1635
|
+
sessionId: s.sessionId,
|
|
1636
|
+
accountDisplay: s.accountDisplay,
|
|
1637
|
+
accessExpiresAt: s.expiresAt
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
async resolve(ref, owner, transport) {
|
|
1641
|
+
const first = this.read(ref, owner);
|
|
1642
|
+
if (!first || ["reauth_required", "logged_out"].includes(first.state)) throw loginRequired();
|
|
1643
|
+
if (first.state === "ready" && !this.needsRefresh(first.session)) return authorization(first.session);
|
|
1644
|
+
return this.withLock(ref, async () => {
|
|
1645
|
+
const current = this.read(ref, owner);
|
|
1646
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1647
|
+
if (!this.needsRefresh(current.session)) return authorization(current.session);
|
|
1648
|
+
const started = this.now();
|
|
1649
|
+
this.write(ref, { ...current, state: "refreshing" });
|
|
1650
|
+
try {
|
|
1651
|
+
const response = await transport.refresh(current.session.refreshToken, current.session.baseUrl);
|
|
1652
|
+
const next = rotated(current.session, response, started);
|
|
1653
|
+
if (next.expiresAt <= this.now()) throw loginRequired();
|
|
1654
|
+
this.write(ref, { ...current, state: "ready", session: next });
|
|
1655
|
+
return authorization(next);
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
this.write(ref, { ...current, state: "reauth_required" });
|
|
1658
|
+
if (error instanceof OAuthCredentialError) throw error;
|
|
1659
|
+
throw loginRequired();
|
|
1660
|
+
}
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
async share(ref, profile, loginId, commit) {
|
|
1664
|
+
requireProfile(loginId);
|
|
1665
|
+
return this.withLock(ref, async () => {
|
|
1666
|
+
const current = this.read(ref, profile);
|
|
1667
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1668
|
+
if (current.loginId && current.loginId !== loginId) throw new OAuthCredentialError("invalid_profile");
|
|
1669
|
+
const receipt = this.write(ref, { ...current, loginId });
|
|
1670
|
+
try {
|
|
1671
|
+
return commit();
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
this.provider.rollback(receipt);
|
|
1674
|
+
throw error;
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
/** Setup changes the DATA endpoint without replacing the identity or racing a refresh. */
|
|
1679
|
+
async changeBaseUrl(ref, owner, baseUrl, beforeWrite) {
|
|
1680
|
+
await this.withLock(ref, async () => {
|
|
1681
|
+
this.provider.transaction((tx) => {
|
|
1682
|
+
beforeWrite();
|
|
1683
|
+
const current = this.read(ref, owner);
|
|
1684
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1685
|
+
const session = { ...current.session, baseUrl };
|
|
1686
|
+
validateSession(session);
|
|
1687
|
+
const before = tx.read(ref), candidate = credentialForSession({ ...current, session });
|
|
1688
|
+
tx.put(ref, { ...candidate, metadata: { ...before.record?.metadata, ...candidate.metadata } }, before);
|
|
1689
|
+
});
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
async logout(ref, owner, transport, beforeRevoke = () => {
|
|
1693
|
+
}) {
|
|
1694
|
+
return this.withLock(ref, async () => {
|
|
1695
|
+
const current = this.read(ref, owner);
|
|
1696
|
+
this.provider.transaction((tx) => {
|
|
1697
|
+
beforeRevoke();
|
|
1698
|
+
const before = tx.read(ref);
|
|
1699
|
+
if (current && before.record) tx.put(ref, { ...before.record, metadata: { ...before.record.metadata, state: "logged_out" }, payload: {} }, before);
|
|
1700
|
+
});
|
|
1701
|
+
let remoteRevoked = false;
|
|
1702
|
+
try {
|
|
1703
|
+
if (current?.session.refreshToken && current.state !== "logged_out") {
|
|
1704
|
+
await transport.revoke(current.session.refreshToken, current.session.baseUrl);
|
|
1705
|
+
remoteRevoked = true;
|
|
1706
|
+
}
|
|
1707
|
+
} catch {
|
|
1708
|
+
}
|
|
1709
|
+
return { remoteRevoked };
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
/** Release config references under the write lock, then delete an unused session.
|
|
1713
|
+
* Waiting on the operation lock first lets an in-flight refresh supply its latest RT.
|
|
1714
|
+
* Returning false from release preserves a session that still has users.
|
|
1715
|
+
*/
|
|
1716
|
+
async retire(ref, owner, transport, release) {
|
|
1717
|
+
return this.withLock(ref, async () => {
|
|
1718
|
+
const current = this.read(ref, owner);
|
|
1719
|
+
const removed = this.provider.transaction((tx) => {
|
|
1720
|
+
if (!release()) return false;
|
|
1721
|
+
tx.remove(tx.read(ref));
|
|
1722
|
+
return true;
|
|
1723
|
+
});
|
|
1724
|
+
if (!removed) return { removed: false, remoteRevoked: false, remoteUnconfirmed: false };
|
|
1725
|
+
if (!current || current.state === "logged_out") return { removed: true, remoteRevoked: false, remoteUnconfirmed: false };
|
|
1726
|
+
try {
|
|
1727
|
+
if (!current.session.refreshToken) throw loginRequired();
|
|
1728
|
+
await transport.revoke(current.session.refreshToken, current.session.baseUrl);
|
|
1729
|
+
return { removed: true, remoteRevoked: true, remoteUnconfirmed: false };
|
|
1730
|
+
} catch {
|
|
1731
|
+
return { removed: true, remoteRevoked: false, remoteUnconfirmed: true };
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
needsRefresh(session) {
|
|
1736
|
+
return this.now() >= session.expiresAt - Math.min(3e4, (session.expiresAt - session.issuedAt) / 2);
|
|
1737
|
+
}
|
|
1738
|
+
read(ref, owner) {
|
|
1739
|
+
if (!/^[a-f0-9]{32}$/.test(ref)) throw unsafe();
|
|
1740
|
+
requireProfile(typeof owner === "string" ? owner : owner.loginId);
|
|
1741
|
+
let stored;
|
|
1742
|
+
try {
|
|
1743
|
+
stored = this.provider.readStoredRecord(ref);
|
|
1744
|
+
} catch {
|
|
1745
|
+
throw unsafe();
|
|
1746
|
+
}
|
|
1747
|
+
if (!stored) return void 0;
|
|
1748
|
+
if (stored.kind !== "oauth-session" || stored.metadata.owner.distribution !== "public") throw new OAuthCredentialError("invalid_profile");
|
|
1749
|
+
const who = stored.metadata.owner;
|
|
1750
|
+
if (typeof owner === "string" ? who.agent !== owner : who.loginId !== owner.loginId) throw new OAuthCredentialError("invalid_profile");
|
|
1751
|
+
return { profile: who.agent, loginId: who.loginId, state: stored.metadata.state, session: oauthSessionFromCredential(stored) };
|
|
1752
|
+
}
|
|
1753
|
+
write(ref, doc) {
|
|
1754
|
+
const before = this.provider.snapshot(ref);
|
|
1755
|
+
if (before.record && ["logged_out", "reauth_required"].includes(before.record.metadata.state)) throw loginRequired();
|
|
1756
|
+
const candidate = credentialForSession(doc);
|
|
1757
|
+
return this.provider.commit(ref, { ...candidate, metadata: { ...before.record?.metadata, ...candidate.metadata } }, before);
|
|
1758
|
+
}
|
|
1759
|
+
async withLock(ref, task) {
|
|
1760
|
+
if (!/^[a-f0-9]{32}$/.test(ref)) throw unsafe();
|
|
1761
|
+
try {
|
|
1762
|
+
return await withCredentialOperation(this.path, ref, task, { lockWaitMs: this.lockWaitMs });
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
if (error instanceof Error && error.message === "credentials: credential_busy") throw new OAuthCredentialError("credential_busy");
|
|
1765
|
+
throw error;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
function oauthSessionCandidate(profile, session, loginId) {
|
|
1770
|
+
requireProfile(profile);
|
|
1771
|
+
validateSession(session);
|
|
1772
|
+
if (loginId !== void 0) requireProfile(loginId);
|
|
1773
|
+
return credentialForSession({ profile, loginId, state: "ready", session });
|
|
1774
|
+
}
|
|
1775
|
+
function credentialForSession(doc) {
|
|
1776
|
+
const { accessToken, refreshToken, ...metadata } = doc.session;
|
|
1777
|
+
return {
|
|
1778
|
+
kind: "oauth-session",
|
|
1779
|
+
payload: ["ready", "refreshing"].includes(doc.state) ? { accessToken, refreshToken } : {},
|
|
1780
|
+
metadata: {
|
|
1781
|
+
...metadata,
|
|
1782
|
+
owner: { distribution: "public", agent: doc.profile, ...doc.loginId ? { loginId: doc.loginId } : {} },
|
|
1783
|
+
state: doc.state,
|
|
1784
|
+
authorization_method: "device-code"
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function oauthSessionFromCredential(record) {
|
|
1789
|
+
if (record.kind !== "oauth-session") throw unsafe();
|
|
1790
|
+
const m = record.metadata, p = record.payload;
|
|
1791
|
+
const session = {
|
|
1792
|
+
baseUrl: m.baseUrl,
|
|
1793
|
+
workspaceId: m.workspaceId,
|
|
1794
|
+
memberId: m.memberId,
|
|
1795
|
+
clientId: m.clientId,
|
|
1796
|
+
scope: m.scope,
|
|
1797
|
+
sessionId: m.sessionId,
|
|
1798
|
+
accountDisplay: m.accountDisplay,
|
|
1799
|
+
issuedAt: m.issuedAt,
|
|
1800
|
+
expiresAt: m.expiresAt,
|
|
1801
|
+
accessToken: p.accessToken,
|
|
1802
|
+
refreshToken: p.refreshToken
|
|
1803
|
+
};
|
|
1804
|
+
validateSession(session, ["ready", "refreshing"].includes(m.state));
|
|
1805
|
+
return session;
|
|
1806
|
+
}
|
|
1807
|
+
function requireProfile(profile) {
|
|
1808
|
+
if (typeof profile !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(profile)) throw new OAuthCredentialError("invalid_profile");
|
|
1809
|
+
}
|
|
1810
|
+
function validateSession(s, requireTokens = true) {
|
|
1811
|
+
if (!s || typeof s !== "object" || s.clientId !== "ctxdb-cli" || !["read_only", "read_write", "default"].includes(s.scope) || !Number.isSafeInteger(s.issuedAt) || !Number.isSafeInteger(s.expiresAt) || s.expiresAt - s.issuedAt < 1e3 || s.expiresAt - s.issuedAt > 36e5) throw unsafe();
|
|
1812
|
+
for (const value of [s.workspaceId, s.memberId, s.sessionId, s.accountDisplay]) {
|
|
1813
|
+
if (typeof value !== "string" || !value || value.length > 256 || /[\r\n\x00-\x1f]/.test(value)) throw unsafe();
|
|
1814
|
+
}
|
|
1815
|
+
for (const value of requireTokens ? [s.accessToken, s.refreshToken] : []) {
|
|
1816
|
+
if (typeof value !== "string" || !value || value.length > 8192 || /\s/.test(value)) throw unsafe();
|
|
1817
|
+
}
|
|
1818
|
+
let url;
|
|
1819
|
+
try {
|
|
1820
|
+
url = new URL(s.baseUrl);
|
|
1821
|
+
} catch {
|
|
1822
|
+
throw unsafe();
|
|
1823
|
+
}
|
|
1824
|
+
const loopback = ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname);
|
|
1825
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.protocol === "https:" || loopback && url.protocol === "http:")) throw unsafe();
|
|
1826
|
+
}
|
|
1827
|
+
function rotated(current, response, issuedAt) {
|
|
1828
|
+
if (!response || response.token_type !== "Bearer" || response.session_id !== current.sessionId || response.scope !== current.scope || !Number.isSafeInteger(response.expires_in) || response.expires_in < 1 || response.expires_in > 3600 || response.refresh_token === current.refreshToken) throw loginRequired();
|
|
1829
|
+
const next = {
|
|
1830
|
+
...current,
|
|
1831
|
+
accessToken: response.access_token,
|
|
1832
|
+
refreshToken: response.refresh_token,
|
|
1833
|
+
issuedAt,
|
|
1834
|
+
expiresAt: issuedAt + response.expires_in * 1e3
|
|
1835
|
+
};
|
|
1836
|
+
validateSession(next);
|
|
1837
|
+
return next;
|
|
1838
|
+
}
|
|
1839
|
+
function authorization(session) {
|
|
1840
|
+
return { scheme: "Bearer", value: session.accessToken, baseUrl: session.baseUrl, expiresAt: session.expiresAt };
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// src/oauth-profile.ts
|
|
1844
|
+
import { dirname as dirname4, join as join4, resolve } from "path";
|
|
1845
|
+
|
|
1846
|
+
// src/auth-config.ts
|
|
1847
|
+
import { closeSync as closeSync3, existsSync as existsSync3, fsyncSync as fsyncSync2, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync3, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1848
|
+
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
1849
|
+
import { dirname as dirname3 } from "path";
|
|
1850
|
+
var AuthConfigError = class extends Error {
|
|
1851
|
+
constructor(message) {
|
|
1852
|
+
super(message);
|
|
1853
|
+
this.name = "AuthConfigError";
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
function readAuthConfig(path) {
|
|
1857
|
+
if (!existsSync3(path)) return { version: 2, agents: {} };
|
|
1858
|
+
let raw;
|
|
1859
|
+
try {
|
|
1860
|
+
raw = JSON.parse(readFileSync3(path, "utf8"));
|
|
1861
|
+
} catch {
|
|
1862
|
+
throw new AuthConfigError("Invalid ctxdb.json; repair the JSON before changing authentication.");
|
|
1863
|
+
}
|
|
1864
|
+
if (raw?.version !== 2 || !object3(raw.agents) || raw.logins !== void 0 && !object3(raw.logins)) {
|
|
1865
|
+
throw new AuthConfigError("Unsupported ctxdb configuration; authentication was not changed.");
|
|
1866
|
+
}
|
|
1867
|
+
return raw;
|
|
1868
|
+
}
|
|
1869
|
+
function object3(value) {
|
|
1870
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1871
|
+
}
|
|
1872
|
+
function ownAuthProfile(raw, agent) {
|
|
1873
|
+
const value = Object.hasOwn(raw.agents, agent) ? raw.agents[agent] : void 0;
|
|
1874
|
+
if (value !== void 0 && !object3(value)) throw new AuthConfigError("Invalid agent configuration; repair its section in ctxdb.json.");
|
|
1875
|
+
return value ?? {};
|
|
1876
|
+
}
|
|
1877
|
+
function requireLoginId(id) {
|
|
1878
|
+
if (typeof id !== "string" || id === "default" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(id) || ["__proto__", "constructor", "prototype"].includes(id)) throw new AuthConfigError("Invalid login ID; use an ID from ctxdb auth status --all.");
|
|
1879
|
+
}
|
|
1880
|
+
function newLoginId(agent, logins) {
|
|
1881
|
+
requireLoginId(`${agent}-00000000`);
|
|
1882
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
1883
|
+
const id = `${agent}-${randomBytes4(4).toString("hex")}`;
|
|
1884
|
+
if (!Object.hasOwn(logins ?? {}, id)) return id;
|
|
1885
|
+
}
|
|
1886
|
+
throw new AuthConfigError("Cannot allocate a unique login ID; retry login.");
|
|
1887
|
+
}
|
|
1888
|
+
function loginUsers(raw, id) {
|
|
1889
|
+
return Object.keys(raw.agents).filter((agent) => ownAuthProfile(raw, agent).access_credential === id).sort();
|
|
1890
|
+
}
|
|
1891
|
+
function validateLoginChoice(section) {
|
|
1892
|
+
if (Object.hasOwn(section, "credential_ref")) {
|
|
1893
|
+
if (typeof section.credential_ref !== "string" || !/^[a-f0-9]{32}$/.test(section.credential_ref)) throw new AuthConfigError("Invalid credential_ref; explicitly select --api-key or --login again.");
|
|
1894
|
+
if (Object.hasOwn(section, "access_credential") || section.api_key || section.oauth_credential_ref) throw new AuthConfigError("Conflicting credential_ref and access_credential/api_key/oauth_credential_ref. Keep one authentication choice.");
|
|
1895
|
+
}
|
|
1896
|
+
if (!Object.hasOwn(section, "access_credential")) return;
|
|
1897
|
+
if (section.access_credential === null) return;
|
|
1898
|
+
requireLoginId(section.access_credential);
|
|
1899
|
+
if (section.api_key || section.oauth_credential_ref) {
|
|
1900
|
+
throw new AuthConfigError("Conflicting access_credential and api_key/oauth_credential_ref in this Agent. Keep one authentication choice, or use setup --api-key/--login to replace it.");
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
function authSnapshot(raw, agent) {
|
|
1904
|
+
const s = ownAuthProfile(raw, agent), d = ownAuthProfile(raw, "default");
|
|
1905
|
+
const fields = (s2) => ["credential_ref", "access_credential", "oauth_credential_ref", "api_key", "base_url"].map((k) => [k, Object.hasOwn(s2, k), s2[k]]);
|
|
1906
|
+
return createHash3("sha256").update(JSON.stringify([
|
|
1907
|
+
fields(s),
|
|
1908
|
+
s.access_credential ? raw.logins?.[s.access_credential] : void 0,
|
|
1909
|
+
agent !== "default" && !Object.hasOwn(s, "credential_ref") && !Object.hasOwn(s, "access_credential") && !s.oauth_credential_ref && !Object.hasOwn(s, "api_key") ? fields(d) : void 0
|
|
1910
|
+
])).digest("hex");
|
|
1911
|
+
}
|
|
1912
|
+
function assertAuthSnapshot(raw, agent, expected) {
|
|
1913
|
+
if (authSnapshot(raw, agent) !== expected) throw new AuthConfigError("Authentication changed while this operation was running. The newer choice was preserved; retry the command.");
|
|
1914
|
+
}
|
|
1915
|
+
function preserveDefaultApiKey(raw, store) {
|
|
1916
|
+
const d = ownAuthProfile(raw, "default");
|
|
1917
|
+
if (typeof d.api_key !== "string" || !d.api_key) return;
|
|
1918
|
+
for (const agent of Object.keys(raw.agents)) {
|
|
1919
|
+
if (agent === "default") continue;
|
|
1920
|
+
const s = ownAuthProfile(raw, agent);
|
|
1921
|
+
if (Object.hasOwn(s, "credential_ref") || Object.hasOwn(s, "api_key") || Object.hasOwn(s, "access_credential") || s.oauth_credential_ref) continue;
|
|
1922
|
+
if (!Object.hasOwn(s, "base_url")) s.base_url = d.base_url ?? "https://api.cn-hangzhou.agentcontext.aliyuncs.com";
|
|
1923
|
+
if (store) s.credential_ref = store(agent, d.api_key, s.base_url);
|
|
1924
|
+
else s.api_key = d.api_key;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
function rejectAuthEnvironment(env) {
|
|
1928
|
+
if (env.CTXDB_ACCESS_TOKEN?.trim() || env.CTXDB_API_KEY || env.CTXDB_BASE_URL) {
|
|
1929
|
+
throw new AuthConfigError("Unset CTXDB_ACCESS_TOKEN, CTXDB_API_KEY and CTXDB_BASE_URL before switching authentication; use --server or --base-url explicitly.");
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
function withAuthConfigLock(path, task) {
|
|
1933
|
+
mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
|
|
1934
|
+
const lock = path + ".oauth-config.lock", deadline = Date.now() + 1e4;
|
|
1935
|
+
let fd;
|
|
1936
|
+
while (true) {
|
|
1937
|
+
try {
|
|
1938
|
+
fd = openSync3(lock, "wx", 384);
|
|
1939
|
+
break;
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
if (error.code !== "EEXIST") throw new AuthConfigError("Cannot lock ctxdb configuration.");
|
|
1942
|
+
if (Date.now() >= deadline) throw new AuthConfigError("Configuration is locked. Check the previous CLI process before removing a stale .oauth-config.lock file.");
|
|
1943
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
try {
|
|
1947
|
+
return task();
|
|
1948
|
+
} finally {
|
|
1949
|
+
closeSync3(fd);
|
|
1950
|
+
unlinkSync2(lock);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
function updateAuthConfig(path, task) {
|
|
1954
|
+
return withAuthConfigLock(path, () => {
|
|
1955
|
+
const raw = readAuthConfig(path), result = task(raw);
|
|
1956
|
+
writeAuthConfig(path, raw);
|
|
1957
|
+
return result;
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
function writeAuthConfig(path, raw) {
|
|
1961
|
+
const temporary = path + "." + randomBytes4(12).toString("hex") + ".tmp";
|
|
1962
|
+
let fd;
|
|
1963
|
+
try {
|
|
1964
|
+
fd = openSync3(temporary, "wx", 384);
|
|
1965
|
+
writeFileSync2(fd, JSON.stringify(raw, null, 2) + "\n");
|
|
1966
|
+
fsyncSync2(fd);
|
|
1967
|
+
closeSync3(fd);
|
|
1968
|
+
fd = void 0;
|
|
1969
|
+
renameSync3(temporary, path);
|
|
1970
|
+
if (process.platform !== "win32") {
|
|
1971
|
+
try {
|
|
1972
|
+
const dir = openSync3(dirname3(path), "r");
|
|
1973
|
+
try {
|
|
1974
|
+
fsyncSync2(dir);
|
|
1975
|
+
} finally {
|
|
1976
|
+
closeSync3(dir);
|
|
1977
|
+
}
|
|
1978
|
+
} catch {
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
} finally {
|
|
1982
|
+
if (fd !== void 0) closeSync3(fd);
|
|
1983
|
+
if (existsSync3(temporary)) unlinkSync2(temporary);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/oauth-profile.ts
|
|
1988
|
+
function oauthProfile(reference, configPath, profile, baseUrl) {
|
|
1989
|
+
if (reference === void 0) return void 0;
|
|
1990
|
+
if (typeof reference !== "string" || !/^[a-f0-9]{32}$/.test(reference) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(profile)) throw new OAuthCredentialError("unsafe_storage");
|
|
1991
|
+
const path = resolve(configPath);
|
|
1992
|
+
return {
|
|
1993
|
+
reference,
|
|
1994
|
+
profile,
|
|
1995
|
+
configPath: path,
|
|
1996
|
+
credentialsPath: join4(dirname4(path), "credentials.json"),
|
|
1997
|
+
baseUrl: normalizeCoreOrigin(baseUrl)
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
function oauthProfileFromConfig(raw, path, agent, baseUrlOverride) {
|
|
2001
|
+
const section = ownAuthProfile(raw, agent);
|
|
2002
|
+
validateLoginChoice(section);
|
|
2003
|
+
if (section.access_credential === null) return void 0;
|
|
2004
|
+
if (Object.hasOwn(section, "access_credential")) {
|
|
2005
|
+
const id = section.access_credential;
|
|
2006
|
+
requireLoginId(id);
|
|
2007
|
+
const record = raw.logins && Object.hasOwn(raw.logins, id) ? raw.logins[id] : void 0;
|
|
2008
|
+
if (!record?.credential_ref) throw new AuthConfigError("access_credential points to a missing login. Use ctxdb auth status --all and explicitly bind a valid login or log in again.");
|
|
2009
|
+
return storedProfile(record.credential_ref, path, agent, id, baseUrlOverride);
|
|
2010
|
+
}
|
|
2011
|
+
return storedProfile(section.oauth_credential_ref, path, agent, void 0, baseUrlOverride);
|
|
2012
|
+
}
|
|
2013
|
+
function storedProfile(reference, path, agent, loginId, baseUrlOverride) {
|
|
2014
|
+
const descriptor = oauthProfile(reference, path, agent, "https://api.cn-hangzhou.agentcontext.aliyuncs.com");
|
|
2015
|
+
if (!descriptor) return void 0;
|
|
2016
|
+
const profile = { ...descriptor, ...loginId ? { loginId } : {} };
|
|
2017
|
+
const status = new OAuthSessionStore(profile.credentialsPath).status(profile.reference, oauthOwner(profile));
|
|
2018
|
+
if (!("baseUrl" in status) || typeof status.baseUrl !== "string") throw new OAuthCredentialError("login_required");
|
|
2019
|
+
profile.baseUrl = normalizeCoreOrigin(status.baseUrl);
|
|
2020
|
+
if (baseUrlOverride && normalizeCoreOrigin(baseUrlOverride) !== profile.baseUrl) {
|
|
2021
|
+
throw new AuthConfigError("Requested base URL does not match the login Core origin; no token was sent.");
|
|
2022
|
+
}
|
|
2023
|
+
return profile;
|
|
2024
|
+
}
|
|
2025
|
+
function oauthOwner(profile) {
|
|
2026
|
+
return profile.loginId ? { loginId: profile.loginId } : profile.profile;
|
|
2027
|
+
}
|
|
2028
|
+
function oauthProfileStatus(profile) {
|
|
2029
|
+
const status = new OAuthSessionStore(profile.credentialsPath).status(profile.reference, oauthOwner(profile));
|
|
2030
|
+
if (typeof status.baseUrl === "string" && normalizeCoreOrigin(status.baseUrl) !== profile.baseUrl) {
|
|
2031
|
+
throw new AuthConfigError("Requested base URL does not match the login Core origin; no token was sent.");
|
|
2032
|
+
}
|
|
2033
|
+
return status;
|
|
2034
|
+
}
|
|
2035
|
+
var OAuthProfileProvider = class {
|
|
2036
|
+
selected;
|
|
2037
|
+
constructor(selected) {
|
|
2038
|
+
this.selected = selected;
|
|
2039
|
+
}
|
|
2040
|
+
async resolve(options = {}) {
|
|
2041
|
+
const timeoutMs = options.timeoutMs ?? 3e4, deadline = Date.now() + timeoutMs;
|
|
2042
|
+
const selected = this.selected;
|
|
2043
|
+
const readCurrent = () => {
|
|
2044
|
+
const raw = readAuthConfig(selected.configPath);
|
|
2045
|
+
return oauthProfileFromConfig(raw, selected.configPath, selected.profile);
|
|
2046
|
+
};
|
|
2047
|
+
const current = readCurrent();
|
|
2048
|
+
if (!current || current.baseUrl !== selected.baseUrl) throw new OAuthCredentialError("login_required");
|
|
2049
|
+
const store = new OAuthSessionStore(current.credentialsPath, { lockWaitMs: timeoutMs });
|
|
2050
|
+
const status = oauthProfileStatus(current);
|
|
2051
|
+
if (!("baseUrl" in status) || typeof status.baseUrl !== "string" || normalizeCoreOrigin(status.baseUrl) !== current.baseUrl) throw new OAuthCredentialError("login_required");
|
|
2052
|
+
const authorization2 = await store.resolve(current.reference, oauthOwner(current), {
|
|
2053
|
+
refresh: (rt, url) => new CoreDeviceClient(url, { requestTimeoutMs: Math.max(1, deadline - Date.now()) }).refresh(rt, url)
|
|
2054
|
+
});
|
|
2055
|
+
const after = readCurrent();
|
|
2056
|
+
if (!after || after.reference !== current.reference || after.loginId !== current.loginId || after.baseUrl !== current.baseUrl || oauthProfileStatus(after).state !== "ready") throw new OAuthCredentialError("login_required");
|
|
2057
|
+
return authorization2;
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
|
|
2061
|
+
// src/login-bindings.ts
|
|
2062
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
2063
|
+
import { dirname as dirname6, join as join6 } from "path";
|
|
2064
|
+
|
|
2065
|
+
// src/data-auth.ts
|
|
2066
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
2067
|
+
function selectDataApiCredential(cfg, env = process.env) {
|
|
2068
|
+
const accessToken = env.CTXDB_ACCESS_TOKEN?.trim();
|
|
2069
|
+
if (accessToken) return { type: "access-token", value: accessToken };
|
|
2070
|
+
if (env.CTXDB_API_KEY) return { type: "api-key", value: env.CTXDB_API_KEY };
|
|
2071
|
+
const context = cfg.authContext ? { context: cfg.authContext } : {};
|
|
2072
|
+
if (cfg.oauthCredential) return { type: "oauth-session", profile: cfg.oauthCredential, ...context };
|
|
2073
|
+
return cfg.apiKey ? { type: "api-key", value: cfg.apiKey, ...context } : null;
|
|
2074
|
+
}
|
|
2075
|
+
var DataApiAuthorizationProvider = class {
|
|
2076
|
+
context;
|
|
2077
|
+
constructor(context) {
|
|
2078
|
+
this.context = context;
|
|
2079
|
+
}
|
|
2080
|
+
async resolve(options = {}) {
|
|
2081
|
+
const c = this.context, raw = readAuthConfig(c.configPath), section = ownAuthProfile(raw, c.agent);
|
|
2082
|
+
const fallback = c.inheritDefaultKey && c.agent !== "default" ? ownAuthProfile(raw, "default") : {};
|
|
2083
|
+
if (c.managedCredentials) {
|
|
2084
|
+
const active = readActiveCredentialSync(c.credentialsPath ?? join5(dirname5(c.configPath), "credentials.json"));
|
|
2085
|
+
if (active) {
|
|
2086
|
+
const baseUrl2 = c.baseUrlOverride ?? active.metadata.baseUrl;
|
|
2087
|
+
if (baseUrl2 !== c.baseUrl) throw new OAuthCredentialError("login_required");
|
|
2088
|
+
return { scheme: "Token", value: active.payload.apiKey, baseUrl: baseUrl2, expiresAt: null };
|
|
2089
|
+
}
|
|
2090
|
+
throw new OAuthCredentialError("login_required");
|
|
2091
|
+
}
|
|
2092
|
+
const stored = publicAuthenticationFromConfig(raw, c.configPath, c.agent, c.baseUrlOverride);
|
|
2093
|
+
const baseUrl = stored?.baseUrl ?? String(c.baseUrlOverride || section.base_url || fallback.base_url || "https://api.cn-hangzhou.agentcontext.aliyuncs.com").replace(/\/+$/, "");
|
|
2094
|
+
if (baseUrl !== c.baseUrl) throw new OAuthCredentialError("login_required");
|
|
2095
|
+
if (stored?.apiKey) return { scheme: "Token", value: stored.apiKey, baseUrl, expiresAt: null };
|
|
2096
|
+
if (stored?.oauthCredential) return new OAuthProfileProvider(stored.oauthCredential).resolve(options);
|
|
2097
|
+
if (Object.hasOwn(section, "access_credential")) throw new OAuthCredentialError("login_required");
|
|
2098
|
+
const key = Object.hasOwn(section, "api_key") ? section.api_key : fallback.api_key;
|
|
2099
|
+
if (typeof key !== "string" || !key) throw new OAuthCredentialError("login_required");
|
|
2100
|
+
return { scheme: "Token", value: key, baseUrl, expiresAt: null };
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
function publicAuthenticationFromConfig(raw, path, agent, baseUrlOverride) {
|
|
2104
|
+
const section = ownAuthProfile(raw, agent);
|
|
2105
|
+
validateLoginChoice(section);
|
|
2106
|
+
if (!Object.hasOwn(section, "credential_ref")) {
|
|
2107
|
+
const oauthCredential = oauthProfileFromConfig(raw, path, agent, baseUrlOverride);
|
|
2108
|
+
return oauthCredential ? { baseUrl: oauthCredential.baseUrl, oauthCredential } : void 0;
|
|
2109
|
+
}
|
|
2110
|
+
const r = new LocalCredentialProvider(join5(dirname5(path), "credentials.json")).readStoredRecord(section.credential_ref);
|
|
2111
|
+
if (!r || r.kind !== "api-key" || r.metadata.state !== "ready" || r.metadata.owner.distribution !== "public" || r.metadata.owner.agent !== agent || r.metadata.owner.loginId) {
|
|
2112
|
+
throw new AuthConfigError("credential_ref is missing, invalid or belongs to another Agent. Use setup --api-key/--login to choose a valid credential.");
|
|
2113
|
+
}
|
|
2114
|
+
const baseUrl = String(r.metadata.base_url).replace(/\/+$/, "");
|
|
2115
|
+
if (baseUrlOverride && baseUrl !== baseUrlOverride.replace(/\/+$/, "")) throw new AuthConfigError("Requested base URL does not match the credential origin; no credential was sent.");
|
|
2116
|
+
return { baseUrl, apiKey: r.payload.api_key };
|
|
2117
|
+
}
|
|
2118
|
+
function publicApiKeyFromConfig(raw, path, agent, baseUrl) {
|
|
2119
|
+
return publicAuthenticationFromConfig(raw, path, agent, baseUrl)?.apiKey;
|
|
2120
|
+
}
|
|
2121
|
+
function createRequestAuthorizationProvider(credential, baseUrl) {
|
|
2122
|
+
if (credential?.context) return new DataApiAuthorizationProvider(credential.context);
|
|
2123
|
+
if (credential?.type === "oauth-session") return new OAuthProfileProvider(credential.profile);
|
|
2124
|
+
return { async resolve(_options) {
|
|
2125
|
+
if (!credential) throw new OAuthCredentialError("login_required");
|
|
2126
|
+
return {
|
|
2127
|
+
scheme: credential.type === "access-token" ? "Bearer" : "Token",
|
|
2128
|
+
value: credential.value,
|
|
2129
|
+
baseUrl,
|
|
2130
|
+
expiresAt: null
|
|
2131
|
+
};
|
|
2132
|
+
} };
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// src/data-endpoint.ts
|
|
2136
|
+
var DEFAULT_DATA_BASE_URL = "https://api.cn-hangzhou.agentcontext.aliyuncs.com";
|
|
2137
|
+
function validateDataBaseUrl(value, coreOrigin = false) {
|
|
2138
|
+
try {
|
|
2139
|
+
if (typeof value !== "string" || !value || /[\s\x00-\x1f\x7f]/.test(value) || !/^https?:\/\//i.test(value)) throw new Error();
|
|
2140
|
+
const url = new URL(value);
|
|
2141
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || url.search || url.hash) throw new Error();
|
|
2142
|
+
if (coreOrigin) normalizeCoreOrigin(value);
|
|
2143
|
+
} catch {
|
|
2144
|
+
throw new Error(`Invalid --base-url: provide a valid service URL. Recommended: --base-url ${DEFAULT_DATA_BASE_URL}`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/login-bindings.ts
|
|
2149
|
+
function selectedLogin(path, agent) {
|
|
2150
|
+
return oauthProfileFromConfig(readAuthConfig(path), path, agent);
|
|
2151
|
+
}
|
|
2152
|
+
function namedLogin(raw, path, name) {
|
|
2153
|
+
if (name === "default") {
|
|
2154
|
+
const selected = oauthProfileFromConfig(raw, path, "default");
|
|
2155
|
+
if (!selected) throw new AuthConfigError("default has no login. Run ctxdb login first, or specify an existing login ID.");
|
|
2156
|
+
return selected;
|
|
2157
|
+
}
|
|
2158
|
+
requireLoginId(name);
|
|
2159
|
+
const ref = raw.logins && Object.hasOwn(raw.logins, name) ? raw.logins[name]?.credential_ref : void 0;
|
|
2160
|
+
if (!ref) throw new AuthConfigError("Login ID was not found. Run ctxdb auth status --all.");
|
|
2161
|
+
const temporary = { ...oauthProfile(ref, path, name, DEFAULT_DATA_BASE_URL), loginId: name };
|
|
2162
|
+
const status = new OAuthSessionStore(temporary.credentialsPath).status(ref, { loginId: name });
|
|
2163
|
+
if (!("baseUrl" in status) || typeof status.baseUrl !== "string") throw new AuthConfigError("Login credential is missing; log in again.");
|
|
2164
|
+
return { ...temporary, baseUrl: normalizeCoreOrigin(status.baseUrl) };
|
|
2165
|
+
}
|
|
2166
|
+
function setChoice(raw, agent, choice) {
|
|
2167
|
+
const s = { ...ownAuthProfile(raw, agent) };
|
|
2168
|
+
delete s.api_key;
|
|
2169
|
+
delete s.oauth_credential_ref;
|
|
2170
|
+
delete s.access_credential;
|
|
2171
|
+
delete s.credential_ref;
|
|
2172
|
+
if (choice.credentialRef !== void 0) s.credential_ref = choice.credentialRef;
|
|
2173
|
+
else s.access_credential = choice.loginId ?? null;
|
|
2174
|
+
delete s.base_url;
|
|
2175
|
+
raw.agents[agent] = s;
|
|
2176
|
+
}
|
|
2177
|
+
function groupSnapshot(raw, profile) {
|
|
2178
|
+
if (!profile?.loginId) return "";
|
|
2179
|
+
return JSON.stringify([raw.logins?.[profile.loginId], loginUsers(raw, profile.loginId).map((a) => [a, authSnapshot(raw, a)])]);
|
|
2180
|
+
}
|
|
2181
|
+
function beginLogin(path, agent, name) {
|
|
2182
|
+
const raw = readAuthConfig(path);
|
|
2183
|
+
const previous = name !== void 0 ? namedLogin(raw, path, name) : selectedLogin(path, agent);
|
|
2184
|
+
const update = name !== void 0 || agent === "default";
|
|
2185
|
+
const loginId = update && previous?.loginId ? previous.loginId : newLoginId(name !== void 0 && previous ? previous.profile : agent, raw.logins);
|
|
2186
|
+
return {
|
|
2187
|
+
path,
|
|
2188
|
+
agent,
|
|
2189
|
+
name,
|
|
2190
|
+
previous,
|
|
2191
|
+
loginId,
|
|
2192
|
+
update,
|
|
2193
|
+
snapshot: authSnapshot(raw, agent),
|
|
2194
|
+
previousState: previous ? oauthProfileStatus(previous).state : void 0,
|
|
2195
|
+
group: update ? groupSnapshot(raw, previous) : "",
|
|
2196
|
+
users: update && previous?.loginId ? loginUsers(raw, previous.loginId) : [agent]
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
function commitLogin(pending, reference, baseUrl) {
|
|
2200
|
+
const { path, agent, previous, loginId, update } = pending;
|
|
2201
|
+
const next = { ...oauthProfile(reference, path, agent, baseUrl), loginId };
|
|
2202
|
+
if (oauthProfileStatus(next).state !== "ready") throw new AuthConfigError("New credential is not ready; the old login was preserved.");
|
|
2203
|
+
updateAuthentication(path, (raw) => {
|
|
2204
|
+
assertAuthSnapshot(raw, agent, pending.snapshot);
|
|
2205
|
+
if (previous && pending.previousState !== "logged_out" && oauthProfileStatus(previous).state === "logged_out") {
|
|
2206
|
+
throw new AuthConfigError("Login was revoked during authorization; retry login. The late result was not saved.");
|
|
2207
|
+
}
|
|
2208
|
+
if (update && groupSnapshot(raw, previous) !== pending.group) throw new AuthConfigError("The shared login or its users changed during authorization. Retry and confirm the current users.");
|
|
2209
|
+
raw.logins ??= {};
|
|
2210
|
+
if (!(update && previous?.loginId) && Object.hasOwn(raw.logins, loginId)) throw new AuthConfigError("Login ID already exists; retry login.");
|
|
2211
|
+
raw.logins[loginId] = { ...raw.logins[loginId], credential_ref: reference };
|
|
2212
|
+
if (update && previous?.loginId) {
|
|
2213
|
+
for (const user of loginUsers(raw, loginId)) {
|
|
2214
|
+
oauthProfileFromConfig(raw, path, user);
|
|
2215
|
+
delete ownAuthProfile(raw, user).base_url;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
if (pending.name === void 0) setChoice(raw, agent, { loginId });
|
|
2219
|
+
else if (!previous?.loginId) setChoice(raw, "default", { loginId });
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
function unbindLogin(path, agent) {
|
|
2223
|
+
updateAuthentication(path, (raw) => {
|
|
2224
|
+
const s = ownAuthProfile(raw, agent);
|
|
2225
|
+
if (!Object.hasOwn(s, "access_credential") && !s.oauth_credential_ref) {
|
|
2226
|
+
if (s.api_key || s.credential_ref) throw new AuthConfigError("This Agent uses an API key, not a local login binding.");
|
|
2227
|
+
}
|
|
2228
|
+
setChoice(raw, agent, { loginId: null });
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
async function bindLogin(path, agent, name) {
|
|
2232
|
+
const raw = readAuthConfig(path), expected = authSnapshot(raw, agent), source = namedLogin(raw, path, name);
|
|
2233
|
+
const status = oauthProfileStatus(source);
|
|
2234
|
+
if (status.state !== "ready") throw new AuthConfigError("Selected login is not ready. Run ctxdb login to authorize it again before binding.");
|
|
2235
|
+
const loginId = source.loginId ?? newLoginId(source.profile, raw.logins);
|
|
2236
|
+
const sourceSnapshot = source.loginId ? JSON.stringify(raw.logins?.[source.loginId]) : authSnapshot(raw, "default");
|
|
2237
|
+
const commit = () => updateAuthentication(path, (current) => {
|
|
2238
|
+
assertAuthSnapshot(current, agent, expected);
|
|
2239
|
+
if (source.loginId ? JSON.stringify(current.logins?.[source.loginId]) !== sourceSnapshot : authSnapshot(current, "default") !== sourceSnapshot) throw new AuthConfigError("Source login changed; retry setup --login.");
|
|
2240
|
+
if (oauthProfileStatus({ ...source, loginId }).state !== "ready") throw new AuthConfigError("Source login is no longer ready; log in again.");
|
|
2241
|
+
if (!source.loginId) {
|
|
2242
|
+
current.logins ??= {};
|
|
2243
|
+
if (Object.hasOwn(current.logins, loginId)) throw new AuthConfigError("Login ID already exists; retry setup --login.");
|
|
2244
|
+
current.logins[loginId] = { credential_ref: source.reference };
|
|
2245
|
+
setChoice(current, "default", { loginId });
|
|
2246
|
+
}
|
|
2247
|
+
setChoice(current, agent, { loginId });
|
|
2248
|
+
});
|
|
2249
|
+
if (source.loginId) commit();
|
|
2250
|
+
else await new OAuthSessionStore(source.credentialsPath).share(source.reference, source.profile, loginId, commit);
|
|
2251
|
+
}
|
|
2252
|
+
async function setupAuthentication(path, agent, options, env = process.env) {
|
|
2253
|
+
if (options.baseUrl !== void 0) validateDataBaseUrl(options.baseUrl);
|
|
2254
|
+
await selectSetupAuthentication(path, agent, options, env);
|
|
2255
|
+
if (options.baseUrl === void 0 && !options.preserveBaseUrl) await resetSetupBaseUrl(path, agent, env);
|
|
2256
|
+
}
|
|
2257
|
+
async function selectSetupAuthentication(path, agent, options, env = process.env) {
|
|
2258
|
+
if (options.apiKey !== void 0 && options.login !== void 0) throw new AuthConfigError("--api-key and --login are mutually exclusive.");
|
|
2259
|
+
if (options.apiKey !== void 0) {
|
|
2260
|
+
rejectAuthEnvironment(env);
|
|
2261
|
+
if (!options.apiKey.trim()) throw new AuthConfigError("--api-key requires a nonempty value.");
|
|
2262
|
+
const raw2 = readAuthConfig(path);
|
|
2263
|
+
const baseUrl = options.baseUrl ?? (options.preserveBaseUrl ? replacementBaseUrl(raw2, path, agent) : DEFAULT_DATA_BASE_URL);
|
|
2264
|
+
saveSetupKey(path, agent, raw2, options.apiKey, baseUrl);
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (options.login !== void 0) {
|
|
2268
|
+
rejectAuthEnvironment(env);
|
|
2269
|
+
const source = namedLogin(readAuthConfig(path), path, options.login);
|
|
2270
|
+
if (options.baseUrl && normalizeCoreOrigin(options.baseUrl) !== source.baseUrl) throw new AuthConfigError("--base-url must match the selected login Core origin.");
|
|
2271
|
+
await bindLogin(path, agent, options.login);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const raw = readAuthConfig(path), s = ownAuthProfile(raw, agent);
|
|
2275
|
+
if (Object.hasOwn(s, "credential_ref") || Object.hasOwn(s, "access_credential") || Object.hasOwn(s, "oauth_credential_ref") || s.api_key) {
|
|
2276
|
+
if (options.baseUrl) publicAuthenticationFromConfig(raw, path, agent, options.baseUrl);
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
if (env.CTXDB_ACCESS_TOKEN?.trim() || env.CTXDB_API_KEY) return;
|
|
2280
|
+
const d = ownAuthProfile(raw, "default");
|
|
2281
|
+
if (!Object.hasOwn(s, "api_key") && d.api_key) return;
|
|
2282
|
+
if (agent !== "default" && (d.access_credential || d.oauth_credential_ref)) {
|
|
2283
|
+
rejectAuthEnvironment(env);
|
|
2284
|
+
const source = namedLogin(raw, path, "default");
|
|
2285
|
+
if (options.baseUrl && normalizeCoreOrigin(options.baseUrl) !== source.baseUrl) throw new AuthConfigError("--base-url must match the default login Core origin.");
|
|
2286
|
+
await bindLogin(path, agent, "default");
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
function saveSetupKey(path, agent, raw, apiKey, baseUrl) {
|
|
2290
|
+
const provider = new LocalCredentialProvider(join6(dirname6(path), "credentials.json"));
|
|
2291
|
+
const previousRef = ownAuthProfile(raw, agent).credential_ref;
|
|
2292
|
+
const previous = typeof previousRef === "string" && /^[a-f0-9]{32}$/.test(previousRef) ? provider.snapshot(previousRef) : void 0;
|
|
2293
|
+
const expected = authSnapshot(raw, agent), ref = randomBytes5(16).toString("hex");
|
|
2294
|
+
const receipt = provider.commit(ref, keyCandidate(agent, apiKey, baseUrl));
|
|
2295
|
+
try {
|
|
2296
|
+
updateAuthentication(path, (current) => {
|
|
2297
|
+
assertAuthSnapshot(current, agent, expected);
|
|
2298
|
+
setChoice(current, agent, { credentialRef: ref });
|
|
2299
|
+
});
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
try {
|
|
2302
|
+
provider.rollback(receipt);
|
|
2303
|
+
} catch {
|
|
2304
|
+
}
|
|
2305
|
+
throw error;
|
|
2306
|
+
}
|
|
2307
|
+
const old = previous?.record;
|
|
2308
|
+
if (old?.kind !== "api-key" || old.metadata.owner.distribution !== "public" || old.metadata.owner.agent !== agent || old.metadata.owner.loginId) return;
|
|
2309
|
+
try {
|
|
2310
|
+
provider.transaction((tx) => withAuthConfigLock(path, () => {
|
|
2311
|
+
const current = readAuthConfig(path);
|
|
2312
|
+
if (Object.values(current.agents).some((s) => s.credential_ref === previousRef || s.oauth_credential_ref === previousRef) || Object.values(current.logins ?? {}).some((s) => s.credential_ref === previousRef)) return;
|
|
2313
|
+
if (tx.read(previousRef).fingerprint === previous.fingerprint) tx.remove(previous);
|
|
2314
|
+
}));
|
|
2315
|
+
} catch {
|
|
2316
|
+
throw new AuthConfigError("New API key was saved, but cleanup of the previous local key could not be confirmed. Inspect credentials.json before retrying.");
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
async function resetSetupBaseUrl(path, agent, env) {
|
|
2320
|
+
const raw = readAuthConfig(path), s = ownAuthProfile(raw, agent);
|
|
2321
|
+
const stored = publicAuthenticationFromConfig(raw, path, agent);
|
|
2322
|
+
if (stored?.baseUrl === DEFAULT_DATA_BASE_URL) return;
|
|
2323
|
+
const profile = stored?.oauthCredential;
|
|
2324
|
+
if (profile) {
|
|
2325
|
+
rejectAuthEnvironment(env);
|
|
2326
|
+
const expected = authSnapshot(raw, agent), group = groupSnapshot(raw, profile);
|
|
2327
|
+
await new OAuthSessionStore(profile.credentialsPath).changeBaseUrl(profile.reference, oauthOwner(profile), DEFAULT_DATA_BASE_URL, () => {
|
|
2328
|
+
const current = readAuthConfig(path);
|
|
2329
|
+
assertAuthSnapshot(current, agent, expected);
|
|
2330
|
+
if (groupSnapshot(current, profile) !== group) throw new AuthConfigError("The shared login or its users changed during setup; retry setup.");
|
|
2331
|
+
});
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
if (Object.hasOwn(s, "access_credential")) return;
|
|
2335
|
+
const d = ownAuthProfile(raw, "default");
|
|
2336
|
+
const key = stored?.apiKey ?? (Object.hasOwn(s, "api_key") ? s.api_key : d.api_key);
|
|
2337
|
+
if (typeof key !== "string" || !key) return;
|
|
2338
|
+
const baseUrl = stored?.baseUrl ?? s.base_url ?? d.base_url ?? DEFAULT_DATA_BASE_URL;
|
|
2339
|
+
if (baseUrl === DEFAULT_DATA_BASE_URL) return;
|
|
2340
|
+
rejectAuthEnvironment(env);
|
|
2341
|
+
if (stored?.apiKey) {
|
|
2342
|
+
const expected = authSnapshot(raw, agent), provider = new LocalCredentialProvider(join6(dirname6(path), "credentials.json"));
|
|
2343
|
+
provider.transaction((tx) => withAuthConfigLock(path, () => {
|
|
2344
|
+
const current = readAuthConfig(path);
|
|
2345
|
+
assertAuthSnapshot(current, agent, expected);
|
|
2346
|
+
publicAuthenticationFromConfig(current, path, agent);
|
|
2347
|
+
const before = tx.read(s.credential_ref);
|
|
2348
|
+
if (!before.record) throw new AuthConfigError("API key credential changed during setup; retry setup.");
|
|
2349
|
+
tx.put(s.credential_ref, { ...before.record, metadata: { ...before.record.metadata, base_url: DEFAULT_DATA_BASE_URL } }, before);
|
|
2350
|
+
}));
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
saveSetupKey(path, agent, raw, key, DEFAULT_DATA_BASE_URL);
|
|
2354
|
+
}
|
|
2355
|
+
function replacementBaseUrl(raw, path, agent) {
|
|
2356
|
+
for (const candidate of agent === "default" ? [agent] : [agent, "default"]) {
|
|
2357
|
+
try {
|
|
2358
|
+
const stored = publicAuthenticationFromConfig(raw, path, candidate);
|
|
2359
|
+
if (stored) return stored.baseUrl;
|
|
2360
|
+
} catch {
|
|
2361
|
+
}
|
|
2362
|
+
const baseUrl = ownAuthProfile(raw, candidate).base_url;
|
|
2363
|
+
if (typeof baseUrl === "string" && baseUrl) return baseUrl;
|
|
2364
|
+
}
|
|
2365
|
+
return DEFAULT_DATA_BASE_URL;
|
|
2366
|
+
}
|
|
2367
|
+
function keyCandidate(agent, apiKey, baseUrl) {
|
|
2368
|
+
return { kind: "api-key", payload: { api_key: apiKey }, metadata: { base_url: baseUrl, owner: { distribution: "public", agent }, state: "ready", authorization_method: "api-key" } };
|
|
2369
|
+
}
|
|
2370
|
+
function updateAuthentication(path, task) {
|
|
2371
|
+
return new LocalCredentialProvider(join6(dirname6(path), "credentials.json")).transaction((tx) => {
|
|
2372
|
+
const created = [];
|
|
2373
|
+
try {
|
|
2374
|
+
return updateAuthConfig(path, (raw) => {
|
|
2375
|
+
const before = structuredClone(raw), snapshot2 = authSnapshot(raw, "default");
|
|
2376
|
+
const result = task(raw), d = ownAuthProfile(before, "default");
|
|
2377
|
+
if (snapshot2 !== authSnapshot(raw, "default") && typeof d.api_key === "string" && d.api_key) {
|
|
2378
|
+
const selected = (s) => ["credential_ref", "access_credential", "api_key"].some((k) => Object.hasOwn(s, k)) || !!s.oauth_credential_ref;
|
|
2379
|
+
for (const agent of Object.keys(before.agents)) {
|
|
2380
|
+
if (agent === "default") continue;
|
|
2381
|
+
const prior = ownAuthProfile(before, agent), current = ownAuthProfile(raw, agent);
|
|
2382
|
+
if (selected(prior) || selected(current)) continue;
|
|
2383
|
+
const baseUrl = prior.base_url ?? d.base_url ?? DEFAULT_DATA_BASE_URL, ref = randomBytes5(16).toString("hex");
|
|
2384
|
+
created.push(tx.put(ref, keyCandidate(agent, d.api_key, baseUrl)));
|
|
2385
|
+
current.credential_ref = ref;
|
|
2386
|
+
delete current.base_url;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
return result;
|
|
2390
|
+
});
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
for (const receipt of created) {
|
|
2393
|
+
try {
|
|
2394
|
+
tx.remove(receipt.after);
|
|
2395
|
+
} catch {
|
|
2396
|
+
try {
|
|
2397
|
+
process.stderr.write("Credential cleanup could not be confirmed; inspect auth status before retrying.\n");
|
|
2398
|
+
} catch {
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
throw error;
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// src/login-cleanup.ts
|
|
2408
|
+
function referenced(raw, reference) {
|
|
2409
|
+
return Object.values(raw.agents).some((s) => s.oauth_credential_ref === reference || s.credential_ref === reference) || Object.entries(raw.logins ?? {}).some(([id, entry]) => entry.credential_ref === reference && loginUsers(raw, id).length > 0);
|
|
2410
|
+
}
|
|
2411
|
+
async function releaseLogin(profile, transport, detach = () => {
|
|
2412
|
+
}) {
|
|
2413
|
+
return new OAuthSessionStore(profile.credentialsPath).retire(profile.reference, oauthOwner(profile), transport, () => updateAuthConfig(profile.configPath, (raw) => {
|
|
2414
|
+
detach(raw);
|
|
2415
|
+
if (referenced(raw, profile.reference)) return false;
|
|
2416
|
+
for (const [id, entry] of Object.entries(raw.logins ?? {})) {
|
|
2417
|
+
if (entry.credential_ref === profile.reference) delete raw.logins[id];
|
|
2418
|
+
}
|
|
2419
|
+
return true;
|
|
2420
|
+
}));
|
|
2421
|
+
}
|
|
2422
|
+
async function collectUnusedLogins(path, transport, previous) {
|
|
2423
|
+
const raw = readAuthConfig(path), candidates = /* @__PURE__ */ new Map();
|
|
2424
|
+
if (previous) candidates.set(previous.reference, previous);
|
|
2425
|
+
for (const [id, entry] of Object.entries(raw.logins ?? {})) {
|
|
2426
|
+
if (loginUsers(raw, id).length) continue;
|
|
2427
|
+
const profile = { ...oauthProfile(entry.credential_ref, path, id, "https://api.cn-hangzhou.agentcontext.aliyuncs.com"), loginId: id };
|
|
2428
|
+
if (profile.reference) candidates.set(profile.reference, profile);
|
|
2429
|
+
}
|
|
2430
|
+
let removed = 0, unconfirmed = 0;
|
|
2431
|
+
for (const profile of candidates.values()) {
|
|
2432
|
+
try {
|
|
2433
|
+
const result = await releaseLogin(profile, transport);
|
|
2434
|
+
if (result.removed) removed++;
|
|
2435
|
+
if (result.remoteUnconfirmed) unconfirmed++;
|
|
2436
|
+
} catch {
|
|
2437
|
+
unconfirmed++;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
return { removed, unconfirmed };
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// src/authorization/authorization-service.ts
|
|
2444
|
+
var AuthorizationError = class extends Error {
|
|
2445
|
+
code;
|
|
2446
|
+
constructor(message, code) {
|
|
2447
|
+
super(message);
|
|
2448
|
+
this.code = code;
|
|
2449
|
+
this.name = "AuthorizationError";
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
var AuthorizationDeclinedError = class extends Error {
|
|
2453
|
+
constructor(message = "authorization declined") {
|
|
2454
|
+
super(message);
|
|
2455
|
+
this.name = "AuthorizationDeclinedError";
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
var AuthorizationService = class {
|
|
2459
|
+
flows = /* @__PURE__ */ new Map();
|
|
2460
|
+
running = /* @__PURE__ */ new Map();
|
|
2461
|
+
credentials;
|
|
2462
|
+
constructor(credentials) {
|
|
2463
|
+
this.credentials = credentials;
|
|
2464
|
+
}
|
|
2465
|
+
registerFlow(flow) {
|
|
2466
|
+
if (this.flows.has(flow.key)) throw new AuthorizationError("authorization flow already registered", "DUPLICATE_FLOW");
|
|
2467
|
+
this.flows.set(flow.key, flow);
|
|
2468
|
+
return () => {
|
|
2469
|
+
this.flows.delete(flow.key);
|
|
2470
|
+
this.running.get(flow.key)?.abort();
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
list() {
|
|
2474
|
+
return [...this.flows.values()].map((flow) => ({ key: flow.key, label: flow.label, methods: flow.methods, inFlight: this.running.has(flow.key) }));
|
|
2475
|
+
}
|
|
2476
|
+
cancel(key) {
|
|
2477
|
+
this.running.get(key)?.abort();
|
|
2478
|
+
}
|
|
2479
|
+
async begin(request) {
|
|
2480
|
+
const flow = this.flows.get(request.key);
|
|
2481
|
+
if (!flow) throw new AuthorizationError("no authorization flow", "NO_FLOW");
|
|
2482
|
+
const method = request.method ?? flow.methods[0].id;
|
|
2483
|
+
if (!flow.methods.some((m) => m.id === method)) throw new AuthorizationError("authorization method is not supported", "UNKNOWN_METHOD");
|
|
2484
|
+
if (this.running.has(request.key)) throw new AuthorizationError("authorization already running", "ALREADY_IN_FLIGHT");
|
|
2485
|
+
if (request.signal?.aborted) return { status: "cancelled" };
|
|
2486
|
+
const provider = this.credentials, key = request.credentialKey ?? request.key;
|
|
2487
|
+
const before = !request.target && provider ? provider.snapshot(key) : void 0;
|
|
2488
|
+
const controller = new AbortController(), withdraw = () => controller.abort(request.signal?.reason);
|
|
2489
|
+
request.signal?.addEventListener("abort", withdraw, { once: true });
|
|
2490
|
+
this.running.set(request.key, controller);
|
|
2491
|
+
const warn = (message) => {
|
|
2492
|
+
try {
|
|
2493
|
+
request.warn?.(message);
|
|
2494
|
+
} catch {
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
const target = request.target ?? {
|
|
2498
|
+
commit: (candidate2) => {
|
|
2499
|
+
if (!provider || !before) throw new AuthorizationError("authorization target required", "NO_TARGET");
|
|
2500
|
+
return provider.commit(key, candidate2, before);
|
|
2501
|
+
},
|
|
2502
|
+
rollback: (receipt2) => provider.rollback(receipt2),
|
|
2503
|
+
retirePrevious: async () => {
|
|
2504
|
+
if (before?.record) await flow.revoke(before.record);
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
let candidate, receipt, committed = false;
|
|
2508
|
+
try {
|
|
2509
|
+
candidate = await flow.run({
|
|
2510
|
+
method,
|
|
2511
|
+
signal: controller.signal,
|
|
2512
|
+
notify: (notice) => {
|
|
2513
|
+
try {
|
|
2514
|
+
request.interaction.notify(notice);
|
|
2515
|
+
} catch {
|
|
2516
|
+
}
|
|
2517
|
+
},
|
|
2518
|
+
prompt: (prompt) => request.interaction.prompt(prompt)
|
|
2519
|
+
});
|
|
2520
|
+
controller.signal.throwIfAborted();
|
|
2521
|
+
if (!candidate) throw new AuthorizationError("authorization completed without a candidate credential", "NOT_COMMITTED");
|
|
2522
|
+
await flow.validate?.(candidate, controller.signal);
|
|
2523
|
+
controller.signal.throwIfAborted();
|
|
2524
|
+
receipt = target.commit(candidate);
|
|
2525
|
+
committed = true;
|
|
2526
|
+
try {
|
|
2527
|
+
if (request.finalize) {
|
|
2528
|
+
controller.signal.throwIfAborted();
|
|
2529
|
+
await request.finalize();
|
|
2530
|
+
controller.signal.throwIfAborted();
|
|
2531
|
+
}
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
try {
|
|
2534
|
+
committed = !target.rollback(receipt);
|
|
2535
|
+
} catch {
|
|
2536
|
+
warn("The previous local credential could not be restored; inspect auth status before retrying.");
|
|
2537
|
+
}
|
|
2538
|
+
throw error;
|
|
2539
|
+
}
|
|
2540
|
+
try {
|
|
2541
|
+
await target.retirePrevious?.();
|
|
2542
|
+
} catch {
|
|
2543
|
+
warn(request.previousCleanupWarning ?? "New login saved; previous credential cleanup could not be confirmed.");
|
|
2544
|
+
}
|
|
2545
|
+
return { status: "authorized" };
|
|
2546
|
+
} catch (error) {
|
|
2547
|
+
if (candidate && !committed) {
|
|
2548
|
+
try {
|
|
2549
|
+
target.discard?.();
|
|
2550
|
+
} catch {
|
|
2551
|
+
warn("New login was not saved; local credential cleanup needs attention.");
|
|
2552
|
+
}
|
|
2553
|
+
try {
|
|
2554
|
+
await flow.revoke(candidate);
|
|
2555
|
+
} catch {
|
|
2556
|
+
warn("New login was not saved; session cleanup could not be confirmed.");
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (controller.signal.aborted || error instanceof AuthorizationDeclinedError) return { status: "cancelled" };
|
|
2560
|
+
throw error;
|
|
2561
|
+
} finally {
|
|
2562
|
+
request.signal?.removeEventListener("abort", withdraw);
|
|
2563
|
+
this.running.delete(request.key);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
|
|
2568
|
+
// src/authorization/public-target.ts
|
|
2569
|
+
import { dirname as dirname7, join as join7 } from "path";
|
|
2570
|
+
function publicAuthorizationTarget(pending, revoke) {
|
|
2571
|
+
const path = join7(dirname7(pending.path), "credentials.json"), store = new OAuthSessionStore(path), provider = new LocalCredentialProvider(path);
|
|
2572
|
+
let reference, published = false;
|
|
2573
|
+
return {
|
|
2574
|
+
commit(candidate) {
|
|
2575
|
+
const session = oauthSessionFromCredential(candidate);
|
|
2576
|
+
reference = store.saveLogin(pending.agent, session, pending.loginId);
|
|
2577
|
+
commitLogin(pending, reference, session.baseUrl);
|
|
2578
|
+
published = true;
|
|
2579
|
+
return reference;
|
|
2580
|
+
},
|
|
2581
|
+
// Public login has no integration finalize step after publishing. A successful commit is final.
|
|
2582
|
+
rollback() {
|
|
2583
|
+
return false;
|
|
2584
|
+
},
|
|
2585
|
+
discard() {
|
|
2586
|
+
if (!reference || published) return;
|
|
2587
|
+
const snapshot2 = provider.snapshot(reference);
|
|
2588
|
+
if (snapshot2.record) {
|
|
2589
|
+
provider.transaction((tx) => tx.remove(snapshot2));
|
|
2590
|
+
}
|
|
2591
|
+
},
|
|
2592
|
+
async retirePrevious() {
|
|
2593
|
+
const result = await collectUnusedLogins(pending.path, revoke, pending.previous);
|
|
2594
|
+
if (result.unconfirmed) throw new Error("Previous session cleanup unconfirmed");
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
996
2598
|
export {
|
|
2599
|
+
ACTIVE_CONTEXTDB_CREDENTIAL,
|
|
2600
|
+
AuthConfigError,
|
|
2601
|
+
AuthorizationDeclinedError,
|
|
2602
|
+
AuthorizationError,
|
|
2603
|
+
AuthorizationService,
|
|
2604
|
+
CoreDeviceClient,
|
|
2605
|
+
DEFAULT_DATA_BASE_URL,
|
|
997
2606
|
DOCUMENT_EVIDENCE_SECTION_TITLE,
|
|
2607
|
+
DataApiAuthorizationProvider,
|
|
2608
|
+
DeviceFlowError,
|
|
998
2609
|
EXTERNAL_KNOWLEDGE_PREAMBLE_HEAD,
|
|
999
2610
|
GRAPH_CONTEXT_MAX_CHARS,
|
|
1000
2611
|
GRAPH_CONTEXT_SECTION_TITLE,
|
|
@@ -1002,6 +2613,10 @@ export {
|
|
|
1002
2613
|
GRAPH_CONTEXT_TAG,
|
|
1003
2614
|
GRAPH_CONTEXT_TRUNCATION_MARKER,
|
|
1004
2615
|
GRAPH_CONTEXT_USAGE_NOTICE,
|
|
2616
|
+
LocalCredentialProvider,
|
|
2617
|
+
OAuthCredentialError,
|
|
2618
|
+
OAuthProfileProvider,
|
|
2619
|
+
OAuthSessionStore,
|
|
1005
2620
|
OFFICIAL_PRODUCTION_BASE_URLS,
|
|
1006
2621
|
RECALLED_MEMORIES_SAFETY_NOTICE,
|
|
1007
2622
|
RECALL_TRACE_AGENTS,
|
|
@@ -1010,11 +2625,19 @@ export {
|
|
|
1010
2625
|
RECALL_TRACE_OUTCOMES,
|
|
1011
2626
|
RECALL_TRACE_ROTATED_GENERATIONS,
|
|
1012
2627
|
RECALL_TRACE_SCHEMA_VERSION,
|
|
2628
|
+
assertAuthSnapshot,
|
|
2629
|
+
authSnapshot,
|
|
2630
|
+
beginLogin,
|
|
2631
|
+
bindLogin,
|
|
1013
2632
|
buildExternalKnowledgeBlock,
|
|
1014
2633
|
buildExternalKnowledgePreamble,
|
|
1015
2634
|
buildNormalizedGraphContext,
|
|
2635
|
+
collectUnusedLogins,
|
|
2636
|
+
commitLogin,
|
|
1016
2637
|
createRecallFinishRecord,
|
|
1017
2638
|
createRecallStartRecord,
|
|
2639
|
+
createRequestAuthorizationProvider,
|
|
2640
|
+
defaultCredentialsPath,
|
|
1018
2641
|
extractKnowledgeBaseIds,
|
|
1019
2642
|
extractKnowledgeChunkContent,
|
|
1020
2643
|
extractKnowledgeChunkSourceLabel,
|
|
@@ -1022,24 +2645,53 @@ export {
|
|
|
1022
2645
|
filterMessagesForExtraction,
|
|
1023
2646
|
formatExternalKnowledgeChunks,
|
|
1024
2647
|
formatRecalledMemoriesBlock,
|
|
2648
|
+
internalCandidate,
|
|
1025
2649
|
isGenericAssistantMessage,
|
|
1026
2650
|
isGraphContextChunk,
|
|
1027
2651
|
isKnowledgeBaseUploadTurn,
|
|
1028
2652
|
isNoiseMessage,
|
|
1029
2653
|
isOfficialProductionUrl,
|
|
1030
2654
|
isSessionSpecificContent,
|
|
2655
|
+
loginUsers,
|
|
2656
|
+
namedLogin,
|
|
1031
2657
|
normalizeBaseUrl,
|
|
2658
|
+
normalizeCoreOrigin,
|
|
2659
|
+
oauthOwner,
|
|
2660
|
+
oauthProfile,
|
|
2661
|
+
oauthProfileFromConfig,
|
|
2662
|
+
oauthProfileStatus,
|
|
2663
|
+
oauthSessionCandidate,
|
|
2664
|
+
oauthSessionFromCredential,
|
|
2665
|
+
ownAuthProfile,
|
|
1032
2666
|
partitionKnowledgeChunks,
|
|
2667
|
+
preserveDefaultApiKey,
|
|
2668
|
+
publicApiKeyFromConfig,
|
|
2669
|
+
publicAuthenticationFromConfig,
|
|
2670
|
+
publicAuthorizationTarget,
|
|
2671
|
+
readActiveCredentialSync,
|
|
2672
|
+
readAuthConfig,
|
|
1033
2673
|
readRecallTraces,
|
|
1034
2674
|
recall,
|
|
1035
2675
|
recallTracePath,
|
|
2676
|
+
rejectAuthEnvironment,
|
|
2677
|
+
releaseLogin,
|
|
2678
|
+
requireLoginId,
|
|
1036
2679
|
resolveDebugPolicy,
|
|
1037
2680
|
sanitizeChunkContent,
|
|
1038
2681
|
sanitizeQuery,
|
|
1039
2682
|
sanitizeRecallFailureReason,
|
|
1040
2683
|
selectAndFormatRecalledMemories,
|
|
2684
|
+
selectDataApiCredential,
|
|
1041
2685
|
selectTurnMessages,
|
|
2686
|
+
selectedLogin,
|
|
2687
|
+
setupAuthentication,
|
|
1042
2688
|
stripNoiseFromContent,
|
|
1043
2689
|
truncateGraphContext,
|
|
2690
|
+
unbindLogin,
|
|
2691
|
+
updateAuthConfig,
|
|
2692
|
+
validateDataBaseUrl,
|
|
2693
|
+
validateLoginChoice,
|
|
2694
|
+
withAuthConfigLock,
|
|
2695
|
+
withAuthorizationLock,
|
|
1044
2696
|
writeRecallTrace
|
|
1045
2697
|
};
|