@aliyunrds/ctxdb 1.0.8-beta.2 → 1.0.8-beta.4

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.
@@ -1,10 +1,10 @@
1
1
  // <define:__CTXDB_DISTRIBUTION_MANIFEST__>
2
- var define_CTXDB_DISTRIBUTION_MANIFEST_default = { id: "public", packageName: "@aliyunrds/ctxdb", packageRegistry: null, capabilities: { interactiveLogin: false, managedCredentials: false } };
2
+ var define_CTXDB_DISTRIBUTION_MANIFEST_default = { id: "public", packageName: "@aliyunrds/ctxdb", packageRegistry: null, capabilities: { interactiveLogin: false, managedCredentials: false, deviceFlow: true } };
3
3
 
4
4
  // src/config.ts
5
- import { readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2 } from "fs";
5
+ import { readFileSync as readFileSync4, existsSync as existsSync3, statSync as statSync2 } from "fs";
6
6
  import { homedir as homedir2 } from "os";
7
- import { dirname as dirname2, join as join2 } from "path";
7
+ import { dirname as dirname3, join as join5 } from "path";
8
8
 
9
9
  // ../shared/src/graph-context.ts
10
10
  var GRAPH_CONTEXT_TAG = "graphrag";
@@ -435,8 +435,8 @@ function getMemoryImportance(memory) {
435
435
  };
436
436
  return defaults[cat] ?? 0.5;
437
437
  }
