@aliyunrds/ctxdb 1.0.8-beta.3 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -15
- package/dist/{chunk-BUK4SZC2.js → chunk-3FAQREIU.js} +308 -220
- package/dist/{chunk-ULAWTBBC.js → chunk-3MEBQDGU.js} +6 -1
- package/dist/{chunk-R67JELM7.js → chunk-5ITT5GSP.js} +3 -2
- package/dist/{chunk-7OX6UVPB.js → chunk-AAWG5SPO.js} +3 -3
- package/dist/{chunk-AUBVVYQL.js → chunk-AH2QEUK4.js} +1 -1
- package/dist/{chunk-3NJ37TEY.js → chunk-LZD55CWE.js} +286 -11
- package/dist/{chunk-EI63DQX3.js → chunk-MWAFAK5M.js} +1 -1
- package/dist/{chunk-VIG4SYLU.js → chunk-PI2SUS3M.js} +1 -1
- package/dist/{chunk-LZ2LOWZL.js → chunk-RRDQCZJ4.js} +2 -2
- package/dist/{chunk-ZFS6OMWE.js → chunk-YTCXHO4F.js} +2 -2
- package/dist/cli/main.js +4099 -1664
- package/dist/hooks/hermes-post-llm-call.js +5 -5
- package/dist/hooks/hermes-pre-llm-call.js +8 -8
- package/dist/hooks/pre-tool-use.js +1 -1
- package/dist/hooks/session-start.js +7 -7
- package/dist/hooks/stop.js +5 -5
- package/dist/hooks/user-prompt-submit.js +7 -7
- package/dist/opencode/index.js +1136 -81
- package/dist/setup/skills/contextdb-knowledge/SKILL.md +14 -3
- package/dist/setup/skills/contextdb-memory/SKILL.md +10 -3
- package/dist/workers/version-check.js +3 -3
- package/package.json +2 -2
package/dist/opencode/index.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
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 } };
|
|
3
|
-
|
|
4
|
-
// src/config.ts
|
|
5
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
6
|
-
import { homedir as homedir2 } from "os";
|
|
7
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
2
|
+
var define_CTXDB_DISTRIBUTION_MANIFEST_default = { id: "public", packageName: "@aliyunrds/ctxdb", packageRegistry: null, capabilities: { interactiveLogin: false, managedCredentials: false, deviceFlow: true } };
|
|
8
3
|
|
|
9
4
|
// ../shared/src/graph-context.ts
|
|
10
5
|
var GRAPH_CONTEXT_TAG = "graphrag";
|
|
@@ -435,8 +430,8 @@ function getMemoryImportance(memory) {
|
|
|
435
430
|
};
|
|
436
431
|
return defaults[cat] ?? 0.5;
|
|
437
432
|
}
|
|
438
|
-
function estimateTokens(
|
|
439
|
-
return Math.ceil(
|
|
433
|
+
function estimateTokens(text2) {
|
|
434
|
+
return Math.ceil(text2.length / CHARS_PER_TOKEN);
|
|
440
435
|
}
|
|
441
436
|
function rankMemories(memories, categoryOrder) {
|
|
442
437
|
const orderMap = new Map(categoryOrder.map((cat, i) => [cat, i]));
|
|
@@ -589,6 +584,7 @@ function selectAndFormatRecalledMemories(memories, userId, config = {}) {
|
|
|
589
584
|
|
|
590
585
|
// ../shared/src/debug-policy.ts
|
|
591
586
|
var OFFICIAL_PRODUCTION_BASE_URLS = [
|
|
587
|
+
"https://api.cn-hangzhou.agentcontext.aliyuncs.com",
|
|
592
588
|
"https://context-database.aliyuncs.com"
|
|
593
589
|
];
|
|
594
590
|
function normalizeBaseUrl(value) {
|
|
@@ -690,9 +686,9 @@ function sanitizeRecallFailureReason(reason, secrets = []) {
|
|
|
690
686
|
}
|
|
691
687
|
function sanitizeTraceString(value, secrets) {
|
|
692
688
|
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
|
|
694
|
-
if (!
|
|
695
|
-
sanitized = sanitized.split(
|
|
689
|
+
for (const secret2 of secrets) {
|
|
690
|
+
if (!secret2) continue;
|
|
691
|
+
sanitized = sanitized.split(secret2).join("[REDACTED]");
|
|
696
692
|
}
|
|
697
693
|
return sanitized;
|
|
698
694
|
}
|
|
@@ -827,10 +823,1049 @@ function isRecallTraceRecord(value) {
|
|
|
827
823
|
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
824
|
if (!baseValid) return false;
|
|
829
825
|
if (record.type === "recall.start") return true;
|
|
826
|
+
if (record.type !== "recall.finish") return false;
|
|
830
827
|
const selection = record.selection;
|
|
831
828
|
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
829
|
}
|
|
833
830
|
|
|
831
|
+
// ../shared/src/device-flow.ts
|
|
832
|
+
import { setTimeout as delay } from "timers/promises";
|
|
833
|
+
var DeviceFlowError = class extends Error {
|
|
834
|
+
code;
|
|
835
|
+
constructor(code) {
|
|
836
|
+
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.`);
|
|
837
|
+
this.name = "DeviceFlowError";
|
|
838
|
+
this.code = code;
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
var invalid = () => new DeviceFlowError("invalid_response");
|
|
842
|
+
var scopes = ["read_only", "read_write", "default"];
|
|
843
|
+
var clientId = "ctxdb-cli";
|
|
844
|
+
var grantType = "urn:ietf:params:oauth:grant-type:device_code";
|
|
845
|
+
function normalizeCoreOrigin(value) {
|
|
846
|
+
let url;
|
|
847
|
+
try {
|
|
848
|
+
url = new URL(value);
|
|
849
|
+
} catch {
|
|
850
|
+
throw new Error("Invalid Core server origin");
|
|
851
|
+
}
|
|
852
|
+
const local = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
853
|
+
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");
|
|
854
|
+
return url.origin;
|
|
855
|
+
}
|
|
856
|
+
var CoreDeviceClient = class {
|
|
857
|
+
baseUrl;
|
|
858
|
+
fetchImpl;
|
|
859
|
+
now;
|
|
860
|
+
requestTimeoutMs;
|
|
861
|
+
sleep;
|
|
862
|
+
constructor(baseUrl, options = {}) {
|
|
863
|
+
this.baseUrl = normalizeCoreOrigin(baseUrl);
|
|
864
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
865
|
+
this.now = options.now ?? Date.now;
|
|
866
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
867
|
+
this.sleep = options.sleep ?? ((ms, signal) => delay(ms, void 0, { signal }));
|
|
868
|
+
}
|
|
869
|
+
async login(options) {
|
|
870
|
+
const started = this.now();
|
|
871
|
+
const initial = await this.post("/v1/auth/device/authorize", {
|
|
872
|
+
client_id: clientId,
|
|
873
|
+
...options.initialAgent === void 0 ? {} : { initial_agent: options.initialAgent }
|
|
874
|
+
}, options.signal);
|
|
875
|
+
if (!initial.ok) throw new DeviceFlowError("request_rejected");
|
|
876
|
+
const data = initial.data;
|
|
877
|
+
const requestId = text(data.request_id, 32), userCode = text(data.user_code, 9), code = text(data.device_code, 43);
|
|
878
|
+
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();
|
|
879
|
+
const expiresIn = integer(data.expires_in, 1, 3600);
|
|
880
|
+
let interval = integer(data.interval, 1, 3600);
|
|
881
|
+
const deadline = started + expiresIn * 1e3;
|
|
882
|
+
options.onPrompt({ requestId, userCode, expiresIn, interval });
|
|
883
|
+
while (true) {
|
|
884
|
+
if (options.signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
885
|
+
if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
|
|
886
|
+
try {
|
|
887
|
+
await this.sleep(Math.min(interval * 1e3, deadline - this.now()), options.signal);
|
|
888
|
+
} catch {
|
|
889
|
+
throw new DeviceFlowError("cancelled");
|
|
890
|
+
}
|
|
891
|
+
if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
|
|
892
|
+
const issuedAt = this.now();
|
|
893
|
+
const result = await this.post("/v1/auth/device/token", { grant_type: grantType, client_id: clientId, device_code: code }, options.signal);
|
|
894
|
+
if (result.ok) return loginResult(result.data, this.baseUrl, issuedAt, this.now());
|
|
895
|
+
const error = result.data.error;
|
|
896
|
+
if (error === "authorization_pending") continue;
|
|
897
|
+
if (error === "slow_down") {
|
|
898
|
+
interval = Math.max(interval + 5, integer(result.data.interval, 1, 3605));
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (error === "access_denied" || error === "expired_token" || error === "invalid_grant") throw new DeviceFlowError(error);
|
|
902
|
+
throw new DeviceFlowError("request_rejected");
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async refresh(refreshToken, baseUrl) {
|
|
906
|
+
this.requireSameOrigin(baseUrl);
|
|
907
|
+
const result = await this.post("/v1/auth/token/refresh", { refresh_token: secret(refreshToken) });
|
|
908
|
+
if (!result.ok) throw new DeviceFlowError("request_rejected");
|
|
909
|
+
return credentialResult(result.data);
|
|
910
|
+
}
|
|
911
|
+
async revoke(refreshToken, baseUrl) {
|
|
912
|
+
this.requireSameOrigin(baseUrl);
|
|
913
|
+
const result = await this.post("/v1/auth/token/revoke", { refresh_token: secret(refreshToken) });
|
|
914
|
+
if (!result.ok) throw new DeviceFlowError("request_rejected");
|
|
915
|
+
}
|
|
916
|
+
requireSameOrigin(baseUrl) {
|
|
917
|
+
if (normalizeCoreOrigin(baseUrl) !== this.baseUrl) throw new Error("OAuth credential Core origin cannot change");
|
|
918
|
+
}
|
|
919
|
+
async post(path, body, signal) {
|
|
920
|
+
if (signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
921
|
+
const controller = new AbortController();
|
|
922
|
+
const abort = () => controller.abort();
|
|
923
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
924
|
+
const timer = setTimeout(abort, this.requestTimeoutMs);
|
|
925
|
+
timer.unref?.();
|
|
926
|
+
try {
|
|
927
|
+
const response = await this.fetchImpl(this.baseUrl + path, {
|
|
928
|
+
method: "POST",
|
|
929
|
+
body: JSON.stringify(body),
|
|
930
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
931
|
+
redirect: "error",
|
|
932
|
+
credentials: "omit",
|
|
933
|
+
cache: "no-store",
|
|
934
|
+
signal: controller.signal
|
|
935
|
+
});
|
|
936
|
+
const box = await boundedJson(response);
|
|
937
|
+
if (!Number.isInteger(box.errorCode)) throw invalid();
|
|
938
|
+
const ok = response.ok && box.errorCode === 0;
|
|
939
|
+
const data = (!ok || path === "/v1/auth/token/revoke") && box.data == null ? {} : object(box.data);
|
|
940
|
+
if (!ok && response.status >= 500) throw new DeviceFlowError("service_unavailable");
|
|
941
|
+
return { ok, data };
|
|
942
|
+
} catch (error) {
|
|
943
|
+
if (signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
944
|
+
if (error instanceof DeviceFlowError) throw error;
|
|
945
|
+
throw new DeviceFlowError("network_error");
|
|
946
|
+
} finally {
|
|
947
|
+
clearTimeout(timer);
|
|
948
|
+
signal?.removeEventListener("abort", abort);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
function text(value, maximum) {
|
|
953
|
+
if (typeof value !== "string" || !value || value.length > maximum || /[\x00-\x1f\x7f]/.test(value)) throw invalid();
|
|
954
|
+
return value;
|
|
955
|
+
}
|
|
956
|
+
function integer(value, min, max) {
|
|
957
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw invalid();
|
|
958
|
+
return value;
|
|
959
|
+
}
|
|
960
|
+
function secret(value) {
|
|
961
|
+
const token = text(value, 8192);
|
|
962
|
+
if (/\s/.test(token)) throw invalid();
|
|
963
|
+
return token;
|
|
964
|
+
}
|
|
965
|
+
function object(value) {
|
|
966
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid();
|
|
967
|
+
return value;
|
|
968
|
+
}
|
|
969
|
+
function credentialResult(data) {
|
|
970
|
+
if (data.token_type !== "Bearer" || !scopes.includes(data.scope)) throw invalid();
|
|
971
|
+
return {
|
|
972
|
+
access_token: secret(data.access_token),
|
|
973
|
+
refresh_token: secret(data.refresh_token),
|
|
974
|
+
token_type: "Bearer",
|
|
975
|
+
expires_in: integer(data.expires_in, 1, 3600),
|
|
976
|
+
session_id: text(data.session_id, 64),
|
|
977
|
+
scope: data.scope
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function loginResult(data, baseUrl, issuedAt, now) {
|
|
981
|
+
const tokens = credentialResult(data), scope = tokens.scope;
|
|
982
|
+
if (data.client_id !== clientId || scope !== "default") throw invalid();
|
|
983
|
+
const expiresAt = issuedAt + tokens.expires_in * 1e3;
|
|
984
|
+
if (expiresAt <= now) throw invalid();
|
|
985
|
+
return {
|
|
986
|
+
baseUrl,
|
|
987
|
+
workspaceId: text(data.workspace_id, 64),
|
|
988
|
+
memberId: text(data.member_id, 64),
|
|
989
|
+
scope,
|
|
990
|
+
clientId,
|
|
991
|
+
sessionId: tokens.session_id,
|
|
992
|
+
accountDisplay: text(data.account_display, 256),
|
|
993
|
+
issuedAt,
|
|
994
|
+
expiresAt,
|
|
995
|
+
accessToken: tokens.access_token,
|
|
996
|
+
refreshToken: tokens.refresh_token
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
async function boundedJson(response) {
|
|
1000
|
+
if (!response.body || Number(response.headers.get("content-length")) > 65536) throw invalid();
|
|
1001
|
+
const reader = response.body.getReader();
|
|
1002
|
+
const chunks = [];
|
|
1003
|
+
let size = 0;
|
|
1004
|
+
try {
|
|
1005
|
+
while (true) {
|
|
1006
|
+
const { done, value } = await reader.read();
|
|
1007
|
+
if (done) break;
|
|
1008
|
+
size += value.byteLength;
|
|
1009
|
+
if (size > 65536) {
|
|
1010
|
+
await reader.cancel();
|
|
1011
|
+
throw invalid();
|
|
1012
|
+
}
|
|
1013
|
+
chunks.push(value);
|
|
1014
|
+
}
|
|
1015
|
+
try {
|
|
1016
|
+
return object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
1017
|
+
} catch {
|
|
1018
|
+
throw invalid();
|
|
1019
|
+
}
|
|
1020
|
+
} finally {
|
|
1021
|
+
reader.releaseLock();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// ../shared/src/oauth-credentials.ts
|
|
1026
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1027
|
+
|
|
1028
|
+
// ../shared/src/credentials/local-credential-provider.ts
|
|
1029
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
1030
|
+
import { homedir as homedir2 } from "os";
|
|
1031
|
+
import { join as join3 } from "path";
|
|
1032
|
+
|
|
1033
|
+
// ../shared/src/credentials/storage.ts
|
|
1034
|
+
import { constants, closeSync as closeSync2, existsSync as existsSync2, fstatSync, fsyncSync, lstatSync, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
|
|
1035
|
+
import { randomBytes, createHash } from "crypto";
|
|
1036
|
+
import { dirname as dirname2 } from "path";
|
|
1037
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
1038
|
+
|
|
1039
|
+
// ../shared/src/windows-private-storage.ts
|
|
1040
|
+
import { execFileSync } from "child_process";
|
|
1041
|
+
import { join as join2 } from "path";
|
|
1042
|
+
var SCRIPT = `
|
|
1043
|
+
$ErrorActionPreference = 'Stop'
|
|
1044
|
+
$path = $env:CTXDB_PRIVATE_STORAGE_PATH
|
|
1045
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
1046
|
+
$item = Get-Item -LiteralPath $path -Force
|
|
1047
|
+
if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'reparse' }
|
|
1048
|
+
if ($env:CTXDB_PRIVATE_STORAGE_INITIALIZE -eq '1') {
|
|
1049
|
+
if (-not $item.PSIsContainer) { throw 'not directory' }
|
|
1050
|
+
$acl = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
1051
|
+
$acl.SetOwner($sid)
|
|
1052
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
1053
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
|
1054
|
+
$acl.AddAccessRule($rule)
|
|
1055
|
+
Set-Acl -LiteralPath $path -AclObject $acl
|
|
1056
|
+
}
|
|
1057
|
+
$acl = Get-Acl -LiteralPath $path
|
|
1058
|
+
if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'owner' }
|
|
1059
|
+
$self = $false
|
|
1060
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
|
1061
|
+
if ($rule.AccessControlType -eq 'Allow') {
|
|
1062
|
+
if ($rule.IdentityReference.Value -ne $sid.Value) { throw 'other principal' }
|
|
1063
|
+
if (($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl) { $self = $true }
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (-not $self) { throw 'missing owner access' }
|
|
1067
|
+
Write-Output 'private'
|
|
1068
|
+
`;
|
|
1069
|
+
function checkWindowsPrivateStorage(path, initialize, run = execFileSync) {
|
|
1070
|
+
const systemRoot = process.env.SystemRoot;
|
|
1071
|
+
if (!systemRoot) throw new Error("Private Windows credential storage could not be verified.");
|
|
1072
|
+
try {
|
|
1073
|
+
const result = run(
|
|
1074
|
+
join2(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
1075
|
+
["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(SCRIPT, "utf16le").toString("base64")],
|
|
1076
|
+
{
|
|
1077
|
+
env: { ...process.env, CTXDB_PRIVATE_STORAGE_PATH: path, CTXDB_PRIVATE_STORAGE_INITIALIZE: initialize ? "1" : "0" },
|
|
1078
|
+
encoding: "utf8",
|
|
1079
|
+
windowsHide: true,
|
|
1080
|
+
timeout: 1e4,
|
|
1081
|
+
maxBuffer: 4096,
|
|
1082
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
if (String(result).trim() !== "private") throw new Error();
|
|
1086
|
+
} catch {
|
|
1087
|
+
throw new Error("Private Windows credential storage could not be verified.");
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// ../shared/src/credentials/storage.ts
|
|
1092
|
+
function privatePath(path, directory = false) {
|
|
1093
|
+
const s = lstatSync(path);
|
|
1094
|
+
if (s.isSymbolicLink() || (directory ? !s.isDirectory() : !s.isFile())) throw new Error("credentials: storage is not private or is invalid");
|
|
1095
|
+
if (process.platform === "win32") {
|
|
1096
|
+
checkWindowsPrivateStorage(path, false);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (s.mode & 63 || process.getuid && s.uid !== process.getuid()) throw new Error("credentials: storage must be private and owner-only (run chmod 600 on credentials.json)");
|
|
1100
|
+
}
|
|
1101
|
+
function prepareStorage(path) {
|
|
1102
|
+
const dir = dirname2(path);
|
|
1103
|
+
if (!existsSync2(dir)) {
|
|
1104
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
1105
|
+
if (process.platform === "win32") checkWindowsPrivateStorage(dir, true);
|
|
1106
|
+
}
|
|
1107
|
+
privatePath(dir, true);
|
|
1108
|
+
}
|
|
1109
|
+
function readPrivate(path) {
|
|
1110
|
+
if (!existsSync2(path)) return void 0;
|
|
1111
|
+
privatePath(dirname2(path), true);
|
|
1112
|
+
privatePath(path);
|
|
1113
|
+
const fd = openSync2(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1114
|
+
try {
|
|
1115
|
+
const stat = fstatSync(fd);
|
|
1116
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new Error("credentials: invalid storage");
|
|
1117
|
+
return readFileSync2(fd, "utf8");
|
|
1118
|
+
} finally {
|
|
1119
|
+
closeSync2(fd);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function atomicCredentialWrite(path, value) {
|
|
1123
|
+
prepareStorage(path);
|
|
1124
|
+
if (existsSync2(path)) privatePath(path);
|
|
1125
|
+
const temp = path + "." + randomBytes(12).toString("hex") + ".tmp";
|
|
1126
|
+
let fd;
|
|
1127
|
+
try {
|
|
1128
|
+
fd = openSync2(temp, "wx", 384);
|
|
1129
|
+
writeFileSync(fd, JSON.stringify(value, null, 2) + "\n");
|
|
1130
|
+
fsyncSync(fd);
|
|
1131
|
+
closeSync2(fd);
|
|
1132
|
+
fd = void 0;
|
|
1133
|
+
renameSync2(temp, path);
|
|
1134
|
+
if (process.platform !== "win32") {
|
|
1135
|
+
try {
|
|
1136
|
+
const dir = openSync2(dirname2(path), "r");
|
|
1137
|
+
try {
|
|
1138
|
+
fsyncSync(dir);
|
|
1139
|
+
} finally {
|
|
1140
|
+
closeSync2(dir);
|
|
1141
|
+
}
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
} finally {
|
|
1146
|
+
if (fd !== void 0) closeSync2(fd);
|
|
1147
|
+
if (existsSync2(temp)) unlinkSync(temp);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function withCredentialWriteLock(path, operation) {
|
|
1151
|
+
prepareStorage(path);
|
|
1152
|
+
const lock = path + ".lock", deadline = Date.now() + 1e4;
|
|
1153
|
+
let fd;
|
|
1154
|
+
while (true) {
|
|
1155
|
+
try {
|
|
1156
|
+
fd = openSync2(lock, "wx", 384);
|
|
1157
|
+
break;
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
if (error.code !== "EEXIST") throw error;
|
|
1160
|
+
if (Date.now() >= deadline) throw new Error("credentials: writer lock is busy; check the previous CLI process");
|
|
1161
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
try {
|
|
1165
|
+
return operation();
|
|
1166
|
+
} finally {
|
|
1167
|
+
closeSync2(fd);
|
|
1168
|
+
unlinkSync(lock);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function operationLockPath(path, key) {
|
|
1172
|
+
return path + "." + createHash("sha256").update(key).digest("hex") + ".lock";
|
|
1173
|
+
}
|
|
1174
|
+
async function withCredentialOperation(path, key, operation, options = {}) {
|
|
1175
|
+
prepareStorage(path);
|
|
1176
|
+
const lock = operationLockPath(path, key), deadline = Date.now() + (options.lockWaitMs ?? 3e4);
|
|
1177
|
+
while (true) {
|
|
1178
|
+
options.signal?.throwIfAborted();
|
|
1179
|
+
let fd;
|
|
1180
|
+
try {
|
|
1181
|
+
fd = openSync2(lock, "wx", 384);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
if (error.code !== "EEXIST") throw error;
|
|
1184
|
+
if (Date.now() >= deadline) throw new Error("credentials: credential_busy");
|
|
1185
|
+
await delay2(25, void 0, { signal: options.signal });
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
writeFileSync(fd, String(process.pid));
|
|
1190
|
+
} finally {
|
|
1191
|
+
closeSync2(fd);
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
return await operation();
|
|
1195
|
+
} finally {
|
|
1196
|
+
unlinkSync(lock);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// ../shared/src/credentials/types.ts
|
|
1202
|
+
var ACTIVE_CONTEXTDB_CREDENTIAL = "contextdb/active";
|
|
1203
|
+
|
|
1204
|
+
// ../shared/src/credentials/local-credential-provider.ts
|
|
1205
|
+
function defaultCredentialsPath() {
|
|
1206
|
+
return join3(homedir2(), ".ctxdb", "credentials.json");
|
|
1207
|
+
}
|
|
1208
|
+
var object2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1209
|
+
var fail = (message) => {
|
|
1210
|
+
throw new Error("credentials: " + message);
|
|
1211
|
+
};
|
|
1212
|
+
function origin(value) {
|
|
1213
|
+
if (typeof value !== "string" || !value) return fail("invalid service URL");
|
|
1214
|
+
let u;
|
|
1215
|
+
try {
|
|
1216
|
+
u = new URL(value);
|
|
1217
|
+
} catch {
|
|
1218
|
+
return fail("invalid service URL");
|
|
1219
|
+
}
|
|
1220
|
+
if (!["http:", "https:"].includes(u.protocol) || u.username || u.password || u.search || u.hash) return fail("invalid service URL");
|
|
1221
|
+
return u.toString().replace(/\/$/, "");
|
|
1222
|
+
}
|
|
1223
|
+
function validateApi(raw, internal, v1 = false) {
|
|
1224
|
+
if (!object2(raw.payload) || typeof raw.payload.api_key !== "string" || !raw.payload.api_key) fail("invalid api-key payload");
|
|
1225
|
+
const metadata = v1 ? raw.payload : raw.metadata;
|
|
1226
|
+
origin(metadata.base_url);
|
|
1227
|
+
if (internal) {
|
|
1228
|
+
origin(metadata.login_server);
|
|
1229
|
+
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");
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function validate(key, raw) {
|
|
1233
|
+
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");
|
|
1234
|
+
if (key === ACTIVE_CONTEXTDB_CREDENTIAL) {
|
|
1235
|
+
if (raw.kind !== "api-key" || raw.metadata.owner.distribution !== "internal" || raw.metadata.state !== "ready") fail("invalid contextdb/active record");
|
|
1236
|
+
} 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");
|
|
1237
|
+
const fields = raw.kind === "api-key" ? ["api_key"] : ["ready", "refreshing"].includes(raw.metadata.state) ? ["accessToken", "refreshToken"] : [];
|
|
1238
|
+
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");
|
|
1239
|
+
if (raw.kind === "api-key") validateApi(raw, key === ACTIVE_CONTEXTDB_CREDENTIAL);
|
|
1240
|
+
}
|
|
1241
|
+
function readDocument(path) {
|
|
1242
|
+
const bytes = readPrivate(path);
|
|
1243
|
+
if (bytes === void 0) return { version: 2, records: {} };
|
|
1244
|
+
let doc;
|
|
1245
|
+
try {
|
|
1246
|
+
doc = JSON.parse(bytes);
|
|
1247
|
+
} catch {
|
|
1248
|
+
return fail("credentials.json is not valid JSON");
|
|
1249
|
+
}
|
|
1250
|
+
if (!object2(doc) || ![1, 2].includes(doc.version) || !object2(doc.records)) return fail("unsupported credentials.json schema/version");
|
|
1251
|
+
if (doc.version === 1) {
|
|
1252
|
+
if (Object.keys(doc.records).some((k) => k !== ACTIVE_CONTEXTDB_CREDENTIAL)) fail("unsupported record key in v1 credentials");
|
|
1253
|
+
const active = doc.records[ACTIVE_CONTEXTDB_CREDENTIAL];
|
|
1254
|
+
if (active !== void 0) {
|
|
1255
|
+
if (active.kind !== "api-key") fail("unsupported record kind");
|
|
1256
|
+
validateApi(active, true, true);
|
|
1257
|
+
}
|
|
1258
|
+
} else for (const [key, record] of Object.entries(doc.records)) validate(key, record);
|
|
1259
|
+
return doc;
|
|
1260
|
+
}
|
|
1261
|
+
function upgrade(doc) {
|
|
1262
|
+
if (doc.version === 2) return doc;
|
|
1263
|
+
const record = doc.records[ACTIVE_CONTEXTDB_CREDENTIAL];
|
|
1264
|
+
return { ...doc, version: 2, records: record ? { [ACTIVE_CONTEXTDB_CREDENTIAL]: legacy(record) } : {} };
|
|
1265
|
+
}
|
|
1266
|
+
function legacy(record) {
|
|
1267
|
+
const { api_key, ...details } = record.payload;
|
|
1268
|
+
return { kind: record.kind, payload: { api_key }, metadata: {
|
|
1269
|
+
...details,
|
|
1270
|
+
...record.metadata,
|
|
1271
|
+
base_url: record.payload.base_url,
|
|
1272
|
+
login_server: record.payload.login_server,
|
|
1273
|
+
owner: { distribution: "internal" },
|
|
1274
|
+
state: "ready",
|
|
1275
|
+
revision: 1,
|
|
1276
|
+
generation: createHash2("sha256").update(JSON.stringify(record)).digest("hex").slice(0, 32)
|
|
1277
|
+
} };
|
|
1278
|
+
}
|
|
1279
|
+
function snapshot(doc, key) {
|
|
1280
|
+
const raw = Object.hasOwn(doc.records, key) ? doc.records[key] : void 0;
|
|
1281
|
+
const record = raw && (doc.version === 1 ? legacy(raw) : raw);
|
|
1282
|
+
return { key, record, fingerprint: createHash2("sha256").update(JSON.stringify(record) ?? "missing").digest("hex") };
|
|
1283
|
+
}
|
|
1284
|
+
function apiRecord(record) {
|
|
1285
|
+
if (!record) return void 0;
|
|
1286
|
+
if (record.kind !== "api-key" || record.metadata.owner.distribution !== "internal") return fail("invalid contextdb/active record");
|
|
1287
|
+
return {
|
|
1288
|
+
kind: "api-key",
|
|
1289
|
+
payload: { apiKey: record.payload.api_key },
|
|
1290
|
+
metadata: {
|
|
1291
|
+
baseUrl: origin(record.metadata.base_url),
|
|
1292
|
+
loginServer: origin(record.metadata.login_server),
|
|
1293
|
+
authorizationMethod: record.metadata.authorization_method,
|
|
1294
|
+
issuedAt: record.metadata.issued_at
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
function internalCandidate(record) {
|
|
1299
|
+
return {
|
|
1300
|
+
kind: "api-key",
|
|
1301
|
+
payload: { api_key: record.payload.apiKey },
|
|
1302
|
+
metadata: {
|
|
1303
|
+
base_url: record.metadata.baseUrl,
|
|
1304
|
+
login_server: record.metadata.loginServer,
|
|
1305
|
+
authorization_method: record.metadata.authorizationMethod,
|
|
1306
|
+
issued_at: record.metadata.issuedAt,
|
|
1307
|
+
owner: { distribution: "internal" },
|
|
1308
|
+
state: "ready"
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
var LocalCredentialProvider = class {
|
|
1313
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1314
|
+
path;
|
|
1315
|
+
constructor(path = defaultCredentialsPath()) {
|
|
1316
|
+
this.path = path;
|
|
1317
|
+
}
|
|
1318
|
+
snapshot(key) {
|
|
1319
|
+
return snapshot(readDocument(this.path), key);
|
|
1320
|
+
}
|
|
1321
|
+
readStoredRecord(key) {
|
|
1322
|
+
return this.snapshot(key).record;
|
|
1323
|
+
}
|
|
1324
|
+
async readRecord(key) {
|
|
1325
|
+
return apiRecord(this.readStoredRecord(key));
|
|
1326
|
+
}
|
|
1327
|
+
async describeRecord(key) {
|
|
1328
|
+
const r = this.readStoredRecord(key);
|
|
1329
|
+
return { configured: !!r && r.metadata.state === "ready", kind: r?.kind, writable: true };
|
|
1330
|
+
}
|
|
1331
|
+
onRecordUpdated(listener) {
|
|
1332
|
+
this.listeners.add(listener);
|
|
1333
|
+
return () => this.listeners.delete(listener);
|
|
1334
|
+
}
|
|
1335
|
+
/** Synchronous short commit; callbacks may acquire a config lock, never the reverse. */
|
|
1336
|
+
transaction(operation) {
|
|
1337
|
+
return withCredentialWriteLock(this.path, () => {
|
|
1338
|
+
let doc = readDocument(this.path);
|
|
1339
|
+
const read = (key) => snapshot(doc, key);
|
|
1340
|
+
const check = (expected) => {
|
|
1341
|
+
if (read(expected.key).fingerprint !== expected.fingerprint) fail("authentication changed; the newer credential was preserved, retry the command");
|
|
1342
|
+
};
|
|
1343
|
+
const save = () => {
|
|
1344
|
+
doc = upgrade(doc);
|
|
1345
|
+
atomicCredentialWrite(this.path, doc);
|
|
1346
|
+
};
|
|
1347
|
+
return operation({
|
|
1348
|
+
read,
|
|
1349
|
+
put: (key, candidate, expected) => {
|
|
1350
|
+
if (expected) {
|
|
1351
|
+
if (expected.key !== key) fail("invalid commit target");
|
|
1352
|
+
check(expected);
|
|
1353
|
+
}
|
|
1354
|
+
const before = read(key), previous = before.record;
|
|
1355
|
+
const next = { kind: candidate.kind, payload: candidate.payload, metadata: {
|
|
1356
|
+
...candidate.metadata,
|
|
1357
|
+
generation: previous?.metadata.generation ?? randomBytes2(16).toString("hex"),
|
|
1358
|
+
revision: (previous?.metadata.revision ?? 0) + 1
|
|
1359
|
+
} };
|
|
1360
|
+
validate(key, next);
|
|
1361
|
+
doc = upgrade(doc);
|
|
1362
|
+
doc.records[key] = next;
|
|
1363
|
+
save();
|
|
1364
|
+
return { before, after: read(key) };
|
|
1365
|
+
},
|
|
1366
|
+
remove: (expected) => {
|
|
1367
|
+
check(expected);
|
|
1368
|
+
if (!read(expected.key).record) return;
|
|
1369
|
+
doc = upgrade(doc);
|
|
1370
|
+
delete doc.records[expected.key];
|
|
1371
|
+
save();
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
commit(key, record, expected = this.snapshot(key)) {
|
|
1377
|
+
return this.transaction((tx) => tx.put(key, record, expected));
|
|
1378
|
+
}
|
|
1379
|
+
rollback(receipt) {
|
|
1380
|
+
return this.transaction((tx) => {
|
|
1381
|
+
if (tx.read(receipt.after.key).fingerprint !== receipt.after.fingerprint) return false;
|
|
1382
|
+
if (receipt.before.record) tx.put(receipt.after.key, receipt.before.record, receipt.after);
|
|
1383
|
+
else tx.remove(receipt.after);
|
|
1384
|
+
return true;
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
async modifyRecord(key, mutate, signal) {
|
|
1388
|
+
const before = this.snapshot(key), next = await mutate(apiRecord(before.record));
|
|
1389
|
+
signal?.throwIfAborted();
|
|
1390
|
+
if (next === void 0) return apiRecord(before.record);
|
|
1391
|
+
const receipt = this.commit(key, internalCandidate(next), before);
|
|
1392
|
+
for (const listener of this.listeners) {
|
|
1393
|
+
try {
|
|
1394
|
+
listener(key);
|
|
1395
|
+
} catch {
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return apiRecord(receipt.after.record);
|
|
1399
|
+
}
|
|
1400
|
+
async deleteRecord(key, signal) {
|
|
1401
|
+
signal?.throwIfAborted();
|
|
1402
|
+
const before = this.snapshot(key);
|
|
1403
|
+
this.transaction((tx) => tx.remove(before));
|
|
1404
|
+
for (const listener of this.listeners) {
|
|
1405
|
+
try {
|
|
1406
|
+
listener(key);
|
|
1407
|
+
} catch {
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
function readActiveCredentialSync(path = defaultCredentialsPath()) {
|
|
1413
|
+
return apiRecord(new LocalCredentialProvider(path).readStoredRecord(ACTIVE_CONTEXTDB_CREDENTIAL));
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// ../shared/src/oauth-credentials.ts
|
|
1417
|
+
var OAuthCredentialError = class extends Error {
|
|
1418
|
+
code;
|
|
1419
|
+
constructor(code) {
|
|
1420
|
+
const messages = {
|
|
1421
|
+
login_required: "OAuth session needs login; run ctxdb login for this profile.",
|
|
1422
|
+
credential_busy: "OAuth credential is busy. If its previous process crashed, run ctxdb login for a new credential.",
|
|
1423
|
+
invalid_profile: "OAuth credential does not belong to this profile.",
|
|
1424
|
+
unsafe_storage: "OAuth credential storage is not private or is invalid."
|
|
1425
|
+
};
|
|
1426
|
+
super(messages[code]);
|
|
1427
|
+
this.name = "OAuthCredentialError";
|
|
1428
|
+
this.code = code;
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
var loginRequired = () => new OAuthCredentialError("login_required");
|
|
1432
|
+
var unsafe = () => new OAuthCredentialError("unsafe_storage");
|
|
1433
|
+
var OAuthSessionStore = class {
|
|
1434
|
+
provider;
|
|
1435
|
+
now;
|
|
1436
|
+
lockWaitMs;
|
|
1437
|
+
path;
|
|
1438
|
+
constructor(path, options = {}) {
|
|
1439
|
+
this.path = path;
|
|
1440
|
+
this.provider = new LocalCredentialProvider(path);
|
|
1441
|
+
this.now = options.now ?? Date.now;
|
|
1442
|
+
this.lockWaitMs = options.lockWaitMs ?? 3e4;
|
|
1443
|
+
}
|
|
1444
|
+
saveLogin(profile, session, loginId) {
|
|
1445
|
+
requireProfile(profile);
|
|
1446
|
+
validateSession(session);
|
|
1447
|
+
if (loginId !== void 0) requireProfile(loginId);
|
|
1448
|
+
const ref = randomBytes3(16).toString("hex");
|
|
1449
|
+
this.write(ref, { profile, ...loginId ? { loginId } : {}, state: "ready", session });
|
|
1450
|
+
return ref;
|
|
1451
|
+
}
|
|
1452
|
+
status(ref, owner) {
|
|
1453
|
+
const record = this.read(ref, owner);
|
|
1454
|
+
if (!record) return { state: "missing" };
|
|
1455
|
+
const s = record.session;
|
|
1456
|
+
return {
|
|
1457
|
+
state: record.state,
|
|
1458
|
+
profile: record.profile,
|
|
1459
|
+
baseUrl: s.baseUrl,
|
|
1460
|
+
workspaceId: s.workspaceId,
|
|
1461
|
+
memberId: s.memberId,
|
|
1462
|
+
clientId: s.clientId,
|
|
1463
|
+
scope: s.scope,
|
|
1464
|
+
sessionId: s.sessionId,
|
|
1465
|
+
accountDisplay: s.accountDisplay,
|
|
1466
|
+
accessExpiresAt: s.expiresAt
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
async resolve(ref, owner, transport) {
|
|
1470
|
+
const first = this.read(ref, owner);
|
|
1471
|
+
if (!first || ["reauth_required", "logged_out"].includes(first.state)) throw loginRequired();
|
|
1472
|
+
if (first.state === "ready" && !this.needsRefresh(first.session)) return authorization(first.session);
|
|
1473
|
+
return this.withLock(ref, async () => {
|
|
1474
|
+
const current = this.read(ref, owner);
|
|
1475
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1476
|
+
if (!this.needsRefresh(current.session)) return authorization(current.session);
|
|
1477
|
+
const started = this.now();
|
|
1478
|
+
this.write(ref, { ...current, state: "refreshing" });
|
|
1479
|
+
try {
|
|
1480
|
+
const response = await transport.refresh(current.session.refreshToken, current.session.baseUrl);
|
|
1481
|
+
const next = rotated(current.session, response, started);
|
|
1482
|
+
if (next.expiresAt <= this.now()) throw loginRequired();
|
|
1483
|
+
this.write(ref, { ...current, state: "ready", session: next });
|
|
1484
|
+
return authorization(next);
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
this.write(ref, { ...current, state: "reauth_required" });
|
|
1487
|
+
if (error instanceof OAuthCredentialError) throw error;
|
|
1488
|
+
throw loginRequired();
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
async share(ref, profile, loginId, commit) {
|
|
1493
|
+
requireProfile(loginId);
|
|
1494
|
+
return this.withLock(ref, async () => {
|
|
1495
|
+
const current = this.read(ref, profile);
|
|
1496
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1497
|
+
if (current.loginId && current.loginId !== loginId) throw new OAuthCredentialError("invalid_profile");
|
|
1498
|
+
const receipt = this.write(ref, { ...current, loginId });
|
|
1499
|
+
try {
|
|
1500
|
+
return commit();
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
this.provider.rollback(receipt);
|
|
1503
|
+
throw error;
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
/** Setup changes the DATA endpoint without replacing the identity or racing a refresh. */
|
|
1508
|
+
async changeBaseUrl(ref, owner, baseUrl, beforeWrite) {
|
|
1509
|
+
await this.withLock(ref, async () => {
|
|
1510
|
+
this.provider.transaction((tx) => {
|
|
1511
|
+
beforeWrite();
|
|
1512
|
+
const current = this.read(ref, owner);
|
|
1513
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1514
|
+
const session = { ...current.session, baseUrl };
|
|
1515
|
+
validateSession(session);
|
|
1516
|
+
const before = tx.read(ref), candidate = credentialForSession({ ...current, session });
|
|
1517
|
+
tx.put(ref, { ...candidate, metadata: { ...before.record?.metadata, ...candidate.metadata } }, before);
|
|
1518
|
+
});
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
async logout(ref, owner, transport, beforeRevoke = () => {
|
|
1522
|
+
}) {
|
|
1523
|
+
return this.withLock(ref, async () => {
|
|
1524
|
+
const current = this.read(ref, owner);
|
|
1525
|
+
this.provider.transaction((tx) => {
|
|
1526
|
+
beforeRevoke();
|
|
1527
|
+
const before = tx.read(ref);
|
|
1528
|
+
if (current && before.record) tx.put(ref, { ...before.record, metadata: { ...before.record.metadata, state: "logged_out" }, payload: {} }, before);
|
|
1529
|
+
});
|
|
1530
|
+
let remoteRevoked = false;
|
|
1531
|
+
try {
|
|
1532
|
+
if (current?.session.refreshToken && current.state !== "logged_out") {
|
|
1533
|
+
await transport.revoke(current.session.refreshToken, current.session.baseUrl);
|
|
1534
|
+
remoteRevoked = true;
|
|
1535
|
+
}
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
return { remoteRevoked };
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
/** Release config references under the write lock, then delete an unused session.
|
|
1542
|
+
* Waiting on the operation lock first lets an in-flight refresh supply its latest RT.
|
|
1543
|
+
* Returning false from release preserves a session that still has users.
|
|
1544
|
+
*/
|
|
1545
|
+
async retire(ref, owner, transport, release) {
|
|
1546
|
+
return this.withLock(ref, async () => {
|
|
1547
|
+
const current = this.read(ref, owner);
|
|
1548
|
+
const removed = this.provider.transaction((tx) => {
|
|
1549
|
+
if (!release()) return false;
|
|
1550
|
+
tx.remove(tx.read(ref));
|
|
1551
|
+
return true;
|
|
1552
|
+
});
|
|
1553
|
+
if (!removed) return { removed: false, remoteRevoked: false, remoteUnconfirmed: false };
|
|
1554
|
+
if (!current || current.state === "logged_out") return { removed: true, remoteRevoked: false, remoteUnconfirmed: false };
|
|
1555
|
+
try {
|
|
1556
|
+
if (!current.session.refreshToken) throw loginRequired();
|
|
1557
|
+
await transport.revoke(current.session.refreshToken, current.session.baseUrl);
|
|
1558
|
+
return { removed: true, remoteRevoked: true, remoteUnconfirmed: false };
|
|
1559
|
+
} catch {
|
|
1560
|
+
return { removed: true, remoteRevoked: false, remoteUnconfirmed: true };
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
needsRefresh(session) {
|
|
1565
|
+
return this.now() >= session.expiresAt - Math.min(3e4, (session.expiresAt - session.issuedAt) / 2);
|
|
1566
|
+
}
|
|
1567
|
+
read(ref, owner) {
|
|
1568
|
+
if (!/^[a-f0-9]{32}$/.test(ref)) throw unsafe();
|
|
1569
|
+
requireProfile(typeof owner === "string" ? owner : owner.loginId);
|
|
1570
|
+
let stored;
|
|
1571
|
+
try {
|
|
1572
|
+
stored = this.provider.readStoredRecord(ref);
|
|
1573
|
+
} catch {
|
|
1574
|
+
throw unsafe();
|
|
1575
|
+
}
|
|
1576
|
+
if (!stored) return void 0;
|
|
1577
|
+
if (stored.kind !== "oauth-session" || stored.metadata.owner.distribution !== "public") throw new OAuthCredentialError("invalid_profile");
|
|
1578
|
+
const who = stored.metadata.owner;
|
|
1579
|
+
if (typeof owner === "string" ? who.agent !== owner : who.loginId !== owner.loginId) throw new OAuthCredentialError("invalid_profile");
|
|
1580
|
+
return { profile: who.agent, loginId: who.loginId, state: stored.metadata.state, session: oauthSessionFromCredential(stored) };
|
|
1581
|
+
}
|
|
1582
|
+
write(ref, doc) {
|
|
1583
|
+
const before = this.provider.snapshot(ref);
|
|
1584
|
+
if (before.record && ["logged_out", "reauth_required"].includes(before.record.metadata.state)) throw loginRequired();
|
|
1585
|
+
const candidate = credentialForSession(doc);
|
|
1586
|
+
return this.provider.commit(ref, { ...candidate, metadata: { ...before.record?.metadata, ...candidate.metadata } }, before);
|
|
1587
|
+
}
|
|
1588
|
+
async withLock(ref, task) {
|
|
1589
|
+
if (!/^[a-f0-9]{32}$/.test(ref)) throw unsafe();
|
|
1590
|
+
try {
|
|
1591
|
+
return await withCredentialOperation(this.path, ref, task, { lockWaitMs: this.lockWaitMs });
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
if (error instanceof Error && error.message === "credentials: credential_busy") throw new OAuthCredentialError("credential_busy");
|
|
1594
|
+
throw error;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
function credentialForSession(doc) {
|
|
1599
|
+
const { accessToken, refreshToken, ...metadata } = doc.session;
|
|
1600
|
+
return {
|
|
1601
|
+
kind: "oauth-session",
|
|
1602
|
+
payload: ["ready", "refreshing"].includes(doc.state) ? { accessToken, refreshToken } : {},
|
|
1603
|
+
metadata: {
|
|
1604
|
+
...metadata,
|
|
1605
|
+
owner: { distribution: "public", agent: doc.profile, ...doc.loginId ? { loginId: doc.loginId } : {} },
|
|
1606
|
+
state: doc.state,
|
|
1607
|
+
authorization_method: "device-code"
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function oauthSessionFromCredential(record) {
|
|
1612
|
+
if (record.kind !== "oauth-session") throw unsafe();
|
|
1613
|
+
const m = record.metadata, p = record.payload;
|
|
1614
|
+
const session = {
|
|
1615
|
+
baseUrl: m.baseUrl,
|
|
1616
|
+
workspaceId: m.workspaceId,
|
|
1617
|
+
memberId: m.memberId,
|
|
1618
|
+
clientId: m.clientId,
|
|
1619
|
+
scope: m.scope,
|
|
1620
|
+
sessionId: m.sessionId,
|
|
1621
|
+
accountDisplay: m.accountDisplay,
|
|
1622
|
+
issuedAt: m.issuedAt,
|
|
1623
|
+
expiresAt: m.expiresAt,
|
|
1624
|
+
accessToken: p.accessToken,
|
|
1625
|
+
refreshToken: p.refreshToken
|
|
1626
|
+
};
|
|
1627
|
+
validateSession(session, ["ready", "refreshing"].includes(m.state));
|
|
1628
|
+
return session;
|
|
1629
|
+
}
|
|
1630
|
+
function requireProfile(profile) {
|
|
1631
|
+
if (typeof profile !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(profile)) throw new OAuthCredentialError("invalid_profile");
|
|
1632
|
+
}
|
|
1633
|
+
function validateSession(s, requireTokens = true) {
|
|
1634
|
+
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();
|
|
1635
|
+
for (const value of [s.workspaceId, s.memberId, s.sessionId, s.accountDisplay]) {
|
|
1636
|
+
if (typeof value !== "string" || !value || value.length > 256 || /[\r\n\x00-\x1f]/.test(value)) throw unsafe();
|
|
1637
|
+
}
|
|
1638
|
+
for (const value of requireTokens ? [s.accessToken, s.refreshToken] : []) {
|
|
1639
|
+
if (typeof value !== "string" || !value || value.length > 8192 || /\s/.test(value)) throw unsafe();
|
|
1640
|
+
}
|
|
1641
|
+
let url;
|
|
1642
|
+
try {
|
|
1643
|
+
url = new URL(s.baseUrl);
|
|
1644
|
+
} catch {
|
|
1645
|
+
throw unsafe();
|
|
1646
|
+
}
|
|
1647
|
+
const loopback = ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname);
|
|
1648
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.protocol === "https:" || loopback && url.protocol === "http:")) throw unsafe();
|
|
1649
|
+
}
|
|
1650
|
+
function rotated(current, response, issuedAt) {
|
|
1651
|
+
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();
|
|
1652
|
+
const next = {
|
|
1653
|
+
...current,
|
|
1654
|
+
accessToken: response.access_token,
|
|
1655
|
+
refreshToken: response.refresh_token,
|
|
1656
|
+
issuedAt,
|
|
1657
|
+
expiresAt: issuedAt + response.expires_in * 1e3
|
|
1658
|
+
};
|
|
1659
|
+
validateSession(next);
|
|
1660
|
+
return next;
|
|
1661
|
+
}
|
|
1662
|
+
function authorization(session) {
|
|
1663
|
+
return { scheme: "Bearer", value: session.accessToken, baseUrl: session.baseUrl, expiresAt: session.expiresAt };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// ../shared/src/oauth-profile.ts
|
|
1667
|
+
import { dirname as dirname4, join as join4, resolve } from "path";
|
|
1668
|
+
|
|
1669
|
+
// ../shared/src/auth-config.ts
|
|
1670
|
+
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";
|
|
1671
|
+
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
1672
|
+
import { dirname as dirname3 } from "path";
|
|
1673
|
+
var AuthConfigError = class extends Error {
|
|
1674
|
+
constructor(message) {
|
|
1675
|
+
super(message);
|
|
1676
|
+
this.name = "AuthConfigError";
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
function readAuthConfig(path) {
|
|
1680
|
+
if (!existsSync3(path)) return { version: 2, agents: {} };
|
|
1681
|
+
let raw;
|
|
1682
|
+
try {
|
|
1683
|
+
raw = JSON.parse(readFileSync3(path, "utf8"));
|
|
1684
|
+
} catch {
|
|
1685
|
+
throw new AuthConfigError("Invalid ctxdb.json; repair the JSON before changing authentication.");
|
|
1686
|
+
}
|
|
1687
|
+
if (raw?.version !== 2 || !object3(raw.agents) || raw.logins !== void 0 && !object3(raw.logins)) {
|
|
1688
|
+
throw new AuthConfigError("Unsupported ctxdb configuration; authentication was not changed.");
|
|
1689
|
+
}
|
|
1690
|
+
return raw;
|
|
1691
|
+
}
|
|
1692
|
+
function object3(value) {
|
|
1693
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1694
|
+
}
|
|
1695
|
+
function ownAuthProfile(raw, agent) {
|
|
1696
|
+
const value = Object.hasOwn(raw.agents, agent) ? raw.agents[agent] : void 0;
|
|
1697
|
+
if (value !== void 0 && !object3(value)) throw new AuthConfigError("Invalid agent configuration; repair its section in ctxdb.json.");
|
|
1698
|
+
return value ?? {};
|
|
1699
|
+
}
|
|
1700
|
+
function requireLoginId(id) {
|
|
1701
|
+
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.");
|
|
1702
|
+
}
|
|
1703
|
+
function validateLoginChoice(section) {
|
|
1704
|
+
if (Object.hasOwn(section, "credential_ref")) {
|
|
1705
|
+
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.");
|
|
1706
|
+
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.");
|
|
1707
|
+
}
|
|
1708
|
+
if (!Object.hasOwn(section, "access_credential")) return;
|
|
1709
|
+
if (section.access_credential === null) return;
|
|
1710
|
+
requireLoginId(section.access_credential);
|
|
1711
|
+
if (section.api_key || section.oauth_credential_ref) {
|
|
1712
|
+
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.");
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// ../shared/src/oauth-profile.ts
|
|
1717
|
+
function oauthProfile(reference, configPath, profile, baseUrl) {
|
|
1718
|
+
if (reference === void 0) return void 0;
|
|
1719
|
+
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");
|
|
1720
|
+
const path = resolve(configPath);
|
|
1721
|
+
return {
|
|
1722
|
+
reference,
|
|
1723
|
+
profile,
|
|
1724
|
+
configPath: path,
|
|
1725
|
+
credentialsPath: join4(dirname4(path), "credentials.json"),
|
|
1726
|
+
baseUrl: normalizeCoreOrigin(baseUrl)
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function oauthProfileFromConfig(raw, path, agent, baseUrlOverride) {
|
|
1730
|
+
const section = ownAuthProfile(raw, agent);
|
|
1731
|
+
validateLoginChoice(section);
|
|
1732
|
+
if (section.access_credential === null) return void 0;
|
|
1733
|
+
if (Object.hasOwn(section, "access_credential")) {
|
|
1734
|
+
const id = section.access_credential;
|
|
1735
|
+
requireLoginId(id);
|
|
1736
|
+
const record = raw.logins && Object.hasOwn(raw.logins, id) ? raw.logins[id] : void 0;
|
|
1737
|
+
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.");
|
|
1738
|
+
return storedProfile(record.credential_ref, path, agent, id, baseUrlOverride);
|
|
1739
|
+
}
|
|
1740
|
+
return storedProfile(section.oauth_credential_ref, path, agent, void 0, baseUrlOverride);
|
|
1741
|
+
}
|
|
1742
|
+
function storedProfile(reference, path, agent, loginId, baseUrlOverride) {
|
|
1743
|
+
const descriptor = oauthProfile(reference, path, agent, "https://api.cn-hangzhou.agentcontext.aliyuncs.com");
|
|
1744
|
+
if (!descriptor) return void 0;
|
|
1745
|
+
const profile = { ...descriptor, ...loginId ? { loginId } : {} };
|
|
1746
|
+
const status = new OAuthSessionStore(profile.credentialsPath).status(profile.reference, oauthOwner(profile));
|
|
1747
|
+
if (!("baseUrl" in status) || typeof status.baseUrl !== "string") throw new OAuthCredentialError("login_required");
|
|
1748
|
+
profile.baseUrl = normalizeCoreOrigin(status.baseUrl);
|
|
1749
|
+
if (baseUrlOverride && normalizeCoreOrigin(baseUrlOverride) !== profile.baseUrl) {
|
|
1750
|
+
throw new AuthConfigError("Requested base URL does not match the login Core origin; no token was sent.");
|
|
1751
|
+
}
|
|
1752
|
+
return profile;
|
|
1753
|
+
}
|
|
1754
|
+
function oauthOwner(profile) {
|
|
1755
|
+
return profile.loginId ? { loginId: profile.loginId } : profile.profile;
|
|
1756
|
+
}
|
|
1757
|
+
function oauthProfileStatus(profile) {
|
|
1758
|
+
const status = new OAuthSessionStore(profile.credentialsPath).status(profile.reference, oauthOwner(profile));
|
|
1759
|
+
if (typeof status.baseUrl === "string" && normalizeCoreOrigin(status.baseUrl) !== profile.baseUrl) {
|
|
1760
|
+
throw new AuthConfigError("Requested base URL does not match the login Core origin; no token was sent.");
|
|
1761
|
+
}
|
|
1762
|
+
return status;
|
|
1763
|
+
}
|
|
1764
|
+
var OAuthProfileProvider = class {
|
|
1765
|
+
selected;
|
|
1766
|
+
constructor(selected) {
|
|
1767
|
+
this.selected = selected;
|
|
1768
|
+
}
|
|
1769
|
+
async resolve(options = {}) {
|
|
1770
|
+
const timeoutMs = options.timeoutMs ?? 3e4, deadline = Date.now() + timeoutMs;
|
|
1771
|
+
const selected = this.selected;
|
|
1772
|
+
const readCurrent = () => {
|
|
1773
|
+
const raw = readAuthConfig(selected.configPath);
|
|
1774
|
+
return oauthProfileFromConfig(raw, selected.configPath, selected.profile);
|
|
1775
|
+
};
|
|
1776
|
+
const current = readCurrent();
|
|
1777
|
+
if (!current || current.baseUrl !== selected.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1778
|
+
const store = new OAuthSessionStore(current.credentialsPath, { lockWaitMs: timeoutMs });
|
|
1779
|
+
const status = oauthProfileStatus(current);
|
|
1780
|
+
if (!("baseUrl" in status) || typeof status.baseUrl !== "string" || normalizeCoreOrigin(status.baseUrl) !== current.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1781
|
+
const authorization2 = await store.resolve(current.reference, oauthOwner(current), {
|
|
1782
|
+
refresh: (rt, url) => new CoreDeviceClient(url, { requestTimeoutMs: Math.max(1, deadline - Date.now()) }).refresh(rt, url)
|
|
1783
|
+
});
|
|
1784
|
+
const after = readCurrent();
|
|
1785
|
+
if (!after || after.reference !== current.reference || after.loginId !== current.loginId || after.baseUrl !== current.baseUrl || oauthProfileStatus(after).state !== "ready") throw new OAuthCredentialError("login_required");
|
|
1786
|
+
return authorization2;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
// ../shared/src/login-bindings.ts
|
|
1791
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
1792
|
+
import { dirname as dirname6, join as join6 } from "path";
|
|
1793
|
+
|
|
1794
|
+
// ../shared/src/data-auth.ts
|
|
1795
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
1796
|
+
function selectDataApiCredential(cfg, env = process.env) {
|
|
1797
|
+
const accessToken = env.CTXDB_ACCESS_TOKEN?.trim();
|
|
1798
|
+
if (accessToken) return { type: "access-token", value: accessToken };
|
|
1799
|
+
if (env.CTXDB_API_KEY) return { type: "api-key", value: env.CTXDB_API_KEY };
|
|
1800
|
+
const context = cfg.authContext ? { context: cfg.authContext } : {};
|
|
1801
|
+
if (cfg.oauthCredential) return { type: "oauth-session", profile: cfg.oauthCredential, ...context };
|
|
1802
|
+
return cfg.apiKey ? { type: "api-key", value: cfg.apiKey, ...context } : null;
|
|
1803
|
+
}
|
|
1804
|
+
var DataApiAuthorizationProvider = class {
|
|
1805
|
+
context;
|
|
1806
|
+
constructor(context) {
|
|
1807
|
+
this.context = context;
|
|
1808
|
+
}
|
|
1809
|
+
async resolve(options = {}) {
|
|
1810
|
+
const c = this.context, raw = readAuthConfig(c.configPath), section = ownAuthProfile(raw, c.agent);
|
|
1811
|
+
const fallback = c.inheritDefaultKey && c.agent !== "default" ? ownAuthProfile(raw, "default") : {};
|
|
1812
|
+
if (c.managedCredentials) {
|
|
1813
|
+
const active = readActiveCredentialSync(c.credentialsPath ?? join5(dirname5(c.configPath), "credentials.json"));
|
|
1814
|
+
if (active) {
|
|
1815
|
+
const baseUrl2 = c.baseUrlOverride ?? active.metadata.baseUrl;
|
|
1816
|
+
if (baseUrl2 !== c.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1817
|
+
return { scheme: "Token", value: active.payload.apiKey, baseUrl: baseUrl2, expiresAt: null };
|
|
1818
|
+
}
|
|
1819
|
+
throw new OAuthCredentialError("login_required");
|
|
1820
|
+
}
|
|
1821
|
+
const stored = publicAuthenticationFromConfig(raw, c.configPath, c.agent, c.baseUrlOverride);
|
|
1822
|
+
const baseUrl = stored?.baseUrl ?? String(c.baseUrlOverride || section.base_url || fallback.base_url || "https://api.cn-hangzhou.agentcontext.aliyuncs.com").replace(/\/+$/, "");
|
|
1823
|
+
if (baseUrl !== c.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1824
|
+
if (stored?.apiKey) return { scheme: "Token", value: stored.apiKey, baseUrl, expiresAt: null };
|
|
1825
|
+
if (stored?.oauthCredential) return new OAuthProfileProvider(stored.oauthCredential).resolve(options);
|
|
1826
|
+
if (Object.hasOwn(section, "access_credential")) throw new OAuthCredentialError("login_required");
|
|
1827
|
+
const key = Object.hasOwn(section, "api_key") ? section.api_key : fallback.api_key;
|
|
1828
|
+
if (typeof key !== "string" || !key) throw new OAuthCredentialError("login_required");
|
|
1829
|
+
return { scheme: "Token", value: key, baseUrl, expiresAt: null };
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
function publicAuthenticationFromConfig(raw, path, agent, baseUrlOverride) {
|
|
1833
|
+
const section = ownAuthProfile(raw, agent);
|
|
1834
|
+
validateLoginChoice(section);
|
|
1835
|
+
if (!Object.hasOwn(section, "credential_ref")) {
|
|
1836
|
+
const oauthCredential = oauthProfileFromConfig(raw, path, agent, baseUrlOverride);
|
|
1837
|
+
return oauthCredential ? { baseUrl: oauthCredential.baseUrl, oauthCredential } : void 0;
|
|
1838
|
+
}
|
|
1839
|
+
const r = new LocalCredentialProvider(join5(dirname5(path), "credentials.json")).readStoredRecord(section.credential_ref);
|
|
1840
|
+
if (!r || r.kind !== "api-key" || r.metadata.state !== "ready" || r.metadata.owner.distribution !== "public" || r.metadata.owner.agent !== agent || r.metadata.owner.loginId) {
|
|
1841
|
+
throw new AuthConfigError("credential_ref is missing, invalid or belongs to another Agent. Use setup --api-key/--login to choose a valid credential.");
|
|
1842
|
+
}
|
|
1843
|
+
const baseUrl = String(r.metadata.base_url).replace(/\/+$/, "");
|
|
1844
|
+
if (baseUrlOverride && baseUrl !== baseUrlOverride.replace(/\/+$/, "")) throw new AuthConfigError("Requested base URL does not match the credential origin; no credential was sent.");
|
|
1845
|
+
return { baseUrl, apiKey: r.payload.api_key };
|
|
1846
|
+
}
|
|
1847
|
+
function createRequestAuthorizationProvider(credential, baseUrl) {
|
|
1848
|
+
if (credential?.context) return new DataApiAuthorizationProvider(credential.context);
|
|
1849
|
+
if (credential?.type === "oauth-session") return new OAuthProfileProvider(credential.profile);
|
|
1850
|
+
return { async resolve(_options) {
|
|
1851
|
+
if (!credential) throw new OAuthCredentialError("login_required");
|
|
1852
|
+
return {
|
|
1853
|
+
scheme: credential.type === "access-token" ? "Bearer" : "Token",
|
|
1854
|
+
value: credential.value,
|
|
1855
|
+
baseUrl,
|
|
1856
|
+
expiresAt: null
|
|
1857
|
+
};
|
|
1858
|
+
} };
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// ../shared/src/authorization/public-target.ts
|
|
1862
|
+
import { dirname as dirname7, join as join7 } from "path";
|
|
1863
|
+
|
|
1864
|
+
// src/config.ts
|
|
1865
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1866
|
+
import { homedir as homedir3 } from "os";
|
|
1867
|
+
import { dirname as dirname8, join as join8 } from "path";
|
|
1868
|
+
|
|
834
1869
|
// src/distribution-capabilities.ts
|
|
835
1870
|
var PUBLIC_DISTRIBUTION = {
|
|
836
1871
|
id: "public",
|
|
@@ -838,13 +1873,14 @@ var PUBLIC_DISTRIBUTION = {
|
|
|
838
1873
|
packageRegistry: null,
|
|
839
1874
|
capabilities: {
|
|
840
1875
|
interactiveLogin: false,
|
|
841
|
-
managedCredentials: false
|
|
1876
|
+
managedCredentials: false,
|
|
1877
|
+
deviceFlow: true
|
|
842
1878
|
}
|
|
843
1879
|
};
|
|
844
1880
|
var DISTRIBUTION_MANIFEST = typeof define_CTXDB_DISTRIBUTION_MANIFEST_default === "undefined" ? PUBLIC_DISTRIBUTION : define_CTXDB_DISTRIBUTION_MANIFEST_default;
|
|
845
1881
|
|
|
846
1882
|
// src/config.ts
|
|
847
|
-
var DEFAULT_BASE_URL = "https://
|
|
1883
|
+
var DEFAULT_BASE_URL = "https://api.cn-hangzhou.agentcontext.aliyuncs.com";
|
|
848
1884
|
var DEFAULT_USER_ID = "default";
|
|
849
1885
|
var DEFAULT_TOP_K = 5;
|
|
850
1886
|
var DEFAULT_THRESHOLD = 0.4;
|
|
@@ -852,7 +1888,7 @@ var DEFAULT_KNOWLEDGE_TOP_K = 6;
|
|
|
852
1888
|
var DEFAULT_KB_CATALOG_INJECTION = "session_start";
|
|
853
1889
|
function defaultPath(env) {
|
|
854
1890
|
if (env.CTXDB_CONFIG_PATH) return env.CTXDB_CONFIG_PATH;
|
|
855
|
-
return
|
|
1891
|
+
return join8(homedir3(), ".ctxdb", "ctxdb.json");
|
|
856
1892
|
}
|
|
857
1893
|
function coerceInt(v, fallback) {
|
|
858
1894
|
if (v === null || v === void 0 || v === "") return fallback;
|
|
@@ -874,9 +1910,9 @@ function coerceKbCatalogInjection(v) {
|
|
|
874
1910
|
return DEFAULT_KB_CATALOG_INJECTION;
|
|
875
1911
|
}
|
|
876
1912
|
function readRaw(path) {
|
|
877
|
-
if (!
|
|
1913
|
+
if (!existsSync4(path)) return {};
|
|
878
1914
|
try {
|
|
879
|
-
const parsed = JSON.parse(
|
|
1915
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
880
1916
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
881
1917
|
return parsed;
|
|
882
1918
|
}
|
|
@@ -911,40 +1947,6 @@ function applyDebugPolicy(cfg) {
|
|
|
911
1947
|
cfg.debugReason = policy.debugReason;
|
|
912
1948
|
return cfg;
|
|
913
1949
|
}
|
|
914
|
-
function managedCredential(path) {
|
|
915
|
-
if (!existsSync2(path)) return null;
|
|
916
|
-
if (process.platform !== "win32" && (statSync2(path).mode & 63) !== 0) {
|
|
917
|
-
throw new Error(`credentials: ${path} must be owner-only (run chmod 600)`);
|
|
918
|
-
}
|
|
919
|
-
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
920
|
-
if (raw.version !== 1 || !raw.records || typeof raw.records !== "object") {
|
|
921
|
-
throw new Error("credentials: unsupported credentials.json schema");
|
|
922
|
-
}
|
|
923
|
-
const unknownKeys = Object.keys(raw.records).filter(
|
|
924
|
-
(key) => key !== "contextdb/active"
|
|
925
|
-
);
|
|
926
|
-
if (unknownKeys.length > 0) {
|
|
927
|
-
throw new Error(`credentials: unsupported record key ${unknownKeys[0]}`);
|
|
928
|
-
}
|
|
929
|
-
const record = raw.records["contextdb/active"];
|
|
930
|
-
if (!record) return null;
|
|
931
|
-
if (record.kind !== "api-key" || typeof record.payload?.api_key !== "string" || !record.payload.api_key || typeof record.payload?.base_url !== "string" || !record.payload.base_url) {
|
|
932
|
-
throw new Error("credentials: invalid contextdb/active record");
|
|
933
|
-
}
|
|
934
|
-
let baseUrl;
|
|
935
|
-
try {
|
|
936
|
-
baseUrl = new URL(record.payload.base_url);
|
|
937
|
-
} catch {
|
|
938
|
-
throw new Error("credentials: invalid contextdb/active base_url");
|
|
939
|
-
}
|
|
940
|
-
if (!["https:", "http:"].includes(baseUrl.protocol) || baseUrl.username || baseUrl.password || baseUrl.search || baseUrl.hash) {
|
|
941
|
-
throw new Error("credentials: invalid contextdb/active base_url");
|
|
942
|
-
}
|
|
943
|
-
return {
|
|
944
|
-
apiKey: record.payload.api_key,
|
|
945
|
-
baseUrl: baseUrl.toString().replace(/\/$/, "")
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
1950
|
function loadOpencodeConfig(options = {}) {
|
|
949
1951
|
const env = options.env ?? process.env;
|
|
950
1952
|
const path = options.path ?? defaultPath(env);
|
|
@@ -970,17 +1972,47 @@ function loadOpencodeConfig(options = {}) {
|
|
|
970
1972
|
debugReason: null,
|
|
971
1973
|
kbCatalogInjection: coerceKbCatalogInjection(section.kb_catalog_injection)
|
|
972
1974
|
};
|
|
973
|
-
const managed = DISTRIBUTION_MANIFEST.capabilities.managedCredentials && options.managedCredentials !== false ?
|
|
974
|
-
options.credentialsPath ??
|
|
1975
|
+
const managed = DISTRIBUTION_MANIFEST.capabilities.managedCredentials && options.managedCredentials !== false ? readActiveCredentialSync(
|
|
1976
|
+
options.credentialsPath ?? join8(dirname8(path), "credentials.json")
|
|
975
1977
|
) : null;
|
|
976
1978
|
if (managed) {
|
|
977
|
-
cfg.apiKey = managed.apiKey;
|
|
978
|
-
cfg.baseUrl = managed.baseUrl;
|
|
1979
|
+
cfg.apiKey = managed.payload.apiKey;
|
|
1980
|
+
cfg.baseUrl = managed.metadata.baseUrl;
|
|
1981
|
+
}
|
|
1982
|
+
if (DISTRIBUTION_MANIFEST.capabilities.deviceFlow) {
|
|
1983
|
+
validateLoginChoice(section);
|
|
1984
|
+
const stored = env.CTXDB_API_KEY || env.CTXDB_ACCESS_TOKEN?.trim() ? void 0 : publicAuthenticationFromConfig({ ...raw, version: 2, agents: raw.agents ?? {} }, path, "opencode", env.CTXDB_BASE_URL);
|
|
1985
|
+
if (stored) {
|
|
1986
|
+
cfg.baseUrl = stored.baseUrl;
|
|
1987
|
+
cfg.apiKey = stored.apiKey ?? null;
|
|
1988
|
+
cfg.oauthCredential = stored.oauthCredential;
|
|
1989
|
+
} else if (section.credential_ref) cfg.apiKey = null;
|
|
1990
|
+
}
|
|
1991
|
+
applyEnv(cfg, env);
|
|
1992
|
+
if (managed) cfg.authContext = {
|
|
1993
|
+
configPath: path,
|
|
1994
|
+
agent: "opencode",
|
|
1995
|
+
baseUrl: cfg.baseUrl,
|
|
1996
|
+
inheritDefaultKey: false,
|
|
1997
|
+
managedCredentials: true,
|
|
1998
|
+
credentialsPath: options.credentialsPath,
|
|
1999
|
+
...env.CTXDB_BASE_URL ? { baseUrlOverride: env.CTXDB_BASE_URL } : {}
|
|
2000
|
+
};
|
|
2001
|
+
if (DISTRIBUTION_MANIFEST.capabilities.deviceFlow) {
|
|
2002
|
+
cfg.authContext = {
|
|
2003
|
+
configPath: path,
|
|
2004
|
+
agent: "opencode",
|
|
2005
|
+
baseUrl: cfg.baseUrl,
|
|
2006
|
+
inheritDefaultKey: false,
|
|
2007
|
+
...env.CTXDB_BASE_URL ? { baseUrlOverride: env.CTXDB_BASE_URL } : {}
|
|
2008
|
+
};
|
|
2009
|
+
if (Object.hasOwn(section, "access_credential") && !env.CTXDB_API_KEY) cfg.apiKey = null;
|
|
979
2010
|
}
|
|
980
|
-
|
|
2011
|
+
if (env.CTXDB_API_KEY || accessTokenFromEnv(env)) cfg.oauthCredential = void 0;
|
|
2012
|
+
return cfg;
|
|
981
2013
|
}
|
|
982
2014
|
function isConfigured(cfg, env = process.env) {
|
|
983
|
-
return Boolean((accessTokenFromEnv(env) || cfg.apiKey) && cfg.baseUrl);
|
|
2015
|
+
return Boolean((accessTokenFromEnv(env) || cfg.oauthCredential || cfg.apiKey) && cfg.baseUrl);
|
|
984
2016
|
}
|
|
985
2017
|
|
|
986
2018
|
// src/http-client.ts
|
|
@@ -1001,20 +2033,29 @@ var HttpClient = class {
|
|
|
1001
2033
|
accessToken;
|
|
1002
2034
|
userAgent;
|
|
1003
2035
|
fetchImpl;
|
|
2036
|
+
oauthProvider;
|
|
2037
|
+
credential;
|
|
1004
2038
|
constructor(opts) {
|
|
1005
2039
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
1006
2040
|
this.apiKey = opts.apiKey;
|
|
1007
2041
|
this.accessToken = opts.accessToken ?? null;
|
|
2042
|
+
this.credential = selectDataApiCredential(opts, { CTXDB_ACCESS_TOKEN: opts.accessToken ?? void 0, CTXDB_API_KEY: opts.environmentApiKey });
|
|
2043
|
+
this.oauthProvider = createRequestAuthorizationProvider(this.credential, this.baseUrl);
|
|
1008
2044
|
this.userAgent = opts.userAgent ?? "ctxdb-opencode-plugin/0.0.0";
|
|
1009
2045
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
1010
2046
|
}
|
|
1011
|
-
headers(contentType) {
|
|
2047
|
+
async headers(contentType, timeoutMs) {
|
|
1012
2048
|
const h = {
|
|
1013
2049
|
"User-Agent": this.userAgent,
|
|
1014
2050
|
Connection: "close"
|
|
1015
2051
|
};
|
|
1016
|
-
if (this.
|
|
1017
|
-
|
|
2052
|
+
if (this.oauthProvider) {
|
|
2053
|
+
const authorization2 = await this.oauthProvider.resolve({ timeoutMs });
|
|
2054
|
+
if (authorization2.baseUrl !== this.baseUrl) throw new Error("OAuth environment changed; reload this runtime.");
|
|
2055
|
+
h.Authorization = `${authorization2.scheme} ${authorization2.value}`;
|
|
2056
|
+
} else if (this.credential && this.credential.type !== "oauth-session") {
|
|
2057
|
+
h.Authorization = `${this.credential.type === "access-token" ? "Bearer" : "Token"} ${this.credential.value}`;
|
|
2058
|
+
}
|
|
1018
2059
|
if (contentType) h["Content-Type"] = contentType;
|
|
1019
2060
|
return h;
|
|
1020
2061
|
}
|
|
@@ -1035,49 +2076,54 @@ var HttpClient = class {
|
|
|
1035
2076
|
return this.request("POST", url, path, JSON.stringify(body), "application/json", options.timeoutMs);
|
|
1036
2077
|
}
|
|
1037
2078
|
async request(method, url, path, body, contentType, timeoutMs) {
|
|
1038
|
-
const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2079
|
+
const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS, started = Date.now();
|
|
2080
|
+
const headers = await this.headers(contentType, effectiveTimeout);
|
|
2081
|
+
const bearer = headers.Authorization?.replace(/^(Bearer|Token) /, "");
|
|
2082
|
+
const safeText = (value) => bearer ? String(value).split(bearer).join("[REDACTED]") : String(value);
|
|
1039
2083
|
const controller = new AbortController();
|
|
1040
|
-
const timer = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
2084
|
+
const timer = setTimeout(() => controller.abort(), Math.max(1, effectiveTimeout - (Date.now() - started)));
|
|
1041
2085
|
try {
|
|
1042
2086
|
let resp;
|
|
1043
2087
|
try {
|
|
1044
2088
|
resp = await this.fetchImpl(url, {
|
|
1045
2089
|
method,
|
|
1046
|
-
headers
|
|
2090
|
+
headers,
|
|
1047
2091
|
body,
|
|
1048
|
-
signal: controller.signal
|
|
2092
|
+
signal: controller.signal,
|
|
2093
|
+
...headers.Authorization?.startsWith("Bearer ") ? { redirect: "error" } : {}
|
|
1049
2094
|
});
|
|
1050
2095
|
} catch (err) {
|
|
1051
2096
|
if (err?.name === "AbortError") {
|
|
1052
2097
|
throw new CtxdbHttpError(path, null, `timeout after ${effectiveTimeout}ms`);
|
|
1053
2098
|
}
|
|
1054
|
-
throw new CtxdbHttpError(path, null, `network error: ${err?.message ?? err}`);
|
|
2099
|
+
throw new CtxdbHttpError(path, null, `network error: ${safeText(err?.message ?? err)}`);
|
|
1055
2100
|
}
|
|
1056
2101
|
if (resp.status === 204) return {};
|
|
1057
|
-
let
|
|
2102
|
+
let text2;
|
|
1058
2103
|
try {
|
|
1059
|
-
|
|
2104
|
+
const bodyText = await resp.text();
|
|
2105
|
+
text2 = headers.Authorization?.startsWith("Bearer ") || !resp.ok ? safeText(bodyText) : bodyText;
|
|
1060
2106
|
} catch (err) {
|
|
1061
|
-
throw new CtxdbHttpError(path, resp.status, `body read failed: ${err?.message ?? err}`);
|
|
2107
|
+
throw new CtxdbHttpError(path, resp.status, `body read failed: ${safeText(err?.message ?? err)}`);
|
|
1062
2108
|
}
|
|
1063
2109
|
if (!resp.ok) {
|
|
1064
|
-
throw new CtxdbHttpError(path, resp.status, extractDetail(
|
|
2110
|
+
throw new CtxdbHttpError(path, resp.status, extractDetail(text2) || `HTTP ${resp.status}`);
|
|
1065
2111
|
}
|
|
1066
|
-
if (!
|
|
2112
|
+
if (!text2) return {};
|
|
1067
2113
|
try {
|
|
1068
|
-
return JSON.parse(
|
|
2114
|
+
return JSON.parse(text2);
|
|
1069
2115
|
} catch {
|
|
1070
|
-
return
|
|
2116
|
+
return text2;
|
|
1071
2117
|
}
|
|
1072
2118
|
} finally {
|
|
1073
2119
|
clearTimeout(timer);
|
|
1074
2120
|
}
|
|
1075
2121
|
}
|
|
1076
2122
|
};
|
|
1077
|
-
function extractDetail(
|
|
1078
|
-
if (!
|
|
2123
|
+
function extractDetail(text2) {
|
|
2124
|
+
if (!text2) return "";
|
|
1079
2125
|
try {
|
|
1080
|
-
const parsed = JSON.parse(
|
|
2126
|
+
const parsed = JSON.parse(text2);
|
|
1081
2127
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1082
2128
|
for (const k of ["detail", "message", "error"]) {
|
|
1083
2129
|
const v = parsed[k];
|
|
@@ -1087,7 +2133,7 @@ function extractDetail(text) {
|
|
|
1087
2133
|
}
|
|
1088
2134
|
return String(parsed);
|
|
1089
2135
|
} catch {
|
|
1090
|
-
return
|
|
2136
|
+
return text2;
|
|
1091
2137
|
}
|
|
1092
2138
|
}
|
|
1093
2139
|
|
|
@@ -1330,7 +2376,7 @@ function buildWarmupQuery(cwd, git) {
|
|
|
1330
2376
|
}
|
|
1331
2377
|
|
|
1332
2378
|
// src/capture.ts
|
|
1333
|
-
import { createHash } from "crypto";
|
|
2379
|
+
import { createHash as createHash4 } from "crypto";
|
|
1334
2380
|
var EMPTY2 = {
|
|
1335
2381
|
captured: false,
|
|
1336
2382
|
reason: "",
|
|
@@ -1397,7 +2443,7 @@ function toParsedMessage(m, index) {
|
|
|
1397
2443
|
return null;
|
|
1398
2444
|
}
|
|
1399
2445
|
function fingerprintMessages(messages) {
|
|
1400
|
-
const h =
|
|
2446
|
+
const h = createHash4("sha256");
|
|
1401
2447
|
for (const m of messages) {
|
|
1402
2448
|
h.update(m.role);
|
|
1403
2449
|
h.update("\0");
|
|
@@ -1470,6 +2516,9 @@ function buildRuntime(config, cwd, env = process.env) {
|
|
|
1470
2516
|
http: new HttpClient({
|
|
1471
2517
|
baseUrl: config.baseUrl,
|
|
1472
2518
|
apiKey: config.apiKey,
|
|
2519
|
+
oauthCredential: config.oauthCredential,
|
|
2520
|
+
authContext: config.authContext,
|
|
2521
|
+
environmentApiKey: env.CTXDB_API_KEY,
|
|
1473
2522
|
accessToken: accessTokenFromEnv(env)
|
|
1474
2523
|
}),
|
|
1475
2524
|
sessionState: /* @__PURE__ */ new Map(),
|
|
@@ -1509,7 +2558,13 @@ function extractTextPrompt(parts) {
|
|
|
1509
2558
|
}
|
|
1510
2559
|
async function buildHooks(input) {
|
|
1511
2560
|
if (shouldSkipHooks()) return {};
|
|
1512
|
-
|
|
2561
|
+
let config;
|
|
2562
|
+
try {
|
|
2563
|
+
config = loadOpencodeConfig();
|
|
2564
|
+
} catch {
|
|
2565
|
+
process.stderr.write("[ctxdb] Invalid authentication configuration; OpenCode integration skipped. Run ctxdb auth status --all.\n");
|
|
2566
|
+
return {};
|
|
2567
|
+
}
|
|
1513
2568
|
if (!isConfigured(config)) {
|
|
1514
2569
|
logDebug(
|
|
1515
2570
|
config,
|
|
@@ -1523,10 +2578,10 @@ async function buildHooks(input) {
|
|
|
1523
2578
|
const hooks = {
|
|
1524
2579
|
"chat.message": async (input2, output) => {
|
|
1525
2580
|
try {
|
|
1526
|
-
const
|
|
1527
|
-
if (!
|
|
2581
|
+
const text2 = extractTextPrompt(output.parts);
|
|
2582
|
+
if (!text2) return;
|
|
1528
2583
|
const s = touchSession(rt, input2.sessionID);
|
|
1529
|
-
s.lastPrompt =
|
|
2584
|
+
s.lastPrompt = text2;
|
|
1530
2585
|
} catch (err) {
|
|
1531
2586
|
logError(rt.config, "chat.message", err);
|
|
1532
2587
|
}
|