@aliyunrds/ctxdb 1.0.8-beta.4 → 1.0.9-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -38
- package/dist/{chunk-SVO6H62S.js → chunk-2U7G6UXE.js} +287 -217
- package/dist/{chunk-OEZM2ZYA.js → chunk-3MEBQDGU.js} +5 -0
- package/dist/{chunk-5DGRNP55.js → chunk-7TPYIT63.js} +1 -1
- package/dist/{chunk-6MEWI73Y.js → chunk-FD3IPF6R.js} +2 -2
- package/dist/{chunk-76AXXLVE.js → chunk-N5JGS5EC.js} +2 -2
- package/dist/{chunk-ZW7YXNJ6.js → chunk-T5ZJEPR4.js} +286 -11
- package/dist/{chunk-O6MKQ2I3.js → chunk-U4DRH4NZ.js} +1 -1
- package/dist/{chunk-WJTV7JNR.js → chunk-V7IZ2UYJ.js} +3 -3
- package/dist/cli/main.js +4161 -1925
- package/dist/hooks/hermes-post-llm-call.js +3 -3
- package/dist/hooks/hermes-pre-llm-call.js +6 -6
- package/dist/hooks/session-start.js +5 -5
- package/dist/hooks/stop.js +3 -3
- package/dist/hooks/user-prompt-submit.js +5 -5
- package/dist/opencode/index.js +713 -293
- package/dist/setup/skills/contextdb-knowledge/SKILL.md +14 -3
- package/dist/setup/skills/contextdb-memory/SKILL.md +10 -3
- package/dist/workers/version-check.js +1 -1
- package/package.json +2 -2
package/dist/opencode/index.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
// <define:__CTXDB_DISTRIBUTION_MANIFEST__>
|
|
2
2
|
var define_CTXDB_DISTRIBUTION_MANIFEST_default = { id: "public", packageName: "@aliyunrds/ctxdb", packageRegistry: null, capabilities: { interactiveLogin: false, managedCredentials: false, deviceFlow: true } };
|
|
3
3
|
|
|
4
|
-
// src/config.ts
|
|
5
|
-
import { readFileSync as readFileSync4, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
6
|
-
import { homedir as homedir2 } from "os";
|
|
7
|
-
import { dirname as dirname3, join as join5 } from "path";
|
|
8
|
-
|
|
9
4
|
// ../shared/src/graph-context.ts
|
|
10
5
|
var GRAPH_CONTEXT_TAG = "graphrag";
|
|
11
6
|
var GRAPH_CONTEXT_SOURCE_LABEL = "Related content in Knowledge Graph";
|
|
@@ -589,6 +584,7 @@ function selectAndFormatRecalledMemories(memories, userId, config = {}) {
|
|
|
589
584
|
|
|
590
585
|
// ../shared/src/debug-policy.ts
|
|
591
586
|
var OFFICIAL_PRODUCTION_BASE_URLS = [
|
|
587
|
+
"https://api.cn-hangzhou.agentcontext.aliyuncs.com",
|
|
592
588
|
"https://context-database.aliyuncs.com"
|
|
593
589
|
];
|
|
594
590
|
function normalizeBaseUrl(value) {
|
|
@@ -861,16 +857,21 @@ var CoreDeviceClient = class {
|
|
|
861
857
|
baseUrl;
|
|
862
858
|
fetchImpl;
|
|
863
859
|
now;
|
|
860
|
+
requestTimeoutMs;
|
|
864
861
|
sleep;
|
|
865
862
|
constructor(baseUrl, options = {}) {
|
|
866
863
|
this.baseUrl = normalizeCoreOrigin(baseUrl);
|
|
867
864
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
868
865
|
this.now = options.now ?? Date.now;
|
|
866
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
869
867
|
this.sleep = options.sleep ?? ((ms, signal) => delay(ms, void 0, { signal }));
|
|
870
868
|
}
|
|
871
869
|
async login(options) {
|
|
872
870
|
const started = this.now();
|
|
873
|
-
const initial = await this.post("/v1/auth/device/authorize", {
|
|
871
|
+
const initial = await this.post("/v1/auth/device/authorize", {
|
|
872
|
+
client_id: clientId,
|
|
873
|
+
...options.initialAgent === void 0 ? {} : { initial_agent: options.initialAgent }
|
|
874
|
+
}, options.signal);
|
|
874
875
|
if (!initial.ok) throw new DeviceFlowError("request_rejected");
|
|
875
876
|
const data = initial.data;
|
|
876
877
|
const requestId = text(data.request_id, 32), userCode = text(data.user_code, 9), code = text(data.device_code, 43);
|
|
@@ -920,7 +921,7 @@ var CoreDeviceClient = class {
|
|
|
920
921
|
const controller = new AbortController();
|
|
921
922
|
const abort = () => controller.abort();
|
|
922
923
|
signal?.addEventListener("abort", abort, { once: true });
|
|
923
|
-
const timer = setTimeout(abort,
|
|
924
|
+
const timer = setTimeout(abort, this.requestTimeoutMs);
|
|
924
925
|
timer.unref?.();
|
|
925
926
|
try {
|
|
926
927
|
const response = await this.fetchImpl(this.baseUrl + path, {
|
|
@@ -1022,74 +1023,334 @@ async function boundedJson(response) {
|
|
|
1022
1023
|
}
|
|
1023
1024
|
|
|
1024
1025
|
// ../shared/src/oauth-credentials.ts
|
|
1025
|
-
import {
|
|
1026
|
-
constants,
|
|
1027
|
-
closeSync as closeSync2,
|
|
1028
|
-
existsSync as existsSync2,
|
|
1029
|
-
fstatSync,
|
|
1030
|
-
fsyncSync,
|
|
1031
|
-
lstatSync,
|
|
1032
|
-
mkdirSync as mkdirSync2,
|
|
1033
|
-
openSync as openSync2,
|
|
1034
|
-
readFileSync as readFileSync2,
|
|
1035
|
-
renameSync as renameSync2,
|
|
1036
|
-
unlinkSync,
|
|
1037
|
-
writeFileSync
|
|
1038
|
-
} from "fs";
|
|
1039
|
-
import { randomBytes } from "crypto";
|
|
1040
|
-
import { join as join3 } from "path";
|
|
1041
|
-
import { setTimeout as delay2 } from "timers/promises";
|
|
1026
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1042
1027
|
|
|
1043
|
-
// ../shared/src/
|
|
1044
|
-
import {
|
|
1028
|
+
// ../shared/src/credentials/local-credential-provider.ts
|
|
1029
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
1030
|
+
import { homedir as homedir2 } from "os";
|
|
1045
1031
|
import { join as join2 } from "path";
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1032
|
+
|
|
1033
|
+
// ../shared/src/credentials/storage.ts
|
|
1034
|
+
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";
|
|
1035
|
+
import { randomBytes, createHash } from "crypto";
|
|
1036
|
+
import { dirname as dirname2 } from "path";
|
|
1037
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
1038
|
+
function assertStoragePath(path, directory = false) {
|
|
1039
|
+
const s = lstatSync(path);
|
|
1040
|
+
if (s.isSymbolicLink() || (directory ? !s.isDirectory() : !s.isFile())) throw new Error("credentials: invalid storage path");
|
|
1041
|
+
}
|
|
1042
|
+
function prepareStorage(path) {
|
|
1043
|
+
const dir = dirname2(path);
|
|
1044
|
+
if (!existsSync2(dir)) {
|
|
1045
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
1046
|
+
}
|
|
1047
|
+
assertStoragePath(dir, true);
|
|
1048
|
+
}
|
|
1049
|
+
function readCredentialFile(path) {
|
|
1050
|
+
if (!existsSync2(path)) return void 0;
|
|
1051
|
+
assertStoragePath(dirname2(path), true);
|
|
1052
|
+
assertStoragePath(path);
|
|
1053
|
+
const fd = openSync2(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1054
|
+
try {
|
|
1055
|
+
const stat = fstatSync(fd);
|
|
1056
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new Error("credentials: invalid storage");
|
|
1057
|
+
return readFileSync2(fd, "utf8");
|
|
1058
|
+
} finally {
|
|
1059
|
+
closeSync2(fd);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function atomicCredentialWrite(path, value) {
|
|
1063
|
+
prepareStorage(path);
|
|
1064
|
+
if (existsSync2(path)) assertStoragePath(path);
|
|
1065
|
+
const temp = path + "." + randomBytes(12).toString("hex") + ".tmp";
|
|
1066
|
+
let fd;
|
|
1076
1067
|
try {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1068
|
+
fd = openSync2(temp, "wx", 384);
|
|
1069
|
+
writeFileSync(fd, JSON.stringify(value, null, 2) + "\n");
|
|
1070
|
+
fsyncSync(fd);
|
|
1071
|
+
closeSync2(fd);
|
|
1072
|
+
fd = void 0;
|
|
1073
|
+
renameSync2(temp, path);
|
|
1074
|
+
if (process.platform !== "win32") {
|
|
1075
|
+
try {
|
|
1076
|
+
const dir = openSync2(dirname2(path), "r");
|
|
1077
|
+
try {
|
|
1078
|
+
fsyncSync(dir);
|
|
1079
|
+
} finally {
|
|
1080
|
+
closeSync2(dir);
|
|
1081
|
+
}
|
|
1082
|
+
} catch {
|
|
1087
1083
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1084
|
+
}
|
|
1085
|
+
} finally {
|
|
1086
|
+
if (fd !== void 0) closeSync2(fd);
|
|
1087
|
+
if (existsSync2(temp)) unlinkSync(temp);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function withCredentialWriteLock(path, operation) {
|
|
1091
|
+
prepareStorage(path);
|
|
1092
|
+
const lock = path + ".lock", deadline = Date.now() + 1e4;
|
|
1093
|
+
let fd;
|
|
1094
|
+
while (true) {
|
|
1095
|
+
try {
|
|
1096
|
+
fd = openSync2(lock, "wx", 384);
|
|
1097
|
+
break;
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
if (error.code !== "EEXIST") throw error;
|
|
1100
|
+
if (Date.now() >= deadline) throw new Error("credentials: writer lock is busy; check the previous CLI process");
|
|
1101
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
try {
|
|
1105
|
+
return operation();
|
|
1106
|
+
} finally {
|
|
1107
|
+
closeSync2(fd);
|
|
1108
|
+
unlinkSync(lock);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function operationLockPath(path, key) {
|
|
1112
|
+
return path + "." + createHash("sha256").update(key).digest("hex") + ".lock";
|
|
1113
|
+
}
|
|
1114
|
+
async function withCredentialOperation(path, key, operation, options = {}) {
|
|
1115
|
+
prepareStorage(path);
|
|
1116
|
+
const lock = operationLockPath(path, key), deadline = Date.now() + (options.lockWaitMs ?? 3e4);
|
|
1117
|
+
while (true) {
|
|
1118
|
+
options.signal?.throwIfAborted();
|
|
1119
|
+
let fd;
|
|
1120
|
+
try {
|
|
1121
|
+
fd = openSync2(lock, "wx", 384);
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
if (error.code !== "EEXIST") throw error;
|
|
1124
|
+
if (Date.now() >= deadline) throw new Error("credentials: credential_busy");
|
|
1125
|
+
await delay2(25, void 0, { signal: options.signal });
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
writeFileSync(fd, String(process.pid));
|
|
1130
|
+
} finally {
|
|
1131
|
+
closeSync2(fd);
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
return await operation();
|
|
1135
|
+
} finally {
|
|
1136
|
+
unlinkSync(lock);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// ../shared/src/credentials/types.ts
|
|
1142
|
+
var ACTIVE_CONTEXTDB_CREDENTIAL = "contextdb/active";
|
|
1143
|
+
|
|
1144
|
+
// ../shared/src/credentials/local-credential-provider.ts
|
|
1145
|
+
function defaultCredentialsPath() {
|
|
1146
|
+
return join2(homedir2(), ".ctxdb", "credentials.json");
|
|
1147
|
+
}
|
|
1148
|
+
var object2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1149
|
+
var fail = (message) => {
|
|
1150
|
+
throw new Error("credentials: " + message);
|
|
1151
|
+
};
|
|
1152
|
+
function origin(value) {
|
|
1153
|
+
if (typeof value !== "string" || !value) return fail("invalid service URL");
|
|
1154
|
+
let u;
|
|
1155
|
+
try {
|
|
1156
|
+
u = new URL(value);
|
|
1090
1157
|
} catch {
|
|
1091
|
-
|
|
1158
|
+
return fail("invalid service URL");
|
|
1159
|
+
}
|
|
1160
|
+
if (!["http:", "https:"].includes(u.protocol) || u.username || u.password || u.search || u.hash) return fail("invalid service URL");
|
|
1161
|
+
return u.toString().replace(/\/$/, "");
|
|
1162
|
+
}
|
|
1163
|
+
function validateApi(raw, internal, v1 = false) {
|
|
1164
|
+
if (!object2(raw.payload) || typeof raw.payload.api_key !== "string" || !raw.payload.api_key) fail("invalid api-key payload");
|
|
1165
|
+
const metadata = v1 ? raw.payload : raw.metadata;
|
|
1166
|
+
origin(metadata.base_url);
|
|
1167
|
+
if (internal) {
|
|
1168
|
+
origin(metadata.login_server);
|
|
1169
|
+
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");
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function validate(key, raw) {
|
|
1173
|
+
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");
|
|
1174
|
+
if (key === ACTIVE_CONTEXTDB_CREDENTIAL) {
|
|
1175
|
+
if (raw.kind !== "api-key" || raw.metadata.owner.distribution !== "internal" || raw.metadata.state !== "ready") fail("invalid contextdb/active record");
|
|
1176
|
+
} 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");
|
|
1177
|
+
const fields = raw.kind === "api-key" ? ["api_key"] : ["ready", "refreshing"].includes(raw.metadata.state) ? ["accessToken", "refreshToken"] : [];
|
|
1178
|
+
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");
|
|
1179
|
+
if (raw.kind === "api-key") validateApi(raw, key === ACTIVE_CONTEXTDB_CREDENTIAL);
|
|
1180
|
+
}
|
|
1181
|
+
function readDocument(path) {
|
|
1182
|
+
const bytes = readCredentialFile(path);
|
|
1183
|
+
if (bytes === void 0) return { version: 2, records: {} };
|
|
1184
|
+
let doc;
|
|
1185
|
+
try {
|
|
1186
|
+
doc = JSON.parse(bytes);
|
|
1187
|
+
} catch {
|
|
1188
|
+
return fail("credentials.json is not valid JSON");
|
|
1189
|
+
}
|
|
1190
|
+
if (!object2(doc) || ![1, 2].includes(doc.version) || !object2(doc.records)) return fail("unsupported credentials.json schema/version");
|
|
1191
|
+
if (doc.version === 1) {
|
|
1192
|
+
if (Object.keys(doc.records).some((k) => k !== ACTIVE_CONTEXTDB_CREDENTIAL)) fail("unsupported record key in v1 credentials");
|
|
1193
|
+
const active = doc.records[ACTIVE_CONTEXTDB_CREDENTIAL];
|
|
1194
|
+
if (active !== void 0) {
|
|
1195
|
+
if (active.kind !== "api-key") fail("unsupported record kind");
|
|
1196
|
+
validateApi(active, true, true);
|
|
1197
|
+
}
|
|
1198
|
+
} else for (const [key, record] of Object.entries(doc.records)) validate(key, record);
|
|
1199
|
+
return doc;
|
|
1200
|
+
}
|
|
1201
|
+
function upgrade(doc) {
|
|
1202
|
+
if (doc.version === 2) return doc;
|
|
1203
|
+
const record = doc.records[ACTIVE_CONTEXTDB_CREDENTIAL];
|
|
1204
|
+
return { ...doc, version: 2, records: record ? { [ACTIVE_CONTEXTDB_CREDENTIAL]: legacy(record) } : {} };
|
|
1205
|
+
}
|
|
1206
|
+
function legacy(record) {
|
|
1207
|
+
const { api_key, ...details } = record.payload;
|
|
1208
|
+
return { kind: record.kind, payload: { api_key }, metadata: {
|
|
1209
|
+
...details,
|
|
1210
|
+
...record.metadata,
|
|
1211
|
+
base_url: record.payload.base_url,
|
|
1212
|
+
login_server: record.payload.login_server,
|
|
1213
|
+
owner: { distribution: "internal" },
|
|
1214
|
+
state: "ready",
|
|
1215
|
+
revision: 1,
|
|
1216
|
+
generation: createHash2("sha256").update(JSON.stringify(record)).digest("hex").slice(0, 32)
|
|
1217
|
+
} };
|
|
1218
|
+
}
|
|
1219
|
+
function snapshot(doc, key) {
|
|
1220
|
+
const raw = Object.hasOwn(doc.records, key) ? doc.records[key] : void 0;
|
|
1221
|
+
const record = raw && (doc.version === 1 ? legacy(raw) : raw);
|
|
1222
|
+
return { key, record, fingerprint: createHash2("sha256").update(JSON.stringify(record) ?? "missing").digest("hex") };
|
|
1223
|
+
}
|
|
1224
|
+
function apiRecord(record) {
|
|
1225
|
+
if (!record) return void 0;
|
|
1226
|
+
if (record.kind !== "api-key" || record.metadata.owner.distribution !== "internal") return fail("invalid contextdb/active record");
|
|
1227
|
+
return {
|
|
1228
|
+
kind: "api-key",
|
|
1229
|
+
payload: { apiKey: record.payload.api_key },
|
|
1230
|
+
metadata: {
|
|
1231
|
+
baseUrl: origin(record.metadata.base_url),
|
|
1232
|
+
loginServer: origin(record.metadata.login_server),
|
|
1233
|
+
authorizationMethod: record.metadata.authorization_method,
|
|
1234
|
+
issuedAt: record.metadata.issued_at
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function internalCandidate(record) {
|
|
1239
|
+
return {
|
|
1240
|
+
kind: "api-key",
|
|
1241
|
+
payload: { api_key: record.payload.apiKey },
|
|
1242
|
+
metadata: {
|
|
1243
|
+
base_url: record.metadata.baseUrl,
|
|
1244
|
+
login_server: record.metadata.loginServer,
|
|
1245
|
+
authorization_method: record.metadata.authorizationMethod,
|
|
1246
|
+
issued_at: record.metadata.issuedAt,
|
|
1247
|
+
owner: { distribution: "internal" },
|
|
1248
|
+
state: "ready"
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
var LocalCredentialProvider = class {
|
|
1253
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1254
|
+
path;
|
|
1255
|
+
constructor(path = defaultCredentialsPath()) {
|
|
1256
|
+
this.path = path;
|
|
1257
|
+
}
|
|
1258
|
+
snapshot(key) {
|
|
1259
|
+
return snapshot(readDocument(this.path), key);
|
|
1260
|
+
}
|
|
1261
|
+
readStoredRecord(key) {
|
|
1262
|
+
return this.snapshot(key).record;
|
|
1263
|
+
}
|
|
1264
|
+
async readRecord(key) {
|
|
1265
|
+
return apiRecord(this.readStoredRecord(key));
|
|
1266
|
+
}
|
|
1267
|
+
async describeRecord(key) {
|
|
1268
|
+
const r = this.readStoredRecord(key);
|
|
1269
|
+
return { configured: !!r && r.metadata.state === "ready", kind: r?.kind, writable: true };
|
|
1270
|
+
}
|
|
1271
|
+
onRecordUpdated(listener) {
|
|
1272
|
+
this.listeners.add(listener);
|
|
1273
|
+
return () => this.listeners.delete(listener);
|
|
1274
|
+
}
|
|
1275
|
+
/** Synchronous short commit; callbacks may acquire a config lock, never the reverse. */
|
|
1276
|
+
transaction(operation) {
|
|
1277
|
+
return withCredentialWriteLock(this.path, () => {
|
|
1278
|
+
let doc = readDocument(this.path);
|
|
1279
|
+
const read = (key) => snapshot(doc, key);
|
|
1280
|
+
const check = (expected) => {
|
|
1281
|
+
if (read(expected.key).fingerprint !== expected.fingerprint) fail("authentication changed; the newer credential was preserved, retry the command");
|
|
1282
|
+
};
|
|
1283
|
+
const save = () => {
|
|
1284
|
+
doc = upgrade(doc);
|
|
1285
|
+
atomicCredentialWrite(this.path, doc);
|
|
1286
|
+
};
|
|
1287
|
+
return operation({
|
|
1288
|
+
read,
|
|
1289
|
+
put: (key, candidate, expected) => {
|
|
1290
|
+
if (expected) {
|
|
1291
|
+
if (expected.key !== key) fail("invalid commit target");
|
|
1292
|
+
check(expected);
|
|
1293
|
+
}
|
|
1294
|
+
const before = read(key), previous = before.record;
|
|
1295
|
+
const next = { kind: candidate.kind, payload: candidate.payload, metadata: {
|
|
1296
|
+
...candidate.metadata,
|
|
1297
|
+
generation: previous?.metadata.generation ?? randomBytes2(16).toString("hex"),
|
|
1298
|
+
revision: (previous?.metadata.revision ?? 0) + 1
|
|
1299
|
+
} };
|
|
1300
|
+
validate(key, next);
|
|
1301
|
+
doc = upgrade(doc);
|
|
1302
|
+
doc.records[key] = next;
|
|
1303
|
+
save();
|
|
1304
|
+
return { before, after: read(key) };
|
|
1305
|
+
},
|
|
1306
|
+
remove: (expected) => {
|
|
1307
|
+
check(expected);
|
|
1308
|
+
if (!read(expected.key).record) return;
|
|
1309
|
+
doc = upgrade(doc);
|
|
1310
|
+
delete doc.records[expected.key];
|
|
1311
|
+
save();
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
commit(key, record, expected = this.snapshot(key)) {
|
|
1317
|
+
return this.transaction((tx) => tx.put(key, record, expected));
|
|
1318
|
+
}
|
|
1319
|
+
rollback(receipt) {
|
|
1320
|
+
return this.transaction((tx) => {
|
|
1321
|
+
if (tx.read(receipt.after.key).fingerprint !== receipt.after.fingerprint) return false;
|
|
1322
|
+
if (receipt.before.record) tx.put(receipt.after.key, receipt.before.record, receipt.after);
|
|
1323
|
+
else tx.remove(receipt.after);
|
|
1324
|
+
return true;
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
async modifyRecord(key, mutate, signal) {
|
|
1328
|
+
const before = this.snapshot(key), next = await mutate(apiRecord(before.record));
|
|
1329
|
+
signal?.throwIfAborted();
|
|
1330
|
+
if (next === void 0) return apiRecord(before.record);
|
|
1331
|
+
const receipt = this.commit(key, internalCandidate(next), before);
|
|
1332
|
+
for (const listener of this.listeners) {
|
|
1333
|
+
try {
|
|
1334
|
+
listener(key);
|
|
1335
|
+
} catch {
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return apiRecord(receipt.after.record);
|
|
1339
|
+
}
|
|
1340
|
+
async deleteRecord(key, signal) {
|
|
1341
|
+
signal?.throwIfAborted();
|
|
1342
|
+
const before = this.snapshot(key);
|
|
1343
|
+
this.transaction((tx) => tx.remove(before));
|
|
1344
|
+
for (const listener of this.listeners) {
|
|
1345
|
+
try {
|
|
1346
|
+
listener(key);
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1092
1350
|
}
|
|
1351
|
+
};
|
|
1352
|
+
function readActiveCredentialSync(path = defaultCredentialsPath()) {
|
|
1353
|
+
return apiRecord(new LocalCredentialProvider(path).readStoredRecord(ACTIVE_CONTEXTDB_CREDENTIAL));
|
|
1093
1354
|
}
|
|
1094
1355
|
|
|
1095
1356
|
// ../shared/src/oauth-credentials.ts
|
|
@@ -1100,7 +1361,7 @@ var OAuthCredentialError = class extends Error {
|
|
|
1100
1361
|
login_required: "OAuth session needs login; run ctxdb login for this profile.",
|
|
1101
1362
|
credential_busy: "OAuth credential is busy. If its previous process crashed, run ctxdb login for a new credential.",
|
|
1102
1363
|
invalid_profile: "OAuth credential does not belong to this profile.",
|
|
1103
|
-
unsafe_storage: "OAuth credential storage is
|
|
1364
|
+
unsafe_storage: "OAuth credential storage is inaccessible or invalid."
|
|
1104
1365
|
};
|
|
1105
1366
|
super(messages[code]);
|
|
1106
1367
|
this.name = "OAuthCredentialError";
|
|
@@ -1110,29 +1371,31 @@ var OAuthCredentialError = class extends Error {
|
|
|
1110
1371
|
var loginRequired = () => new OAuthCredentialError("login_required");
|
|
1111
1372
|
var unsafe = () => new OAuthCredentialError("unsafe_storage");
|
|
1112
1373
|
var OAuthSessionStore = class {
|
|
1113
|
-
|
|
1374
|
+
provider;
|
|
1114
1375
|
now;
|
|
1115
1376
|
lockWaitMs;
|
|
1116
|
-
|
|
1117
|
-
|
|
1377
|
+
path;
|
|
1378
|
+
constructor(path, options = {}) {
|
|
1379
|
+
this.path = path;
|
|
1380
|
+
this.provider = new LocalCredentialProvider(path);
|
|
1118
1381
|
this.now = options.now ?? Date.now;
|
|
1119
1382
|
this.lockWaitMs = options.lockWaitMs ?? 3e4;
|
|
1120
1383
|
}
|
|
1121
|
-
saveLogin(profile, session) {
|
|
1384
|
+
saveLogin(profile, session, loginId) {
|
|
1122
1385
|
requireProfile(profile);
|
|
1123
1386
|
validateSession(session);
|
|
1124
|
-
|
|
1125
|
-
const ref =
|
|
1126
|
-
this.write(ref, {
|
|
1387
|
+
if (loginId !== void 0) requireProfile(loginId);
|
|
1388
|
+
const ref = randomBytes3(16).toString("hex");
|
|
1389
|
+
this.write(ref, { profile, ...loginId ? { loginId } : {}, state: "ready", session });
|
|
1127
1390
|
return ref;
|
|
1128
1391
|
}
|
|
1129
|
-
status(ref,
|
|
1130
|
-
const record = this.read(ref,
|
|
1131
|
-
if (!record) return { state:
|
|
1392
|
+
status(ref, owner) {
|
|
1393
|
+
const record = this.read(ref, owner);
|
|
1394
|
+
if (!record) return { state: "missing" };
|
|
1132
1395
|
const s = record.session;
|
|
1133
1396
|
return {
|
|
1134
1397
|
state: record.state,
|
|
1135
|
-
profile,
|
|
1398
|
+
profile: record.profile,
|
|
1136
1399
|
baseUrl: s.baseUrl,
|
|
1137
1400
|
workspaceId: s.workspaceId,
|
|
1138
1401
|
memberId: s.memberId,
|
|
@@ -1143,12 +1406,12 @@ var OAuthSessionStore = class {
|
|
|
1143
1406
|
accessExpiresAt: s.expiresAt
|
|
1144
1407
|
};
|
|
1145
1408
|
}
|
|
1146
|
-
async resolve(ref,
|
|
1147
|
-
const first = this.read(ref,
|
|
1148
|
-
if (!first || first.state
|
|
1409
|
+
async resolve(ref, owner, transport) {
|
|
1410
|
+
const first = this.read(ref, owner);
|
|
1411
|
+
if (!first || ["reauth_required", "logged_out"].includes(first.state)) throw loginRequired();
|
|
1149
1412
|
if (first.state === "ready" && !this.needsRefresh(first.session)) return authorization(first.session);
|
|
1150
1413
|
return this.withLock(ref, async () => {
|
|
1151
|
-
const current = this.read(ref,
|
|
1414
|
+
const current = this.read(ref, owner);
|
|
1152
1415
|
if (!current || current.state !== "ready") throw loginRequired();
|
|
1153
1416
|
if (!this.needsRefresh(current.session)) return authorization(current.session);
|
|
1154
1417
|
const started = this.now();
|
|
@@ -1159,170 +1422,160 @@ var OAuthSessionStore = class {
|
|
|
1159
1422
|
if (next.expiresAt <= this.now()) throw loginRequired();
|
|
1160
1423
|
this.write(ref, { ...current, state: "ready", session: next });
|
|
1161
1424
|
return authorization(next);
|
|
1162
|
-
} catch {
|
|
1163
|
-
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
this.write(ref, { ...current, state: "reauth_required" });
|
|
1427
|
+
if (error instanceof OAuthCredentialError) throw error;
|
|
1164
1428
|
throw loginRequired();
|
|
1165
1429
|
}
|
|
1166
1430
|
});
|
|
1167
1431
|
}
|
|
1168
|
-
async
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if (current)
|
|
1173
|
-
|
|
1174
|
-
|
|
1432
|
+
async share(ref, profile, loginId, commit) {
|
|
1433
|
+
requireProfile(loginId);
|
|
1434
|
+
return this.withLock(ref, async () => {
|
|
1435
|
+
const current = this.read(ref, profile);
|
|
1436
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1437
|
+
if (current.loginId && current.loginId !== loginId) throw new OAuthCredentialError("invalid_profile");
|
|
1438
|
+
const receipt = this.write(ref, { ...current, loginId });
|
|
1439
|
+
try {
|
|
1440
|
+
return commit();
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
this.provider.rollback(receipt);
|
|
1443
|
+
throw error;
|
|
1175
1444
|
}
|
|
1176
|
-
}
|
|
1177
|
-
} finally {
|
|
1178
|
-
this.prepareDirectory();
|
|
1179
|
-
this.atomicWrite(this.path(ref, ".logged-out"), "");
|
|
1180
|
-
remove(this.path(ref));
|
|
1181
|
-
}
|
|
1182
|
-
return { remoteRevoked };
|
|
1183
|
-
}
|
|
1184
|
-
needsRefresh(session) {
|
|
1185
|
-
const advance = Math.min(3e4, (session.expiresAt - session.issuedAt) / 2);
|
|
1186
|
-
return this.now() >= session.expiresAt - advance;
|
|
1445
|
+
});
|
|
1187
1446
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1447
|
+
/** Setup changes the DATA endpoint without replacing the identity or racing a refresh. */
|
|
1448
|
+
async changeBaseUrl(ref, owner, baseUrl, beforeWrite) {
|
|
1449
|
+
await this.withLock(ref, async () => {
|
|
1450
|
+
this.provider.transaction((tx) => {
|
|
1451
|
+
beforeWrite();
|
|
1452
|
+
const current = this.read(ref, owner);
|
|
1453
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1454
|
+
const session = { ...current.session, baseUrl };
|
|
1455
|
+
validateSession(session);
|
|
1456
|
+
const before = tx.read(ref), candidate = credentialForSession({ ...current, session });
|
|
1457
|
+
tx.put(ref, { ...candidate, metadata: { ...before.record?.metadata, ...candidate.metadata } }, before);
|
|
1458
|
+
});
|
|
1459
|
+
});
|
|
1191
1460
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1461
|
+
async logout(ref, owner, transport, beforeRevoke = () => {
|
|
1462
|
+
}) {
|
|
1463
|
+
return this.withLock(ref, async () => {
|
|
1464
|
+
const current = this.read(ref, owner);
|
|
1465
|
+
this.provider.transaction((tx) => {
|
|
1466
|
+
beforeRevoke();
|
|
1467
|
+
const before = tx.read(ref);
|
|
1468
|
+
if (current && before.record) tx.put(ref, { ...before.record, metadata: { ...before.record.metadata, state: "logged_out" }, payload: {} }, before);
|
|
1469
|
+
});
|
|
1470
|
+
let remoteRevoked = false;
|
|
1471
|
+
try {
|
|
1472
|
+
if (current?.session.refreshToken && current.state !== "logged_out") {
|
|
1473
|
+
await transport.revoke(current.session.refreshToken, current.session.baseUrl);
|
|
1474
|
+
remoteRevoked = true;
|
|
1475
|
+
}
|
|
1476
|
+
} catch {
|
|
1477
|
+
}
|
|
1478
|
+
return { remoteRevoked };
|
|
1479
|
+
});
|
|
1194
1480
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1481
|
+
/** Release config references under the write lock, then delete an unused session.
|
|
1482
|
+
* Waiting on the operation lock first lets an in-flight refresh supply its latest RT.
|
|
1483
|
+
* Returning false from release preserves a session that still has users.
|
|
1484
|
+
*/
|
|
1485
|
+
async retire(ref, owner, transport, release) {
|
|
1486
|
+
return this.withLock(ref, async () => {
|
|
1487
|
+
const current = this.read(ref, owner);
|
|
1488
|
+
const removed = this.provider.transaction((tx) => {
|
|
1489
|
+
if (!release()) return false;
|
|
1490
|
+
tx.remove(tx.read(ref));
|
|
1491
|
+
return true;
|
|
1492
|
+
});
|
|
1493
|
+
if (!removed) return { removed: false, remoteRevoked: false, remoteUnconfirmed: false };
|
|
1494
|
+
if (!current || current.state === "logged_out") return { removed: true, remoteRevoked: false, remoteUnconfirmed: false };
|
|
1198
1495
|
try {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1496
|
+
if (!current.session.refreshToken) throw loginRequired();
|
|
1497
|
+
await transport.revoke(current.session.refreshToken, current.session.baseUrl);
|
|
1498
|
+
return { removed: true, remoteRevoked: true, remoteUnconfirmed: false };
|
|
1499
|
+
} catch {
|
|
1500
|
+
return { removed: true, remoteRevoked: false, remoteUnconfirmed: true };
|
|
1203
1501
|
}
|
|
1204
|
-
}
|
|
1205
|
-
requirePrivate(this.directory, true);
|
|
1502
|
+
});
|
|
1206
1503
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
if (
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const fd = openSync2(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1215
|
-
let text2;
|
|
1216
|
-
try {
|
|
1217
|
-
const stat = fstatSync(fd);
|
|
1218
|
-
if (stat.size > 65536 || !stat.isFile()) throw unsafe();
|
|
1219
|
-
text2 = readFileSync2(fd, "utf8");
|
|
1220
|
-
} finally {
|
|
1221
|
-
closeSync2(fd);
|
|
1222
|
-
}
|
|
1223
|
-
let doc;
|
|
1504
|
+
needsRefresh(session) {
|
|
1505
|
+
return this.now() >= session.expiresAt - Math.min(3e4, (session.expiresAt - session.issuedAt) / 2);
|
|
1506
|
+
}
|
|
1507
|
+
read(ref, owner) {
|
|
1508
|
+
if (!/^[a-f0-9]{32}$/.test(ref)) throw unsafe();
|
|
1509
|
+
requireProfile(typeof owner === "string" ? owner : owner.loginId);
|
|
1510
|
+
let stored;
|
|
1224
1511
|
try {
|
|
1225
|
-
|
|
1512
|
+
stored = this.provider.readStoredRecord(ref);
|
|
1226
1513
|
} catch {
|
|
1227
1514
|
throw unsafe();
|
|
1228
1515
|
}
|
|
1229
|
-
if (!
|
|
1230
|
-
if (
|
|
1231
|
-
|
|
1232
|
-
|
|
1516
|
+
if (!stored) return void 0;
|
|
1517
|
+
if (stored.kind !== "oauth-session" || stored.metadata.owner.distribution !== "public") throw new OAuthCredentialError("invalid_profile");
|
|
1518
|
+
const who = stored.metadata.owner;
|
|
1519
|
+
if (typeof owner === "string" ? who.agent !== owner : who.loginId !== owner.loginId) throw new OAuthCredentialError("invalid_profile");
|
|
1520
|
+
return { profile: who.agent, loginId: who.loginId, state: stored.metadata.state, session: oauthSessionFromCredential(stored) };
|
|
1233
1521
|
}
|
|
1234
1522
|
write(ref, doc) {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
throw loginRequired();
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
atomicWrite(path, text2) {
|
|
1243
|
-
this.prepareDirectory();
|
|
1244
|
-
const temporary = path + "." + randomBytes(12).toString("hex") + ".tmp";
|
|
1245
|
-
let fd;
|
|
1246
|
-
try {
|
|
1247
|
-
fd = openSync2(temporary, "wx", 384);
|
|
1248
|
-
writeFileSync(fd, text2);
|
|
1249
|
-
fsyncSync(fd);
|
|
1250
|
-
closeSync2(fd);
|
|
1251
|
-
fd = void 0;
|
|
1252
|
-
renameSync2(temporary, path);
|
|
1253
|
-
if (process.platform !== "win32") {
|
|
1254
|
-
const dir = openSync2(this.directory, "r");
|
|
1255
|
-
try {
|
|
1256
|
-
fsyncSync(dir);
|
|
1257
|
-
} finally {
|
|
1258
|
-
closeSync2(dir);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
} finally {
|
|
1262
|
-
if (fd !== void 0) closeSync2(fd);
|
|
1263
|
-
remove(temporary);
|
|
1264
|
-
}
|
|
1523
|
+
const before = this.provider.snapshot(ref);
|
|
1524
|
+
if (before.record && ["logged_out", "reauth_required"].includes(before.record.metadata.state)) throw loginRequired();
|
|
1525
|
+
const candidate = credentialForSession(doc);
|
|
1526
|
+
return this.provider.commit(ref, { ...candidate, metadata: { ...before.record?.metadata, ...candidate.metadata } }, before);
|
|
1265
1527
|
}
|
|
1266
1528
|
async withLock(ref, task) {
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
fd = openSync2(path, "wx", 384);
|
|
1274
|
-
} catch (error) {
|
|
1275
|
-
if (error.code !== "EEXIST") throw unsafe();
|
|
1276
|
-
if (Date.now() >= deadline) throw new OAuthCredentialError("credential_busy");
|
|
1277
|
-
await delay2(25);
|
|
1278
|
-
continue;
|
|
1279
|
-
}
|
|
1280
|
-
try {
|
|
1281
|
-
writeFileSync(fd, String(process.pid));
|
|
1282
|
-
fsyncSync(fd);
|
|
1283
|
-
} catch {
|
|
1284
|
-
closeSync2(fd);
|
|
1285
|
-
remove(path);
|
|
1286
|
-
throw unsafe();
|
|
1287
|
-
}
|
|
1288
|
-
closeSync2(fd);
|
|
1289
|
-
try {
|
|
1290
|
-
return await task();
|
|
1291
|
-
} finally {
|
|
1292
|
-
remove(path);
|
|
1293
|
-
}
|
|
1529
|
+
if (!/^[a-f0-9]{32}$/.test(ref)) throw unsafe();
|
|
1530
|
+
try {
|
|
1531
|
+
return await withCredentialOperation(this.path, ref, task, { lockWaitMs: this.lockWaitMs });
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
if (error instanceof Error && error.message === "credentials: credential_busy") throw new OAuthCredentialError("credential_busy");
|
|
1534
|
+
throw error;
|
|
1294
1535
|
}
|
|
1295
1536
|
}
|
|
1296
1537
|
};
|
|
1297
|
-
function
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
throw unsafe();
|
|
1538
|
+
function credentialForSession(doc) {
|
|
1539
|
+
const { accessToken, refreshToken, ...metadata } = doc.session;
|
|
1540
|
+
return {
|
|
1541
|
+
kind: "oauth-session",
|
|
1542
|
+
payload: ["ready", "refreshing"].includes(doc.state) ? { accessToken, refreshToken } : {},
|
|
1543
|
+
metadata: {
|
|
1544
|
+
...metadata,
|
|
1545
|
+
owner: { distribution: "public", agent: doc.profile, ...doc.loginId ? { loginId: doc.loginId } : {} },
|
|
1546
|
+
state: doc.state,
|
|
1547
|
+
authorization_method: "device-code"
|
|
1308
1548
|
}
|
|
1309
|
-
|
|
1310
|
-
}
|
|
1311
|
-
if ((s.mode & 63) !== 0 || process.getuid && s.uid !== process.getuid()) throw unsafe();
|
|
1549
|
+
};
|
|
1312
1550
|
}
|
|
1313
|
-
function
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1551
|
+
function oauthSessionFromCredential(record) {
|
|
1552
|
+
if (record.kind !== "oauth-session") throw unsafe();
|
|
1553
|
+
const m = record.metadata, p = record.payload;
|
|
1554
|
+
const session = {
|
|
1555
|
+
baseUrl: m.baseUrl,
|
|
1556
|
+
workspaceId: m.workspaceId,
|
|
1557
|
+
memberId: m.memberId,
|
|
1558
|
+
clientId: m.clientId,
|
|
1559
|
+
scope: m.scope,
|
|
1560
|
+
sessionId: m.sessionId,
|
|
1561
|
+
accountDisplay: m.accountDisplay,
|
|
1562
|
+
issuedAt: m.issuedAt,
|
|
1563
|
+
expiresAt: m.expiresAt,
|
|
1564
|
+
accessToken: p.accessToken,
|
|
1565
|
+
refreshToken: p.refreshToken
|
|
1566
|
+
};
|
|
1567
|
+
validateSession(session, ["ready", "refreshing"].includes(m.state));
|
|
1568
|
+
return session;
|
|
1569
|
+
}
|
|
1570
|
+
function requireProfile(profile) {
|
|
1571
|
+
if (typeof profile !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(profile)) throw new OAuthCredentialError("invalid_profile");
|
|
1319
1572
|
}
|
|
1320
|
-
function validateSession(s) {
|
|
1573
|
+
function validateSession(s, requireTokens = true) {
|
|
1321
1574
|
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();
|
|
1322
1575
|
for (const value of [s.workspaceId, s.memberId, s.sessionId, s.accountDisplay]) {
|
|
1323
1576
|
if (typeof value !== "string" || !value || value.length > 256 || /[\r\n\x00-\x1f]/.test(value)) throw unsafe();
|
|
1324
1577
|
}
|
|
1325
|
-
for (const value of [s.accessToken, s.refreshToken]) {
|
|
1578
|
+
for (const value of requireTokens ? [s.accessToken, s.refreshToken] : []) {
|
|
1326
1579
|
if (typeof value !== "string" || !value || value.length > 8192 || /\s/.test(value)) throw unsafe();
|
|
1327
1580
|
}
|
|
1328
1581
|
let url;
|
|
@@ -1351,8 +1604,56 @@ function authorization(session) {
|
|
|
1351
1604
|
}
|
|
1352
1605
|
|
|
1353
1606
|
// ../shared/src/oauth-profile.ts
|
|
1354
|
-
import {
|
|
1355
|
-
|
|
1607
|
+
import { dirname as dirname4, join as join3, resolve } from "path";
|
|
1608
|
+
|
|
1609
|
+
// ../shared/src/auth-config.ts
|
|
1610
|
+
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";
|
|
1611
|
+
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
1612
|
+
import { dirname as dirname3 } from "path";
|
|
1613
|
+
var AuthConfigError = class extends Error {
|
|
1614
|
+
constructor(message) {
|
|
1615
|
+
super(message);
|
|
1616
|
+
this.name = "AuthConfigError";
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
function readAuthConfig(path) {
|
|
1620
|
+
if (!existsSync3(path)) return { version: 2, agents: {} };
|
|
1621
|
+
let raw;
|
|
1622
|
+
try {
|
|
1623
|
+
raw = JSON.parse(readFileSync3(path, "utf8"));
|
|
1624
|
+
} catch {
|
|
1625
|
+
throw new AuthConfigError("Invalid ctxdb.json; repair the JSON before changing authentication.");
|
|
1626
|
+
}
|
|
1627
|
+
if (raw?.version !== 2 || !object3(raw.agents) || raw.logins !== void 0 && !object3(raw.logins)) {
|
|
1628
|
+
throw new AuthConfigError("Unsupported ctxdb configuration; authentication was not changed.");
|
|
1629
|
+
}
|
|
1630
|
+
return raw;
|
|
1631
|
+
}
|
|
1632
|
+
function object3(value) {
|
|
1633
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1634
|
+
}
|
|
1635
|
+
function ownAuthProfile(raw, agent) {
|
|
1636
|
+
const value = Object.hasOwn(raw.agents, agent) ? raw.agents[agent] : void 0;
|
|
1637
|
+
if (value !== void 0 && !object3(value)) throw new AuthConfigError("Invalid agent configuration; repair its section in ctxdb.json.");
|
|
1638
|
+
return value ?? {};
|
|
1639
|
+
}
|
|
1640
|
+
function requireLoginId(id) {
|
|
1641
|
+
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.");
|
|
1642
|
+
}
|
|
1643
|
+
function validateLoginChoice(section) {
|
|
1644
|
+
if (Object.hasOwn(section, "credential_ref")) {
|
|
1645
|
+
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.");
|
|
1646
|
+
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.");
|
|
1647
|
+
}
|
|
1648
|
+
if (!Object.hasOwn(section, "access_credential")) return;
|
|
1649
|
+
if (section.access_credential === null) return;
|
|
1650
|
+
requireLoginId(section.access_credential);
|
|
1651
|
+
if (section.api_key || section.oauth_credential_ref) {
|
|
1652
|
+
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.");
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// ../shared/src/oauth-profile.ts
|
|
1356
1657
|
function oauthProfile(reference, configPath, profile, baseUrl) {
|
|
1357
1658
|
if (reference === void 0) return void 0;
|
|
1358
1659
|
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");
|
|
@@ -1361,34 +1662,150 @@ function oauthProfile(reference, configPath, profile, baseUrl) {
|
|
|
1361
1662
|
reference,
|
|
1362
1663
|
profile,
|
|
1363
1664
|
configPath: path,
|
|
1364
|
-
|
|
1665
|
+
credentialsPath: join3(dirname4(path), "credentials.json"),
|
|
1365
1666
|
baseUrl: normalizeCoreOrigin(baseUrl)
|
|
1366
1667
|
};
|
|
1367
1668
|
}
|
|
1669
|
+
function oauthProfileFromConfig(raw, path, agent, baseUrlOverride) {
|
|
1670
|
+
const section = ownAuthProfile(raw, agent);
|
|
1671
|
+
validateLoginChoice(section);
|
|
1672
|
+
if (section.access_credential === null) return void 0;
|
|
1673
|
+
if (Object.hasOwn(section, "access_credential")) {
|
|
1674
|
+
const id = section.access_credential;
|
|
1675
|
+
requireLoginId(id);
|
|
1676
|
+
const record = raw.logins && Object.hasOwn(raw.logins, id) ? raw.logins[id] : void 0;
|
|
1677
|
+
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.");
|
|
1678
|
+
return storedProfile(record.credential_ref, path, agent, id, baseUrlOverride);
|
|
1679
|
+
}
|
|
1680
|
+
return storedProfile(section.oauth_credential_ref, path, agent, void 0, baseUrlOverride);
|
|
1681
|
+
}
|
|
1682
|
+
function storedProfile(reference, path, agent, loginId, baseUrlOverride) {
|
|
1683
|
+
const descriptor = oauthProfile(reference, path, agent, "https://api.cn-hangzhou.agentcontext.aliyuncs.com");
|
|
1684
|
+
if (!descriptor) return void 0;
|
|
1685
|
+
const profile = { ...descriptor, ...loginId ? { loginId } : {} };
|
|
1686
|
+
const status = new OAuthSessionStore(profile.credentialsPath).status(profile.reference, oauthOwner(profile));
|
|
1687
|
+
if (!("baseUrl" in status) || typeof status.baseUrl !== "string") throw new OAuthCredentialError("login_required");
|
|
1688
|
+
profile.baseUrl = normalizeCoreOrigin(status.baseUrl);
|
|
1689
|
+
if (baseUrlOverride && normalizeCoreOrigin(baseUrlOverride) !== profile.baseUrl) {
|
|
1690
|
+
throw new AuthConfigError("Requested base URL does not match the login Core origin; no token was sent.");
|
|
1691
|
+
}
|
|
1692
|
+
return profile;
|
|
1693
|
+
}
|
|
1694
|
+
function oauthOwner(profile) {
|
|
1695
|
+
return profile.loginId ? { loginId: profile.loginId } : profile.profile;
|
|
1696
|
+
}
|
|
1697
|
+
function oauthProfileStatus(profile) {
|
|
1698
|
+
const status = new OAuthSessionStore(profile.credentialsPath).status(profile.reference, oauthOwner(profile));
|
|
1699
|
+
if (typeof status.baseUrl === "string" && normalizeCoreOrigin(status.baseUrl) !== profile.baseUrl) {
|
|
1700
|
+
throw new AuthConfigError("Requested base URL does not match the login Core origin; no token was sent.");
|
|
1701
|
+
}
|
|
1702
|
+
return status;
|
|
1703
|
+
}
|
|
1368
1704
|
var OAuthProfileProvider = class {
|
|
1369
1705
|
selected;
|
|
1370
1706
|
constructor(selected) {
|
|
1371
1707
|
this.selected = selected;
|
|
1372
1708
|
}
|
|
1373
|
-
async resolve() {
|
|
1709
|
+
async resolve(options = {}) {
|
|
1710
|
+
const timeoutMs = options.timeoutMs ?? 3e4, deadline = Date.now() + timeoutMs;
|
|
1374
1711
|
const selected = this.selected;
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
raw
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1381
|
-
const profile = raw?.version === 2 ? raw.agents?.[selected.profile] : void 0;
|
|
1382
|
-
if (!profile || !Object.hasOwn(profile, "oauth_credential_ref")) throw new OAuthCredentialError("login_required");
|
|
1383
|
-
const current = oauthProfile(profile.oauth_credential_ref, selected.configPath, selected.profile, profile.base_url);
|
|
1712
|
+
const readCurrent = () => {
|
|
1713
|
+
const raw = readAuthConfig(selected.configPath);
|
|
1714
|
+
return oauthProfileFromConfig(raw, selected.configPath, selected.profile);
|
|
1715
|
+
};
|
|
1716
|
+
const current = readCurrent();
|
|
1384
1717
|
if (!current || current.baseUrl !== selected.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1385
|
-
const store = new OAuthSessionStore(current.
|
|
1386
|
-
const status =
|
|
1718
|
+
const store = new OAuthSessionStore(current.credentialsPath, { lockWaitMs: timeoutMs });
|
|
1719
|
+
const status = oauthProfileStatus(current);
|
|
1387
1720
|
if (!("baseUrl" in status) || typeof status.baseUrl !== "string" || normalizeCoreOrigin(status.baseUrl) !== current.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1388
|
-
|
|
1721
|
+
const authorization2 = await store.resolve(current.reference, oauthOwner(current), {
|
|
1722
|
+
refresh: (rt, url) => new CoreDeviceClient(url, { requestTimeoutMs: Math.max(1, deadline - Date.now()) }).refresh(rt, url)
|
|
1723
|
+
});
|
|
1724
|
+
const after = readCurrent();
|
|
1725
|
+
if (!after || after.reference !== current.reference || after.loginId !== current.loginId || after.baseUrl !== current.baseUrl || oauthProfileStatus(after).state !== "ready") throw new OAuthCredentialError("login_required");
|
|
1726
|
+
return authorization2;
|
|
1389
1727
|
}
|
|
1390
1728
|
};
|
|
1391
1729
|
|
|
1730
|
+
// ../shared/src/login-bindings.ts
|
|
1731
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
1732
|
+
import { dirname as dirname6, join as join5 } from "path";
|
|
1733
|
+
|
|
1734
|
+
// ../shared/src/data-auth.ts
|
|
1735
|
+
import { dirname as dirname5, join as join4 } from "path";
|
|
1736
|
+
function selectDataApiCredential(cfg, env = process.env) {
|
|
1737
|
+
const accessToken = env.CTXDB_ACCESS_TOKEN?.trim();
|
|
1738
|
+
if (accessToken) return { type: "access-token", value: accessToken };
|
|
1739
|
+
if (env.CTXDB_API_KEY) return { type: "api-key", value: env.CTXDB_API_KEY };
|
|
1740
|
+
const context = cfg.authContext ? { context: cfg.authContext } : {};
|
|
1741
|
+
if (cfg.oauthCredential) return { type: "oauth-session", profile: cfg.oauthCredential, ...context };
|
|
1742
|
+
return cfg.apiKey ? { type: "api-key", value: cfg.apiKey, ...context } : null;
|
|
1743
|
+
}
|
|
1744
|
+
var DataApiAuthorizationProvider = class {
|
|
1745
|
+
context;
|
|
1746
|
+
constructor(context) {
|
|
1747
|
+
this.context = context;
|
|
1748
|
+
}
|
|
1749
|
+
async resolve(options = {}) {
|
|
1750
|
+
const c = this.context, raw = readAuthConfig(c.configPath), section = ownAuthProfile(raw, c.agent);
|
|
1751
|
+
const fallback = c.inheritDefaultKey && c.agent !== "default" ? ownAuthProfile(raw, "default") : {};
|
|
1752
|
+
if (c.managedCredentials) {
|
|
1753
|
+
const active = readActiveCredentialSync(c.credentialsPath ?? join4(dirname5(c.configPath), "credentials.json"));
|
|
1754
|
+
if (active) {
|
|
1755
|
+
const baseUrl2 = c.baseUrlOverride ?? active.metadata.baseUrl;
|
|
1756
|
+
if (baseUrl2 !== c.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1757
|
+
return { scheme: "Token", value: active.payload.apiKey, baseUrl: baseUrl2, expiresAt: null };
|
|
1758
|
+
}
|
|
1759
|
+
throw new OAuthCredentialError("login_required");
|
|
1760
|
+
}
|
|
1761
|
+
const stored = publicAuthenticationFromConfig(raw, c.configPath, c.agent, c.baseUrlOverride);
|
|
1762
|
+
const baseUrl = stored?.baseUrl ?? String(c.baseUrlOverride || section.base_url || fallback.base_url || "https://api.cn-hangzhou.agentcontext.aliyuncs.com").replace(/\/+$/, "");
|
|
1763
|
+
if (baseUrl !== c.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1764
|
+
if (stored?.apiKey) return { scheme: "Token", value: stored.apiKey, baseUrl, expiresAt: null };
|
|
1765
|
+
if (stored?.oauthCredential) return new OAuthProfileProvider(stored.oauthCredential).resolve(options);
|
|
1766
|
+
if (Object.hasOwn(section, "access_credential")) throw new OAuthCredentialError("login_required");
|
|
1767
|
+
const key = Object.hasOwn(section, "api_key") ? section.api_key : fallback.api_key;
|
|
1768
|
+
if (typeof key !== "string" || !key) throw new OAuthCredentialError("login_required");
|
|
1769
|
+
return { scheme: "Token", value: key, baseUrl, expiresAt: null };
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
function publicAuthenticationFromConfig(raw, path, agent, baseUrlOverride) {
|
|
1773
|
+
const section = ownAuthProfile(raw, agent);
|
|
1774
|
+
validateLoginChoice(section);
|
|
1775
|
+
if (!Object.hasOwn(section, "credential_ref")) {
|
|
1776
|
+
const oauthCredential = oauthProfileFromConfig(raw, path, agent, baseUrlOverride);
|
|
1777
|
+
return oauthCredential ? { baseUrl: oauthCredential.baseUrl, oauthCredential } : void 0;
|
|
1778
|
+
}
|
|
1779
|
+
const r = new LocalCredentialProvider(join4(dirname5(path), "credentials.json")).readStoredRecord(section.credential_ref);
|
|
1780
|
+
if (!r || r.kind !== "api-key" || r.metadata.state !== "ready" || r.metadata.owner.distribution !== "public" || r.metadata.owner.agent !== agent || r.metadata.owner.loginId) {
|
|
1781
|
+
throw new AuthConfigError("credential_ref is missing, invalid or belongs to another Agent. Use setup --api-key/--login to choose a valid credential.");
|
|
1782
|
+
}
|
|
1783
|
+
const baseUrl = String(r.metadata.base_url).replace(/\/+$/, "");
|
|
1784
|
+
if (baseUrlOverride && baseUrl !== baseUrlOverride.replace(/\/+$/, "")) throw new AuthConfigError("Requested base URL does not match the credential origin; no credential was sent.");
|
|
1785
|
+
return { baseUrl, apiKey: r.payload.api_key };
|
|
1786
|
+
}
|
|
1787
|
+
function createRequestAuthorizationProvider(credential, baseUrl) {
|
|
1788
|
+
if (credential?.context) return new DataApiAuthorizationProvider(credential.context);
|
|
1789
|
+
if (credential?.type === "oauth-session") return new OAuthProfileProvider(credential.profile);
|
|
1790
|
+
return { async resolve(_options) {
|
|
1791
|
+
if (!credential) throw new OAuthCredentialError("login_required");
|
|
1792
|
+
return {
|
|
1793
|
+
scheme: credential.type === "access-token" ? "Bearer" : "Token",
|
|
1794
|
+
value: credential.value,
|
|
1795
|
+
baseUrl,
|
|
1796
|
+
expiresAt: null
|
|
1797
|
+
};
|
|
1798
|
+
} };
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// ../shared/src/authorization/public-target.ts
|
|
1802
|
+
import { dirname as dirname7, join as join6 } from "path";
|
|
1803
|
+
|
|
1804
|
+
// src/config.ts
|
|
1805
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1806
|
+
import { homedir as homedir3 } from "os";
|
|
1807
|
+
import { dirname as dirname8, join as join7 } from "path";
|
|
1808
|
+
|
|
1392
1809
|
// src/distribution-capabilities.ts
|
|
1393
1810
|
var PUBLIC_DISTRIBUTION = {
|
|
1394
1811
|
id: "public",
|
|
@@ -1403,7 +1820,7 @@ var PUBLIC_DISTRIBUTION = {
|
|
|
1403
1820
|
var DISTRIBUTION_MANIFEST = typeof define_CTXDB_DISTRIBUTION_MANIFEST_default === "undefined" ? PUBLIC_DISTRIBUTION : define_CTXDB_DISTRIBUTION_MANIFEST_default;
|
|
1404
1821
|
|
|
1405
1822
|
// src/config.ts
|
|
1406
|
-
var DEFAULT_BASE_URL = "https://
|
|
1823
|
+
var DEFAULT_BASE_URL = "https://api.cn-hangzhou.agentcontext.aliyuncs.com";
|
|
1407
1824
|
var DEFAULT_USER_ID = "default";
|
|
1408
1825
|
var DEFAULT_TOP_K = 5;
|
|
1409
1826
|
var DEFAULT_THRESHOLD = 0.4;
|
|
@@ -1411,7 +1828,7 @@ var DEFAULT_KNOWLEDGE_TOP_K = 6;
|
|
|
1411
1828
|
var DEFAULT_KB_CATALOG_INJECTION = "session_start";
|
|
1412
1829
|
function defaultPath(env) {
|
|
1413
1830
|
if (env.CTXDB_CONFIG_PATH) return env.CTXDB_CONFIG_PATH;
|
|
1414
|
-
return
|
|
1831
|
+
return join7(homedir3(), ".ctxdb", "ctxdb.json");
|
|
1415
1832
|
}
|
|
1416
1833
|
function coerceInt(v, fallback) {
|
|
1417
1834
|
if (v === null || v === void 0 || v === "") return fallback;
|
|
@@ -1433,7 +1850,7 @@ function coerceKbCatalogInjection(v) {
|
|
|
1433
1850
|
return DEFAULT_KB_CATALOG_INJECTION;
|
|
1434
1851
|
}
|
|
1435
1852
|
function readRaw(path) {
|
|
1436
|
-
if (!
|
|
1853
|
+
if (!existsSync4(path)) return {};
|
|
1437
1854
|
try {
|
|
1438
1855
|
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1439
1856
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
@@ -1470,40 +1887,6 @@ function applyDebugPolicy(cfg) {
|
|
|
1470
1887
|
cfg.debugReason = policy.debugReason;
|
|
1471
1888
|
return cfg;
|
|
1472
1889
|
}
|
|
1473
|
-
function managedCredential(path) {
|
|
1474
|
-
if (!existsSync3(path)) return null;
|
|
1475
|
-
if (process.platform !== "win32" && (statSync2(path).mode & 63) !== 0) {
|
|
1476
|
-
throw new Error(`credentials: ${path} must be owner-only (run chmod 600)`);
|
|
1477
|
-
}
|
|
1478
|
-
const raw = JSON.parse(readFileSync4(path, "utf8"));
|
|
1479
|
-
if (raw.version !== 1 || !raw.records || typeof raw.records !== "object") {
|
|
1480
|
-
throw new Error("credentials: unsupported credentials.json schema");
|
|
1481
|
-
}
|
|
1482
|
-
const unknownKeys = Object.keys(raw.records).filter(
|
|
1483
|
-
(key) => key !== "contextdb/active"
|
|
1484
|
-
);
|
|
1485
|
-
if (unknownKeys.length > 0) {
|
|
1486
|
-
throw new Error(`credentials: unsupported record key ${unknownKeys[0]}`);
|
|
1487
|
-
}
|
|
1488
|
-
const record = raw.records["contextdb/active"];
|
|
1489
|
-
if (!record) return null;
|
|
1490
|
-
if (record.kind !== "api-key" || typeof record.payload?.api_key !== "string" || !record.payload.api_key || typeof record.payload?.base_url !== "string" || !record.payload.base_url) {
|
|
1491
|
-
throw new Error("credentials: invalid contextdb/active record");
|
|
1492
|
-
}
|
|
1493
|
-
let baseUrl;
|
|
1494
|
-
try {
|
|
1495
|
-
baseUrl = new URL(record.payload.base_url);
|
|
1496
|
-
} catch {
|
|
1497
|
-
throw new Error("credentials: invalid contextdb/active base_url");
|
|
1498
|
-
}
|
|
1499
|
-
if (!["https:", "http:"].includes(baseUrl.protocol) || baseUrl.username || baseUrl.password || baseUrl.search || baseUrl.hash) {
|
|
1500
|
-
throw new Error("credentials: invalid contextdb/active base_url");
|
|
1501
|
-
}
|
|
1502
|
-
return {
|
|
1503
|
-
apiKey: record.payload.api_key,
|
|
1504
|
-
baseUrl: baseUrl.toString().replace(/\/$/, "")
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
1890
|
function loadOpencodeConfig(options = {}) {
|
|
1508
1891
|
const env = options.env ?? process.env;
|
|
1509
1892
|
const path = options.path ?? defaultPath(env);
|
|
@@ -1529,16 +1912,41 @@ function loadOpencodeConfig(options = {}) {
|
|
|
1529
1912
|
debugReason: null,
|
|
1530
1913
|
kbCatalogInjection: coerceKbCatalogInjection(section.kb_catalog_injection)
|
|
1531
1914
|
};
|
|
1532
|
-
const managed = DISTRIBUTION_MANIFEST.capabilities.managedCredentials && options.managedCredentials !== false ?
|
|
1533
|
-
options.credentialsPath ??
|
|
1915
|
+
const managed = DISTRIBUTION_MANIFEST.capabilities.managedCredentials && options.managedCredentials !== false ? readActiveCredentialSync(
|
|
1916
|
+
options.credentialsPath ?? join7(dirname8(path), "credentials.json")
|
|
1534
1917
|
) : null;
|
|
1535
1918
|
if (managed) {
|
|
1536
|
-
cfg.apiKey = managed.apiKey;
|
|
1537
|
-
cfg.baseUrl = managed.baseUrl;
|
|
1919
|
+
cfg.apiKey = managed.payload.apiKey;
|
|
1920
|
+
cfg.baseUrl = managed.metadata.baseUrl;
|
|
1921
|
+
}
|
|
1922
|
+
if (DISTRIBUTION_MANIFEST.capabilities.deviceFlow) {
|
|
1923
|
+
validateLoginChoice(section);
|
|
1924
|
+
const stored = env.CTXDB_API_KEY || env.CTXDB_ACCESS_TOKEN?.trim() ? void 0 : publicAuthenticationFromConfig({ ...raw, version: 2, agents: raw.agents ?? {} }, path, "opencode", env.CTXDB_BASE_URL);
|
|
1925
|
+
if (stored) {
|
|
1926
|
+
cfg.baseUrl = stored.baseUrl;
|
|
1927
|
+
cfg.apiKey = stored.apiKey ?? null;
|
|
1928
|
+
cfg.oauthCredential = stored.oauthCredential;
|
|
1929
|
+
} else if (section.credential_ref) cfg.apiKey = null;
|
|
1538
1930
|
}
|
|
1539
1931
|
applyEnv(cfg, env);
|
|
1932
|
+
if (managed) cfg.authContext = {
|
|
1933
|
+
configPath: path,
|
|
1934
|
+
agent: "opencode",
|
|
1935
|
+
baseUrl: cfg.baseUrl,
|
|
1936
|
+
inheritDefaultKey: false,
|
|
1937
|
+
managedCredentials: true,
|
|
1938
|
+
credentialsPath: options.credentialsPath,
|
|
1939
|
+
...env.CTXDB_BASE_URL ? { baseUrlOverride: env.CTXDB_BASE_URL } : {}
|
|
1940
|
+
};
|
|
1540
1941
|
if (DISTRIBUTION_MANIFEST.capabilities.deviceFlow) {
|
|
1541
|
-
cfg.
|
|
1942
|
+
cfg.authContext = {
|
|
1943
|
+
configPath: path,
|
|
1944
|
+
agent: "opencode",
|
|
1945
|
+
baseUrl: cfg.baseUrl,
|
|
1946
|
+
inheritDefaultKey: false,
|
|
1947
|
+
...env.CTXDB_BASE_URL ? { baseUrlOverride: env.CTXDB_BASE_URL } : {}
|
|
1948
|
+
};
|
|
1949
|
+
if (Object.hasOwn(section, "access_credential") && !env.CTXDB_API_KEY) cfg.apiKey = null;
|
|
1542
1950
|
}
|
|
1543
1951
|
if (env.CTXDB_API_KEY || accessTokenFromEnv(env)) cfg.oauthCredential = void 0;
|
|
1544
1952
|
return cfg;
|
|
@@ -1566,25 +1974,28 @@ var HttpClient = class {
|
|
|
1566
1974
|
userAgent;
|
|
1567
1975
|
fetchImpl;
|
|
1568
1976
|
oauthProvider;
|
|
1977
|
+
credential;
|
|
1569
1978
|
constructor(opts) {
|
|
1570
1979
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
1571
1980
|
this.apiKey = opts.apiKey;
|
|
1572
1981
|
this.accessToken = opts.accessToken ?? null;
|
|
1573
|
-
this.
|
|
1982
|
+
this.credential = selectDataApiCredential(opts, { CTXDB_ACCESS_TOKEN: opts.accessToken ?? void 0, CTXDB_API_KEY: opts.environmentApiKey });
|
|
1983
|
+
this.oauthProvider = createRequestAuthorizationProvider(this.credential, this.baseUrl);
|
|
1574
1984
|
this.userAgent = opts.userAgent ?? "ctxdb-opencode-plugin/0.0.0";
|
|
1575
1985
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
1576
1986
|
}
|
|
1577
|
-
async headers(contentType) {
|
|
1987
|
+
async headers(contentType, timeoutMs) {
|
|
1578
1988
|
const h = {
|
|
1579
1989
|
"User-Agent": this.userAgent,
|
|
1580
1990
|
Connection: "close"
|
|
1581
1991
|
};
|
|
1582
|
-
if (this.
|
|
1583
|
-
|
|
1584
|
-
const authorization2 = await this.oauthProvider.resolve();
|
|
1992
|
+
if (this.oauthProvider) {
|
|
1993
|
+
const authorization2 = await this.oauthProvider.resolve({ timeoutMs });
|
|
1585
1994
|
if (authorization2.baseUrl !== this.baseUrl) throw new Error("OAuth environment changed; reload this runtime.");
|
|
1586
|
-
h.Authorization =
|
|
1587
|
-
} else if (this.
|
|
1995
|
+
h.Authorization = `${authorization2.scheme} ${authorization2.value}`;
|
|
1996
|
+
} else if (this.credential && this.credential.type !== "oauth-session") {
|
|
1997
|
+
h.Authorization = `${this.credential.type === "access-token" ? "Bearer" : "Token"} ${this.credential.value}`;
|
|
1998
|
+
}
|
|
1588
1999
|
if (contentType) h["Content-Type"] = contentType;
|
|
1589
2000
|
return h;
|
|
1590
2001
|
}
|
|
@@ -1605,12 +2016,12 @@ var HttpClient = class {
|
|
|
1605
2016
|
return this.request("POST", url, path, JSON.stringify(body), "application/json", options.timeoutMs);
|
|
1606
2017
|
}
|
|
1607
2018
|
async request(method, url, path, body, contentType, timeoutMs) {
|
|
1608
|
-
const
|
|
1609
|
-
const
|
|
2019
|
+
const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS, started = Date.now();
|
|
2020
|
+
const headers = await this.headers(contentType, effectiveTimeout);
|
|
2021
|
+
const bearer = headers.Authorization?.replace(/^(Bearer|Token) /, "");
|
|
1610
2022
|
const safeText = (value) => bearer ? String(value).split(bearer).join("[REDACTED]") : String(value);
|
|
1611
|
-
const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1612
2023
|
const controller = new AbortController();
|
|
1613
|
-
const timer = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
2024
|
+
const timer = setTimeout(() => controller.abort(), Math.max(1, effectiveTimeout - (Date.now() - started)));
|
|
1614
2025
|
try {
|
|
1615
2026
|
let resp;
|
|
1616
2027
|
try {
|
|
@@ -1630,7 +2041,8 @@ var HttpClient = class {
|
|
|
1630
2041
|
if (resp.status === 204) return {};
|
|
1631
2042
|
let text2;
|
|
1632
2043
|
try {
|
|
1633
|
-
|
|
2044
|
+
const bodyText = await resp.text();
|
|
2045
|
+
text2 = headers.Authorization?.startsWith("Bearer ") || !resp.ok ? safeText(bodyText) : bodyText;
|
|
1634
2046
|
} catch (err) {
|
|
1635
2047
|
throw new CtxdbHttpError(path, resp.status, `body read failed: ${safeText(err?.message ?? err)}`);
|
|
1636
2048
|
}
|
|
@@ -1904,7 +2316,7 @@ function buildWarmupQuery(cwd, git) {
|
|
|
1904
2316
|
}
|
|
1905
2317
|
|
|
1906
2318
|
// src/capture.ts
|
|
1907
|
-
import { createHash } from "crypto";
|
|
2319
|
+
import { createHash as createHash4 } from "crypto";
|
|
1908
2320
|
var EMPTY2 = {
|
|
1909
2321
|
captured: false,
|
|
1910
2322
|
reason: "",
|
|
@@ -1971,7 +2383,7 @@ function toParsedMessage(m, index) {
|
|
|
1971
2383
|
return null;
|
|
1972
2384
|
}
|
|
1973
2385
|
function fingerprintMessages(messages) {
|
|
1974
|
-
const h =
|
|
2386
|
+
const h = createHash4("sha256");
|
|
1975
2387
|
for (const m of messages) {
|
|
1976
2388
|
h.update(m.role);
|
|
1977
2389
|
h.update("\0");
|
|
@@ -2045,6 +2457,8 @@ function buildRuntime(config, cwd, env = process.env) {
|
|
|
2045
2457
|
baseUrl: config.baseUrl,
|
|
2046
2458
|
apiKey: config.apiKey,
|
|
2047
2459
|
oauthCredential: config.oauthCredential,
|
|
2460
|
+
authContext: config.authContext,
|
|
2461
|
+
environmentApiKey: env.CTXDB_API_KEY,
|
|
2048
2462
|
accessToken: accessTokenFromEnv(env)
|
|
2049
2463
|
}),
|
|
2050
2464
|
sessionState: /* @__PURE__ */ new Map(),
|
|
@@ -2084,7 +2498,13 @@ function extractTextPrompt(parts) {
|
|
|
2084
2498
|
}
|
|
2085
2499
|
async function buildHooks(input) {
|
|
2086
2500
|
if (shouldSkipHooks()) return {};
|
|
2087
|
-
|
|
2501
|
+
let config;
|
|
2502
|
+
try {
|
|
2503
|
+
config = loadOpencodeConfig();
|
|
2504
|
+
} catch {
|
|
2505
|
+
process.stderr.write("[ctxdb] Invalid authentication configuration; OpenCode integration skipped. Run ctxdb auth status --all.\n");
|
|
2506
|
+
return {};
|
|
2507
|
+
}
|
|
2088
2508
|
if (!isConfigured(config)) {
|
|
2089
2509
|
logDebug(
|
|
2090
2510
|
config,
|