438
- function estimateTokens(text) {
439
- return Math.ceil(text.length / CHARS_PER_TOKEN);
438
+ function estimateTokens(text2) {
439
+ return Math.ceil(text2.length / CHARS_PER_TOKEN);
440
440
  }
441
441
  function rankMemories(memories, categoryOrder) {
442
442
  const orderMap = new Map(categoryOrder.map((cat, i) => [cat, i]));
@@ -690,9 +690,9 @@ function sanitizeRecallFailureReason(reason, secrets = []) {
690
690
  }
691
691
  function sanitizeTraceString(value, secrets) {
692
692
  let sanitized = value.replace(/authorization\s*[:=]\s*[^\r\n]*/gi, "Authorization: [REDACTED]").replace(/(api[_-]?key|access[_-]?token)\s*[:=]\s*[^\s,;]+/gi, "$1=[REDACTED]");
693
- for (const secret of secrets) {
694
- if (!secret) continue;
695
- sanitized = sanitized.split(secret).join("[REDACTED]");
693
+ for (const secret2 of secrets) {
694
+ if (!secret2) continue;
695
+ sanitized = sanitized.split(secret2).join("[REDACTED]");
696
696
  }
697
697
  return sanitized;
698
698
  }
@@ -827,10 +827,568 @@ function isRecallTraceRecord(value) {
827
827
  const baseValid = record.schema_version === RECALL_TRACE_SCHEMA_VERSION && (record.type === "recall.start" || record.type === "recall.finish") && isIsoTimestamp(record.timestamp) && typeof record.recall_event_id === "string" && record.recall_event_id.length > 0 && (typeof record.session_id === "string" || record.session_id === null) && isRecallTraceAgent(record.agent) && (record.kind === "prompt" || record.kind === "warmup") && typeof record.query === "string" && isRecallTraceRequest(record.request);
828
828
  if (!baseValid) return false;
829
829
  if (record.type === "recall.start") return true;
830
+ if (record.type !== "recall.finish") return false;
830
831
  const selection = record.selection;
831
832
  return isNonNegativeFinite(record.duration_ms) && typeof record.outcome === "string" && TRACE_OUTCOMES.has(record.outcome) && (typeof record.failure_reason === "string" || record.failure_reason === null) && Array.isArray(record.returned_memories) && Array.isArray(record.returned_knowledge) && Boolean(selection) && Array.isArray(selection?.selected) && Array.isArray(selection?.excluded) && isNonNegativeFinite(selection?.token_estimate) && typeof record.recall_context === "string";
832
833
  }
833
834
 
835
+ // ../shared/src/device-flow.ts
836
+ import { setTimeout as delay } from "timers/promises";
837
+ var DeviceFlowError = class extends Error {
838
+ code;
839
+ constructor(code) {
840
+ super(code === "service_unavailable" ? "ContextDB authentication service is unavailable. No automatic retry was performed; start a new login later if authorization could not be completed." : `ContextDB device authorization: ${code}. Start a new login if authorization could not be completed.`);
841
+ this.name = "DeviceFlowError";
842
+ this.code = code;
843
+ }
844
+ };
845
+ var invalid = () => new DeviceFlowError("invalid_response");
846
+ var scopes = ["read_only", "read_write", "default"];
847
+ var clientId = "ctxdb-cli";
848
+ var grantType = "urn:ietf:params:oauth:grant-type:device_code";
849
+ function normalizeCoreOrigin(value) {
850
+ let url;
851
+ try {
852
+ url = new URL(value);
853
+ } catch {
854
+ throw new Error("Invalid Core server origin");
855
+ }
856
+ const local = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
857
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.protocol === "https:" || local && url.protocol === "http:")) throw new Error("Invalid Core server origin");
858
+ return url.origin;
859
+ }
860
+ var CoreDeviceClient = class {
861
+ baseUrl;
862
+ fetchImpl;
863
+ now;
864
+ sleep;
865
+ constructor(baseUrl, options = {}) {
866
+ this.baseUrl = normalizeCoreOrigin(baseUrl);
867
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
868
+ this.now = options.now ?? Date.now;
869
+ this.sleep = options.sleep ?? ((ms, signal) => delay(ms, void 0, { signal }));
870
+ }
871
+ async login(options) {
872
+ const started = this.now();
873
+ const initial = await this.post("/v1/auth/device/authorize", { client_id: clientId }, options.signal);
874
+ if (!initial.ok) throw new DeviceFlowError("request_rejected");
875
+ const data = initial.data;
876
+ const requestId = text(data.request_id, 32), userCode = text(data.user_code, 9), code = text(data.device_code, 43);
877
+ if (!/^[0-9a-f]{32}$/.test(requestId) || !/^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$/.test(userCode) || !/^[A-Za-z0-9_-]{43}$/.test(code)) throw invalid();
878
+ const expiresIn = integer(data.expires_in, 1, 3600);
879
+ let interval = integer(data.interval, 1, 3600);
880
+ const deadline = started + expiresIn * 1e3;
881
+ options.onPrompt({ requestId, userCode, expiresIn, interval });
882
+ while (true) {
883
+ if (options.signal?.aborted) throw new DeviceFlowError("cancelled");
884
+ if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
885
+ try {
886
+ await this.sleep(Math.min(interval * 1e3, deadline - this.now()), options.signal);
887
+ } catch {
888
+ throw new DeviceFlowError("cancelled");
889
+ }
890
+ if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
891
+ const issuedAt = this.now();
892
+ const result = await this.post("/v1/auth/device/token", { grant_type: grantType, client_id: clientId, device_code: code }, options.signal);
893
+ if (result.ok) return loginResult(result.data, this.baseUrl, issuedAt, this.now());
894
+ const error = result.data.error;
895
+ if (error === "authorization_pending") continue;
896
+ if (error === "slow_down") {
897
+ interval = Math.max(interval + 5, integer(result.data.interval, 1, 3605));
898
+ continue;
899
+ }
900
+ if (error === "access_denied" || error === "expired_token" || error === "invalid_grant") throw new DeviceFlowError(error);
901
+ throw new DeviceFlowError("request_rejected");
902
+ }
903
+ }
904
+ async refresh(refreshToken, baseUrl) {
905
+ this.requireSameOrigin(baseUrl);
906
+ const result = await this.post("/v1/auth/token/refresh", { refresh_token: secret(refreshToken) });
907
+ if (!result.ok) throw new DeviceFlowError("request_rejected");
908
+ return credentialResult(result.data);
909
+ }
910
+ async revoke(refreshToken, baseUrl) {
911
+ this.requireSameOrigin(baseUrl);
912
+ const result = await this.post("/v1/auth/token/revoke", { refresh_token: secret(refreshToken) });
913
+ if (!result.ok) throw new DeviceFlowError("request_rejected");
914
+ }
915
+ requireSameOrigin(baseUrl) {
916
+ if (normalizeCoreOrigin(baseUrl) !== this.baseUrl) throw new Error("OAuth credential Core origin cannot change");
917
+ }
918
+ async post(path, body, signal) {
919
+ if (signal?.aborted) throw new DeviceFlowError("cancelled");
920
+ const controller = new AbortController();
921
+ const abort = () => controller.abort();
922
+ signal?.addEventListener("abort", abort, { once: true });
923
+ const timer = setTimeout(abort, 3e4);
924
+ timer.unref?.();
925
+ try {
926
+ const response = await this.fetchImpl(this.baseUrl + path, {
927
+ method: "POST",
928
+ body: JSON.stringify(body),
929
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
930
+ redirect: "error",
931
+ credentials: "omit",
932
+ cache: "no-store",
933
+ signal: controller.signal
934
+ });
935
+ const box = await boundedJson(response);
936
+ if (!Number.isInteger(box.errorCode)) throw invalid();
937
+ const ok = response.ok && box.errorCode === 0;
938
+ const data = (!ok || path === "/v1/auth/token/revoke") && box.data == null ? {} : object(box.data);
939
+ if (!ok && response.status >= 500) throw new DeviceFlowError("service_unavailable");
940
+ return { ok, data };
941
+ } catch (error) {
942
+ if (signal?.aborted) throw new DeviceFlowError("cancelled");
943
+ if (error instanceof DeviceFlowError) throw error;
944
+ throw new DeviceFlowError("network_error");
945
+ } finally {
946
+ clearTimeout(timer);
947
+ signal?.removeEventListener("abort", abort);
948
+ }
949
+ }
950
+ };
951
+ function text(value, maximum) {
952
+ if (typeof value !== "string" || !value || value.length > maximum || /[\x00-\x1f\x7f]/.test(value)) throw invalid();
953
+ return value;
954
+ }
955
+ function integer(value, min, max) {
956
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw invalid();
957
+ return value;
958
+ }
959
+ function secret(value) {
960
+ const token = text(value, 8192);
961
+ if (/\s/.test(token)) throw invalid();
962
+ return token;
963
+ }
964
+ function object(value) {
965
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid();
966
+ return value;
967
+ }
968
+ function credentialResult(data) {
969
+ if (data.token_type !== "Bearer" || !scopes.includes(data.scope)) throw invalid();
970
+ return {
971
+ access_token: secret(data.access_token),
972
+ refresh_token: secret(data.refresh_token),
973
+ token_type: "Bearer",
974
+ expires_in: integer(data.expires_in, 1, 3600),
975
+ session_id: text(data.session_id, 64),
976
+ scope: data.scope
977
+ };
978
+ }
979
+ function loginResult(data, baseUrl, issuedAt, now) {
980
+ const tokens = credentialResult(data), scope = tokens.scope;
981
+ if (data.client_id !== clientId || scope !== "default") throw invalid();
982
+ const expiresAt = issuedAt + tokens.expires_in * 1e3;
983
+ if (expiresAt <= now) throw invalid();
984
+ return {
985
+ baseUrl,
986
+ workspaceId: text(data.workspace_id, 64),
987
+ memberId: text(data.member_id, 64),
988
+ scope,
989
+ clientId,
990
+ sessionId: tokens.session_id,
991
+ accountDisplay: text(data.account_display, 256),
992
+ issuedAt,
993
+ expiresAt,
994
+ accessToken: tokens.access_token,
995
+ refreshToken: tokens.refresh_token
996
+ };
997
+ }
998
+ async function boundedJson(response) {
999
+ if (!response.body || Number(response.headers.get("content-length")) > 65536) throw invalid();
1000
+ const reader = response.body.getReader();
1001
+ const chunks = [];
1002
+ let size = 0;
1003
+ try {
1004
+ while (true) {
1005
+ const { done, value } = await reader.read();
1006
+ if (done) break;
1007
+ size += value.byteLength;
1008
+ if (size > 65536) {
1009
+ await reader.cancel();
1010
+ throw invalid();
1011
+ }
1012
+ chunks.push(value);
1013
+ }
1014
+ try {
1015
+ return object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
1016
+ } catch {
1017
+ throw invalid();
1018
+ }
1019
+ } finally {
1020
+ reader.releaseLock();
1021
+ }
1022
+ }
1023
+
1024
+ // ../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";
1042
+
1043
+ // ../shared/src/windows-private-storage.ts
1044
+ import { execFileSync } from "child_process";
1045
+ import { join as join2 } from "path";
1046
+ var SCRIPT = `
1047
+ $ErrorActionPreference = 'Stop'
1048
+ $path = $env:CTXDB_PRIVATE_STORAGE_PATH
1049
+ $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
1050
+ $item = Get-Item -LiteralPath $path -Force
1051
+ if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'reparse' }
1052
+ if ($env:CTXDB_PRIVATE_STORAGE_INITIALIZE -eq '1') {
1053
+ if (-not $item.PSIsContainer) { throw 'not directory' }
1054
+ $acl = [System.Security.AccessControl.DirectorySecurity]::new()
1055
+ $acl.SetOwner($sid)
1056
+ $acl.SetAccessRuleProtection($true, $false)
1057
+ $rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
1058
+ $acl.AddAccessRule($rule)
1059
+ Set-Acl -LiteralPath $path -AclObject $acl
1060
+ }
1061
+ $acl = Get-Acl -LiteralPath $path
1062
+ if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'owner' }
1063
+ $self = $false
1064
+ foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
1065
+ if ($rule.AccessControlType -eq 'Allow') {
1066
+ if ($rule.IdentityReference.Value -ne $sid.Value) { throw 'other principal' }
1067
+ if (($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl) { $self = $true }
1068
+ }
1069
+ }
1070
+ if (-not $self) { throw 'missing owner access' }
1071
+ Write-Output 'private'
1072
+ `;
1073
+ function checkWindowsPrivateStorage(path, initialize, run = execFileSync) {
1074
+ const systemRoot = process.env.SystemRoot;
1075
+ if (!systemRoot) throw new Error("Private Windows credential storage could not be verified.");
1076
+ try {
1077
+ const result = run(
1078
+ join2(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
1079
+ ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(SCRIPT, "utf16le").toString("base64")],
1080
+ {
1081
+ env: { ...process.env, CTXDB_PRIVATE_STORAGE_PATH: path, CTXDB_PRIVATE_STORAGE_INITIALIZE: initialize ? "1" : "0" },
1082
+ encoding: "utf8",
1083
+ windowsHide: true,
1084
+ timeout: 1e4,
1085
+ maxBuffer: 4096,
1086
+ stdio: ["ignore", "pipe", "pipe"]
1087
+ }
1088
+ );
1089
+ if (String(result).trim() !== "private") throw new Error();
1090
+ } catch {
1091
+ throw new Error("Private Windows credential storage could not be verified.");
1092
+ }
1093
+ }
1094
+
1095
+ // ../shared/src/oauth-credentials.ts
1096
+ var OAuthCredentialError = class extends Error {
1097
+ code;
1098
+ constructor(code) {
1099
+ const messages = {
1100
+ login_required: "OAuth session needs login; run ctxdb login for this profile.",
1101
+ credential_busy: "OAuth credential is busy. If its previous process crashed, run ctxdb login for a new credential.",
1102
+ invalid_profile: "OAuth credential does not belong to this profile.",
1103
+ unsafe_storage: "OAuth credential storage is not private or is invalid."
1104
+ };
1105
+ super(messages[code]);
1106
+ this.name = "OAuthCredentialError";
1107
+ this.code = code;
1108
+ }
1109
+ };
1110
+ var loginRequired = () => new OAuthCredentialError("login_required");
1111
+ var unsafe = () => new OAuthCredentialError("unsafe_storage");
1112
+ var OAuthSessionStore = class {
1113
+ directory;
1114
+ now;
1115
+ lockWaitMs;
1116
+ constructor(directory, options = {}) {
1117
+ this.directory = directory;
1118
+ this.now = options.now ?? Date.now;
1119
+ this.lockWaitMs = options.lockWaitMs ?? 3e4;
1120
+ }
1121
+ saveLogin(profile, session) {
1122
+ requireProfile(profile);
1123
+ validateSession(session);
1124
+ this.prepareDirectory();
1125
+ const ref = randomBytes(16).toString("hex");
1126
+ this.write(ref, { version: 1, profile, state: "ready", session });
1127
+ return ref;
1128
+ }
1129
+ status(ref, profile) {
1130
+ const record = this.read(ref, profile);
1131
+ if (!record) return { state: this.isLoggedOut(ref) ? "logged_out" : "missing" };
1132
+ const s = record.session;
1133
+ return {
1134
+ state: record.state,
1135
+ profile,
1136
+ baseUrl: s.baseUrl,
1137
+ workspaceId: s.workspaceId,
1138
+ memberId: s.memberId,
1139
+ clientId: s.clientId,
1140
+ scope: s.scope,
1141
+ sessionId: s.sessionId,
1142
+ accountDisplay: s.accountDisplay,
1143
+ accessExpiresAt: s.expiresAt
1144
+ };
1145
+ }
1146
+ async resolve(ref, profile, transport) {
1147
+ const first = this.read(ref, profile);
1148
+ if (!first || first.state === "reauth_required") throw loginRequired();
1149
+ if (first.state === "ready" && !this.needsRefresh(first.session)) return authorization(first.session);
1150
+ return this.withLock(ref, async () => {
1151
+ const current = this.read(ref, profile);
1152
+ if (!current || current.state !== "ready") throw loginRequired();
1153
+ if (!this.needsRefresh(current.session)) return authorization(current.session);
1154
+ const started = this.now();
1155
+ this.write(ref, { ...current, state: "refreshing" });
1156
+ try {
1157
+ const response = await transport.refresh(current.session.refreshToken, current.session.baseUrl);
1158
+ const next = rotated(current.session, response, started);
1159
+ if (next.expiresAt <= this.now()) throw loginRequired();
1160
+ this.write(ref, { ...current, state: "ready", session: next });
1161
+ return authorization(next);
1162
+ } catch {
1163
+ if (!this.isLoggedOut(ref)) this.write(ref, { ...current, state: "reauth_required" });
1164
+ throw loginRequired();
1165
+ }
1166
+ });
1167
+ }
1168
+ async logout(ref, profile, transport) {
1169
+ const current = this.read(ref, profile);
1170
+ let remoteRevoked = false;
1171
+ try {
1172
+ if (current) {
1173
+ await transport.revoke(current.session.refreshToken, current.session.baseUrl);
1174
+ remoteRevoked = true;
1175
+ }
1176
+ } catch {
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;
1187
+ }
1188
+ path(ref, suffix = ".json") {
1189
+ if (!/^[a-f0-9]{32}$/.test(ref)) throw unsafe();
1190
+ return join3(this.directory, ref + suffix);
1191
+ }
1192
+ isLoggedOut(ref) {
1193
+ return existsSync2(this.path(ref, ".logged-out"));
1194
+ }
1195
+ prepareDirectory() {
1196
+ if (!existsSync2(this.directory)) {
1197
+ mkdirSync2(join3(this.directory, ".."), { recursive: true, mode: 448 });
1198
+ try {
1199
+ mkdirSync2(this.directory, { mode: 448 });
1200
+ if (process.platform === "win32") checkWindowsPrivateStorage(this.directory, true);
1201
+ } catch (error) {
1202
+ if (error.code !== "EEXIST") throw unsafe();
1203
+ }
1204
+ }
1205
+ requirePrivate(this.directory, true);
1206
+ }
1207
+ read(ref, profile) {
1208
+ requireProfile(profile);
1209
+ if (this.isLoggedOut(ref)) return void 0;
1210
+ const path = this.path(ref);
1211
+ if (!existsSync2(path)) return void 0;
1212
+ requirePrivate(this.directory, true);
1213
+ requirePrivate(path, false);
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;
1224
+ try {
1225
+ doc = JSON.parse(text2);
1226
+ } catch {
1227
+ throw unsafe();
1228
+ }
1229
+ if (!doc || doc.version !== 1 || !["ready", "refreshing", "reauth_required"].includes(doc.state)) throw unsafe();
1230
+ if (doc.profile !== profile) throw new OAuthCredentialError("invalid_profile");
1231
+ validateSession(doc.session);
1232
+ return this.isLoggedOut(ref) ? void 0 : doc;
1233
+ }
1234
+ write(ref, doc) {
1235
+ if (this.isLoggedOut(ref)) throw loginRequired();
1236
+ this.atomicWrite(this.path(ref), JSON.stringify(doc));
1237
+ if (this.isLoggedOut(ref)) {
1238
+ remove(this.path(ref));
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
+ }
1265
+ }
1266
+ async withLock(ref, task) {
1267
+ const path = this.path(ref, ".lock");
1268
+ const deadline = Date.now() + this.lockWaitMs;
1269
+ this.prepareDirectory();
1270
+ while (true) {
1271
+ let fd;
1272
+ try {
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
+ }
1294
+ }
1295
+ }
1296
+ };
1297
+ function requireProfile(profile) {
1298
+ if (typeof profile !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(profile)) throw new OAuthCredentialError("invalid_profile");
1299
+ }
1300
+ function requirePrivate(path, directory) {
1301
+ const s = lstatSync(path);
1302
+ if (s.isSymbolicLink() || (directory ? !s.isDirectory() : !s.isFile())) throw unsafe();
1303
+ if (process.platform === "win32") {
1304
+ try {
1305
+ checkWindowsPrivateStorage(path, false);
1306
+ } catch {
1307
+ throw unsafe();
1308
+ }
1309
+ return;
1310
+ }
1311
+ if ((s.mode & 63) !== 0 || process.getuid && s.uid !== process.getuid()) throw unsafe();
1312
+ }
1313
+ function remove(path) {
1314
+ try {
1315
+ unlinkSync(path);
1316
+ } catch (error) {
1317
+ if (error.code !== "ENOENT") throw unsafe();
1318
+ }
1319
+ }
1320
+ function validateSession(s) {
1321
+ 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
+ for (const value of [s.workspaceId, s.memberId, s.sessionId, s.accountDisplay]) {
1323
+ if (typeof value !== "string" || !value || value.length > 256 || /[\r\n\x00-\x1f]/.test(value)) throw unsafe();
1324
+ }
1325
+ for (const value of [s.accessToken, s.refreshToken]) {
1326
+ if (typeof value !== "string" || !value || value.length > 8192 || /\s/.test(value)) throw unsafe();
1327
+ }
1328
+ let url;
1329
+ try {
1330
+ url = new URL(s.baseUrl);
1331
+ } catch {
1332
+ throw unsafe();
1333
+ }
1334
+ const loopback = ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname);
1335
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.protocol === "https:" || loopback && url.protocol === "http:")) throw unsafe();
1336
+ }
1337
+ function rotated(current, response, issuedAt) {
1338
+ if (!response || response.token_type !== "Bearer" || response.session_id !== current.sessionId || response.scope !== current.scope || !Number.isSafeInteger(response.expires_in) || response.expires_in < 1 || response.expires_in > 3600 || response.refresh_token === current.refreshToken) throw loginRequired();
1339
+ const next = {
1340
+ ...current,
1341
+ accessToken: response.access_token,
1342
+ refreshToken: response.refresh_token,
1343
+ issuedAt,
1344
+ expiresAt: issuedAt + response.expires_in * 1e3
1345
+ };
1346
+ validateSession(next);
1347
+ return next;
1348
+ }
1349
+ function authorization(session) {
1350
+ return { scheme: "Bearer", value: session.accessToken, baseUrl: session.baseUrl, expiresAt: session.expiresAt };
1351
+ }
1352
+
1353
+ // ../shared/src/oauth-profile.ts
1354
+ import { readFileSync as readFileSync3 } from "fs";
1355
+ import { dirname as dirname2, join as join4, resolve } from "path";
1356
+ function oauthProfile(reference, configPath, profile, baseUrl) {
1357
+ if (reference === void 0) return void 0;
1358
+ 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");
1359
+ const path = resolve(configPath);
1360
+ return {
1361
+ reference,
1362
+ profile,
1363
+ configPath: path,
1364
+ directory: join4(dirname2(path), "oauth-credentials"),
1365
+ baseUrl: normalizeCoreOrigin(baseUrl)
1366
+ };
1367
+ }
1368
+ var OAuthProfileProvider = class {
1369
+ selected;
1370
+ constructor(selected) {
1371
+ this.selected = selected;
1372
+ }
1373
+ async resolve() {
1374
+ const selected = this.selected;
1375
+ let raw;
1376
+ try {
1377
+ raw = JSON.parse(readFileSync3(selected.configPath, "utf8"));
1378
+ } catch {
1379
+ throw new OAuthCredentialError("login_required");
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);
1384
+ if (!current || current.baseUrl !== selected.baseUrl) throw new OAuthCredentialError("login_required");
1385
+ const store = new OAuthSessionStore(current.directory);
1386
+ const status = store.status(current.reference, current.profile);
1387
+ if (!("baseUrl" in status) || typeof status.baseUrl !== "string" || normalizeCoreOrigin(status.baseUrl) !== current.baseUrl) throw new OAuthCredentialError("login_required");
1388
+ return store.resolve(current.reference, current.profile, new CoreDeviceClient(current.baseUrl));
1389
+ }
1390
+ };
1391
+
834
1392
  // src/distribution-capabilities.ts
