@aliyunrds/ctxdb 1.0.8-beta.3 → 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
  }
@@ -912,11 +1471,11 @@ function applyDebugPolicy(cfg) {
912
1471
  return cfg;
913
1472
  }
914
1473
  function managedCredential(path) {
915
- if (!existsSync2(path)) return null;
1474
+ if (!existsSync3(path)) return null;
916
1475
  if (process.platform !== "win32" && (statSync2(path).mode & 63) !== 0) {
917
1476
  throw new Error(`credentials: ${path} must be owner-only (run chmod 600)`);
918
1477
  }
919
- const raw = JSON.parse(readFileSync2(path, "utf8"));
1478
+ const raw = JSON.parse(readFileSync4(path, "utf8"));
920
1479
  if (raw.version !== 1 || !raw.records || typeof raw.records !== "object") {
921
1480
  throw new Error("credentials: unsupported credentials.json schema");
922
1481
  }
@@ -971,16 +1530,21 @@ function loadOpencodeConfig(options = {}) {
971
1530
  kbCatalogInjection: coerceKbCatalogInjection(section.kb_catalog_injection)
972
1531
  };
973
1532
  const managed = DISTRIBUTION_MANIFEST.capabilities.managedCredentials && options.managedCredentials !== false ? managedCredential(
974
- options.credentialsPath ?? join2(dirname2(path), "credentials.json")
1533
+ options.credentialsPath ?? join5(dirname3(path), "credentials.json")
975
1534
  ) : null;
976
1535
  if (managed) {
977
1536
  cfg.apiKey = managed.apiKey;
978
1537
  cfg.baseUrl = managed.baseUrl;
979
1538
  }
980
- 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;
981
1545
  }
982
1546
  function isConfigured(cfg, env = process.env) {
983
- return Boolean((accessTokenFromEnv(env) || cfg.apiKey) && cfg.baseUrl);
1547
+ return Boolean((accessTokenFromEnv(env) || cfg.oauthCredential || cfg.apiKey) && cfg.baseUrl);
984
1548
  }
985
1549
 
986
1550
  // src/http-client.ts
@@ -1001,20 +1565,26 @@ var HttpClient = class {
1001
1565
  accessToken;
1002
1566
  userAgent;
1003
1567
  fetchImpl;
1568
+ oauthProvider;
1004
1569
  constructor(opts) {
1005
1570
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
1006
1571
  this.apiKey = opts.apiKey;
1007
1572
  this.accessToken = opts.accessToken ?? null;
1573
+ this.oauthProvider = opts.oauthCredential ? new OAuthProfileProvider(opts.oauthCredential) : void 0;
1008
1574
  this.userAgent = opts.userAgent ?? "ctxdb-opencode-plugin/0.0.0";
1009
1575
  this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
1010
1576
  }
1011
- headers(contentType) {
1577
+ async headers(contentType) {
1012
1578
  const h = {
1013
1579
  "User-Agent": this.userAgent,
1014
1580
  Connection: "close"
1015
1581
  };
1016
1582
  if (this.accessToken) h.Authorization = `Bearer ${this.accessToken}`;
1017
- else if (this.apiKey) h.Authorization = `Token ${this.apiKey}`;
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}`;
1018
1588
  if (contentType) h["Content-Type"] = contentType;
1019
1589
  return h;
1020
1590
  }
@@ -1035,6 +1605,9 @@ var HttpClient = class {
1035
1605
  return this.request("POST", url, path, JSON.stringify(body), "application/json", options.timeoutMs);
1036
1606
  }
1037
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);
1038
1611
  const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
1039
1612
  const controller = new AbortController();
1040
1613
  const timer = setTimeout(() => controller.abort(), effectiveTimeout);
@@ -1043,41 +1616,42 @@ var HttpClient = class {
1043
1616
  try {
1044
1617
  resp = await this.fetchImpl(url, {
1045
1618
  method,
1046
- headers: this.headers(contentType),
1619
+ headers,
1047
1620
  body,
1048
- signal: controller.signal
1621
+ signal: controller.signal,
1622
+ ...headers.Authorization?.startsWith("Bearer ") ? { redirect: "error" } : {}
1049
1623
  });
1050
1624
  } catch (err) {
1051
1625
  if (err?.name === "AbortError") {
1052
1626
  throw new CtxdbHttpError(path, null, `timeout after ${effectiveTimeout}ms`);
1053
1627
  }
1054
- throw new CtxdbHttpError(path, null, `network error: ${err?.message ?? err}`);
1628
+ throw new CtxdbHttpError(path, null, `network error: ${safeText(err?.message ?? err)}`);
1055
1629
  }
1056
1630
  if (resp.status === 204) return {};
1057
- let text;
1631
+ let text2;
1058
1632
  try {
1059
- text = await resp.text();
1633
+ text2 = safeText(await resp.text());
1060
1634
  } catch (err) {
1061
- 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)}`);
1062
1636
  }
1063
1637
  if (!resp.ok) {
1064
- throw new CtxdbHttpError(path, resp.status, extractDetail(text) || `HTTP ${resp.status}`);
1638
+ throw new CtxdbHttpError(path, resp.status, extractDetail(text2) || `HTTP ${resp.status}`);
1065
1639
  }
1066
- if (!text) return {};
1640
+ if (!text2) return {};
1067
1641
  try {
1068
- return JSON.parse(text);
1642
+ return JSON.parse(text2);
1069
1643
  } catch {
1070
- return text;
1644
+ return text2;
1071
1645
  }
1072
1646
  } finally {
1073
1647
  clearTimeout(timer);
1074
1648
  }
1075
1649
  }
1076
1650
  };
1077
- function extractDetail(text) {
1078
- if (!text) return "";
1651
+ function extractDetail(text2) {
1652
+ if (!text2) return "";
1079
1653
  try {
1080
- const parsed = JSON.parse(text);
1654
+ const parsed = JSON.parse(text2);
1081
1655
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1082
1656
  for (const k of ["detail", "message", "error"]) {
1083
1657
  const v = parsed[k];
@@ -1087,7 +1661,7 @@ function extractDetail(text) {
1087
1661
  }
1088
1662
  return String(parsed);
1089
1663
  } catch {
1090
- return text;
1664
+ return text2;
1091
1665
  }
1092
1666
  }
1093
1667
 
@@ -1470,6 +2044,7 @@ function buildRuntime(config, cwd, env = process.env) {
1470
2044
  http: new HttpClient({
1471
2045
  baseUrl: config.baseUrl,
1472
2046
  apiKey: config.apiKey,
2047
+ oauthCredential: config.oauthCredential,
1473
2048
  accessToken: accessTokenFromEnv(env)
1474
2049
  }),
1475
2050
  sessionState: /* @__PURE__ */ new Map(),
@@ -1523,10 +2098,10 @@ async function buildHooks(input) {
1523
2098
  const hooks = {
1524
2099
  "chat.message": async (input2, output) => {
1525
2100
  try {
1526
- const text = extractTextPrompt(output.parts);
1527
- if (!text) return;
2101
+ const text2 = extractTextPrompt(output.parts);
2102
+ if (!text2) return;
1528
2103
  const s = touchSession(rt, input2.sessionID);
1529
- s.lastPrompt = text;
2104
+ s.lastPrompt = text2;
1530
2105
  } catch (err) {
1531
2106
  logError(rt.config, "chat.message", err);
1532
2107
  }