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