835
1393
  var PUBLIC_DISTRIBUTION = {
836
1394
  id: "public",
@@ -838,7 +1396,8 @@ var PUBLIC_DISTRIBUTION = {
838
1396
  packageRegistry: null,
839
1397
  capabilities: {
840
1398
  interactiveLogin: false,
841
- managedCredentials: false
1399
+ managedCredentials: false,
1400
+ deviceFlow: true
842
1401
  }
843
1402
  };
844
1403
  var DISTRIBUTION_MANIFEST = typeof define_CTXDB_DISTRIBUTION_MANIFEST_default === "undefined" ? PUBLIC_DISTRIBUTION : define_CTXDB_DISTRIBUTION_MANIFEST_default;
@@ -852,7 +1411,7 @@ var DEFAULT_KNOWLEDGE_TOP_K = 6;
852
1411
  var DEFAULT_KB_CATALOG_INJECTION = "session_start";
853
1412
  function defaultPath(env) {
854
1413
  if (env.CTXDB_CONFIG_PATH) return env.CTXDB_CONFIG_PATH;
855
- return join2(homedir2(), ".ctxdb", "ctxdb.json");
1414
+ return join5(homedir2(), ".ctxdb", "ctxdb.json");
856
1415
  }
857
1416
  function coerceInt(v, fallback) {
858
1417
  if (v === null || v === void 0 || v === "") return fallback;
@@ -874,9 +1433,9 @@ function coerceKbCatalogInjection(v) {
874
1433
  return DEFAULT_KB_CATALOG_INJECTION;
875
1434
  }
876
1435
  function readRaw(path) {
877
- if (!existsSync2(path)) return {};
1436
+ if (!existsSync3(path)) return {};
878
1437
  try {
879
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
1438
+ const parsed = JSON.parse(readFileSync4(path, "utf-8"));
880
1439
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
881
1440
  return parsed;
882
1441
  }
@@ -892,6 +1451,9 @@ function agentRaw(raw) {
892
1451
  if (!section || typeof section !== "object" || Array.isArray(section)) return {};
893
1452
  return section;
894
1453
  }
1454
+ function accessTokenFromEnv(env = process.env) {
1455
+ return env.CTXDB_ACCESS_TOKEN?.trim() || null;
1456
+ }
895
1457
  function applyEnv(cfg, env) {
896
1458
  if (env.CTXDB_API_KEY) cfg.apiKey = env.CTXDB_API_KEY;
897
1459
  if (env.CTXDB_BASE_URL) cfg.baseUrl = env.CTXDB_BASE_URL.replace(/\/+$/, "");
@@ -909,11 +1471,11 @@ function applyDebugPolicy(cfg) {
909
1471
  return cfg;
910
1472
  }
911
1473
  function managedCredential(path) {
912
- if (!existsSync2(path)) return null;
1474
+ if (!existsSync3(path)) return null;
913
1475
  if (process.platform !== "win32" && (statSync2(path).mode & 63) !== 0) {
914
1476
  throw new Error(`credentials: ${path} must be owner-only (run chmod 600)`);
915
1477
  }
916
- const raw = JSON.parse(readFileSync2(path, "utf8"));
1478
+ const raw = JSON.parse(readFileSync4(path, "utf8"));
917
1479
  if (raw.version !== 1 || !raw.records || typeof raw.records !== "object") {
918
1480
  throw new Error("credentials: unsupported credentials.json schema");
919
1481
  }
@@ -968,16 +1530,21 @@ function loadOpencodeConfig(options = {}) {
968
1530
  kbCatalogInjection: coerceKbCatalogInjection(section.kb_catalog_injection)
969
1531
  };
970
1532
  const managed = DISTRIBUTION_MANIFEST.capabilities.managedCredentials && options.managedCredentials !== false ? managedCredential(
971
- options.credentialsPath ?? join2(dirname2(path), "credentials.json")
1533
+ options.credentialsPath ?? join5(dirname3(path), "credentials.json")
972
1534
  ) : null;
973
1535
  if (managed) {
974
1536
  cfg.apiKey = managed.apiKey;
975
1537
  cfg.baseUrl = managed.baseUrl;
976
1538
  }
977
- return applyEnv(cfg, env);
1539
+ applyEnv(cfg, env);
1540
+ if (DISTRIBUTION_MANIFEST.capabilities.deviceFlow) {
1541
+ cfg.oauthCredential = oauthProfile(section.oauth_credential_ref, path, "opencode", cfg.baseUrl);
1542
+ }
1543
+ if (env.CTXDB_API_KEY || accessTokenFromEnv(env)) cfg.oauthCredential = void 0;
1544
+ return cfg;
978
1545
  }
979
- function isConfigured(cfg) {
980
- return Boolean(cfg.apiKey && cfg.baseUrl);
1546
+ function isConfigured(cfg, env = process.env) {
1547
+ return Boolean((accessTokenFromEnv(env) || cfg.oauthCredential || cfg.apiKey) && cfg.baseUrl);
981
1548
  }
982
1549
 
983
1550
  // src/http-client.ts
@@ -995,20 +1562,29 @@ var CtxdbHttpError = class extends Error {
995
1562
  var HttpClient = class {
996
1563
  baseUrl;
997
1564
  apiKey;
1565
+ accessToken;
998
1566
  userAgent;
999
1567
  fetchImpl;
1568
+ oauthProvider;
1000
1569
  constructor(opts) {
1001
1570
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
1002
1571
  this.apiKey = opts.apiKey;
1572
+ this.accessToken = opts.accessToken ?? null;
1573
+ this.oauthProvider = opts.oauthCredential ? new OAuthProfileProvider(opts.oauthCredential) : void 0;
1003
1574
  this.userAgent = opts.userAgent ?? "ctxdb-opencode-plugin/0.0.0";
1004
1575
  this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
1005
1576
  }
1006
- headers(contentType) {
1577
+ async headers(contentType) {
1007
1578
  const h = {
1008
1579
  "User-Agent": this.userAgent,
1009
1580
  Connection: "close"
1010
1581
  };
1011
- if (this.apiKey) h.Authorization = `Token ${this.apiKey}`;
1582
+ if (this.accessToken) h.Authorization = `Bearer ${this.accessToken}`;
1583
+ else if (this.oauthProvider) {
1584
+ const authorization2 = await this.oauthProvider.resolve();
1585
+ if (authorization2.baseUrl !== this.baseUrl) throw new Error("OAuth environment changed; reload this runtime.");
1586
+ h.Authorization = `Bearer ${authorization2.value}`;
1587
+ } else if (this.apiKey) h.Authorization = `Token ${this.apiKey}`;
1012
1588
  if (contentType) h["Content-Type"] = contentType;
1013
1589
  return h;
1014
1590
  }
@@ -1029,6 +1605,9 @@ var HttpClient = class {
1029
1605
  return this.request("POST", url, path, JSON.stringify(body), "application/json", options.timeoutMs);
1030
1606
  }
1031
1607
  async request(method, url, path, body, contentType, timeoutMs) {
1608
+ const headers = await this.headers(contentType);
1609
+ const bearer = headers.Authorization?.startsWith("Bearer ") ? headers.Authorization.slice(7) : void 0;
1610
+ const safeText = (value) => bearer ? String(value).split(bearer).join("[REDACTED]") : String(value);
1032
1611
  const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
1033
1612
  const controller = new AbortController();
1034
1613
  const timer = setTimeout(() => controller.abort(), effectiveTimeout);
@@ -1037,41 +1616,42 @@ var HttpClient = class {
1037
1616
  try {
1038
1617
  resp = await this.fetchImpl(url, {
1039
1618
  method,
1040
- headers: this.headers(contentType),
1619
+ headers,
1041
1620
  body,
1042
- signal: controller.signal
1621
+ signal: controller.signal,
1622
+ ...headers.Authorization?.startsWith("Bearer ") ? { redirect: "error" } : {}
1043
1623
  });
1044
1624
  } catch (err) {
1045
1625
  if (err?.name === "AbortError") {
1046
1626
  throw new CtxdbHttpError(path, null, `timeout after ${effectiveTimeout}ms`);
1047
1627
  }
1048
- throw new CtxdbHttpError(path, null, `network error: ${err?.message ?? err}`);
1628
+ throw new CtxdbHttpError(path, null, `network error: ${safeText(err?.message ?? err)}`);
1049
1629
  }
1050
1630
  if (resp.status === 204) return {};
1051
- let text;
1631
+ let text2;
1052
1632
  try {
1053
- text = await resp.text();
1633
+ text2 = safeText(await resp.text());
1054
1634
  } catch (err) {
1055
- throw new CtxdbHttpError(path, resp.status, `body read failed: ${err?.message ?? err}`);
1635
+ throw new CtxdbHttpError(path, resp.status, `body read failed: ${safeText(err?.message ?? err)}`);
1056
1636
  }
1057
1637
  if (!resp.ok) {
1058
- throw new CtxdbHttpError(path, resp.status, extractDetail(text) || `HTTP ${resp.status}`);
1638
+ throw new CtxdbHttpError(path, resp.status, extractDetail(text2) || `HTTP ${resp.status}`);
1059
1639
  }
1060
- if (!text) return {};
1640
+ if (!text2) return {};
1061
1641
  try {
1062
- return JSON.parse(text);
1642
+ return JSON.parse(text2);
1063
1643
  } catch {
1064
- return text;
1644
+ return text2;
1065
1645
  }
1066
1646
  } finally {
1067
1647
  clearTimeout(timer);
1068
1648
  }
1069
1649
  }
1070
1650
  };
1071
- function extractDetail(text) {
1072
- if (!text) return "";
1651
+ function extractDetail(text2) {
1652
+ if (!text2) return "";
1073
1653
  try {
1074
- const parsed = JSON.parse(text);
1654
+ const parsed = JSON.parse(text2);
1075
1655
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1076
1656
  for (const k of ["detail", "message", "error"]) {
1077
1657
  const v = parsed[k];
@@ -1081,7 +1661,7 @@ function extractDetail(text) {
1081
1661
  }
1082
1662
  return String(parsed);
1083
1663
  } catch {
1084
- return text;
1664
+ return text2;
1085
1665
  }
1086
1666
  }
1087
1667
 
@@ -1125,7 +1705,11 @@ async function searchAndFormatRecall(prompt, cfg, client, timeoutMs, traceOption
1125
1705
  },
1126
1706
  store: {
1127
1707
  ...traceOptions.traceStore,
1128
- secrets: [...traceOptions.traceStore?.secrets ?? [], cfg.apiKey]
1708
+ secrets: [
1709
+ ...traceOptions.traceStore?.secrets ?? [],
1710
+ cfg.apiKey,
1711
+ client.accessToken
1712
+ ]
1129
1713
  }
1130
1714
  } : null;
1131
1715
  if (trace) {
@@ -1454,10 +2038,15 @@ var SESSION_TTL_MS = 15 * 60 * 1e3;
1454
2038
  var SESSION_MAX = 100;
1455
2039
  var RECALL_TIMEOUT_MS = 5e3;
1456
2040
  var CAPTURE_TIMEOUT_MS = 8e3;
1457
- function buildRuntime(config, cwd) {
2041
+ function buildRuntime(config, cwd, env = process.env) {
1458
2042
  return {
1459
2043
  config,
1460
- http: new HttpClient({ baseUrl: config.baseUrl, apiKey: config.apiKey }),
2044
+ http: new HttpClient({
2045
+ baseUrl: config.baseUrl,
2046
+ apiKey: config.apiKey,
2047
+ oauthCredential: config.oauthCredential,
2048
+ accessToken: accessTokenFromEnv(env)
2049
+ }),
1461
2050
  sessionState: /* @__PURE__ */ new Map(),
1462
2051
  cwd
1463
2052
  };
@@ -1500,7 +2089,7 @@ async function buildHooks(input) {
1500
2089
  logDebug(
1501
2090
  config,
1502
2091
  "config",
1503
- "opencode integration disabled: missing agents.opencode.api_key in ~/.ctxdb/ctxdb.json or CTXDB_API_KEY"
2092
+ "opencode integration disabled: missing CTXDB_ACCESS_TOKEN or API key in agents.opencode.api_key / CTXDB_API_KEY"
1504
2093
  );
1505
2094
  return {};
1506
2095
  }
@@ -1509,10 +2098,10 @@ async function buildHooks(input) {
1509
2098
  const hooks = {
1510
2099
  "chat.message": async (input2, output) => {
1511
2100
  try {
1512
- const text = extractTextPrompt(output.parts);
1513
- if (!text) return;
2101
+ const text2 = extractTextPrompt(output.parts);
2102
+ if (!text2) return;
1514
2103
  const s = touchSession(rt, input2.sessionID);
1515
- s.lastPrompt = text;
2104
+ s.lastPrompt = text2;
1516
2105
  } catch (err) {
1517
2106
  logError(rt.config, "chat.message", err);
1518
2107
  }