@aliyunrds/ctxdb-shared 0.0.6 → 0.0.8-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.d.ts +144 -1
- package/dist/index.js +572 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Shared B-rule modules + memory/KB types for RDS ContextDatabase access layers.
|
|
|
6
6
|
|
|
7
7
|
ctxdb-dev ships two access layers that both implement the same B-rules
|
|
8
8
|
(B-0 wrapper strip / B-1 KB anti-injection preamble / B-2 KB content
|
|
9
|
-
sanitize / B-3c KB-
|
|
9
|
+
sanitize / B-3c KB-create-or-update-turn skip) and the same memory/KB type system:
|
|
10
10
|
|
|
11
11
|
- **`openclaw_memory_rds_ctxdb`** — OpenClaw plugin (Mem0-mode platform / OSS)
|
|
12
12
|
- **`@aliyunrds/ctxdb-qoder`** — Qoder hooks + `rds-ctxdb` CLI
|
|
@@ -21,7 +21,7 @@ This package is the single source of truth.
|
|
|
21
21
|
|
|
22
22
|
| Module | Purpose |
|
|
23
23
|
|---|---|
|
|
24
|
-
| `kb-detection.ts` | B-3c — detect KB
|
|
24
|
+
| `kb-detection.ts` | B-3c — detect KB create/update turns by scanning assistant tool_use blocks; short-circuits autoCapture |
|
|
25
25
|
| `graph-context.ts` | Classify GraphRAG synthesis rows and normalize graph content, KB IDs, and citation semantics |
|
|
26
26
|
| `external-knowledge.ts` | B-1 + B-2 — preamble + chunk sanitize; render graph synthesis and document evidence separately inside `<external-knowledge>` |
|
|
27
27
|
| `filtering.ts` | B-0 — strip wrapper tags from messages before extraction; drop noise messages |
|
package/dist/index.d.ts
CHANGED
|
@@ -495,4 +495,147 @@ declare function sanitizeRecallFailureReason(reason: string, secrets?: Array<str
|
|
|
495
495
|
declare function writeRecallTrace(record: RecallTraceRecord, options?: RecallTraceStoreOptions): boolean;
|
|
496
496
|
declare function readRecallTraces(options?: ReadRecallTraceOptions): ReadRecallTraceResult;
|
|
497
497
|
|
|
498
|
-
|
|
498
|
+
type OAuthScope = "read_only" | "read_write" | "default";
|
|
499
|
+
type OAuthLogin = {
|
|
500
|
+
baseUrl: string;
|
|
501
|
+
workspaceId: string;
|
|
502
|
+
memberId: string;
|
|
503
|
+
clientId: "ctxdb-cli";
|
|
504
|
+
scope: OAuthScope;
|
|
505
|
+
sessionId: string;
|
|
506
|
+
accountDisplay: string;
|
|
507
|
+
accessToken: string;
|
|
508
|
+
refreshToken: string;
|
|
509
|
+
/** Milliseconds; use the start of the successful token request, not response arrival, for the expiry bound. */
|
|
510
|
+
issuedAt: number;
|
|
511
|
+
expiresAt: number;
|
|
512
|
+
};
|
|
513
|
+
type Phase = "ready" | "refreshing" | "reauth_required";
|
|
514
|
+
type OAuthRefreshResponse = {
|
|
515
|
+
access_token: string;
|
|
516
|
+
refresh_token: string;
|
|
517
|
+
token_type: string;
|
|
518
|
+
expires_in: number;
|
|
519
|
+
session_id: string;
|
|
520
|
+
scope: string;
|
|
521
|
+
};
|
|
522
|
+
type OAuthRefreshTransport = {
|
|
523
|
+
refresh(refreshToken: string, baseUrl: string): Promise<OAuthRefreshResponse>;
|
|
524
|
+
};
|
|
525
|
+
type OAuthRevokeTransport = {
|
|
526
|
+
revoke(refreshToken: string, baseUrl: string): Promise<void>;
|
|
527
|
+
};
|
|
528
|
+
type OAuthRequestAuthorization = {
|
|
529
|
+
scheme: "Bearer";
|
|
530
|
+
value: string;
|
|
531
|
+
baseUrl: string;
|
|
532
|
+
expiresAt: number;
|
|
533
|
+
};
|
|
534
|
+
declare class OAuthCredentialError extends Error {
|
|
535
|
+
readonly code: "login_required" | "credential_busy" | "invalid_profile" | "unsafe_storage";
|
|
536
|
+
constructor(code: OAuthCredentialError["code"]);
|
|
537
|
+
}
|
|
538
|
+
/** A reference identifies one login, not an agent or a mutable global credential slot.
|
|
539
|
+
* Profiles store only this reference. A new login gets a new reference, so it never steals a
|
|
540
|
+
* crashed refresher's lock or lets a late response overwrite the replacement login.
|
|
541
|
+
*/
|
|
542
|
+
declare class OAuthSessionStore {
|
|
543
|
+
private readonly directory;
|
|
544
|
+
private readonly now;
|
|
545
|
+
private readonly lockWaitMs;
|
|
546
|
+
constructor(directory: string, options?: {
|
|
547
|
+
now?: () => number;
|
|
548
|
+
lockWaitMs?: number;
|
|
549
|
+
});
|
|
550
|
+
saveLogin(profile: string, session: OAuthLogin): string;
|
|
551
|
+
status(ref: string, profile: string): {
|
|
552
|
+
state: "logged_out" | "missing";
|
|
553
|
+
profile?: undefined;
|
|
554
|
+
baseUrl?: undefined;
|
|
555
|
+
workspaceId?: undefined;
|
|
556
|
+
memberId?: undefined;
|
|
557
|
+
clientId?: undefined;
|
|
558
|
+
scope?: undefined;
|
|
559
|
+
sessionId?: undefined;
|
|
560
|
+
accountDisplay?: undefined;
|
|
561
|
+
accessExpiresAt?: undefined;
|
|
562
|
+
} | {
|
|
563
|
+
state: Phase;
|
|
564
|
+
profile: string;
|
|
565
|
+
baseUrl: string;
|
|
566
|
+
workspaceId: string;
|
|
567
|
+
memberId: string;
|
|
568
|
+
clientId: "ctxdb-cli";
|
|
569
|
+
scope: OAuthScope;
|
|
570
|
+
sessionId: string;
|
|
571
|
+
accountDisplay: string;
|
|
572
|
+
accessExpiresAt: number;
|
|
573
|
+
};
|
|
574
|
+
resolve(ref: string, profile: string, transport: OAuthRefreshTransport): Promise<OAuthRequestAuthorization>;
|
|
575
|
+
logout(ref: string, profile: string, transport: OAuthRevokeTransport): Promise<{
|
|
576
|
+
remoteRevoked: boolean;
|
|
577
|
+
}>;
|
|
578
|
+
private needsRefresh;
|
|
579
|
+
private path;
|
|
580
|
+
private isLoggedOut;
|
|
581
|
+
private prepareDirectory;
|
|
582
|
+
private read;
|
|
583
|
+
private write;
|
|
584
|
+
private atomicWrite;
|
|
585
|
+
private withLock;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
type DevicePrompt = {
|
|
589
|
+
requestId: string;
|
|
590
|
+
userCode: string;
|
|
591
|
+
expiresIn: number;
|
|
592
|
+
interval: number;
|
|
593
|
+
};
|
|
594
|
+
type ErrorCode = "authorization_pending" | "slow_down" | "access_denied" | "expired_token" | "invalid_grant" | "invalid_response" | "network_error" | "cancelled" | "request_rejected" | "service_unavailable";
|
|
595
|
+
declare class DeviceFlowError extends Error {
|
|
596
|
+
readonly code: ErrorCode;
|
|
597
|
+
constructor(code: ErrorCode);
|
|
598
|
+
}
|
|
599
|
+
/** Strict Core origin; console routing/discovery does not belong to this client. */
|
|
600
|
+
declare function normalizeCoreOrigin(value: string): string;
|
|
601
|
+
/** JSON + Box adaptation. Never follows redirects, emits raw response errors, or retries a Token request. */
|
|
602
|
+
declare class CoreDeviceClient {
|
|
603
|
+
readonly baseUrl: string;
|
|
604
|
+
private readonly fetchImpl;
|
|
605
|
+
private readonly now;
|
|
606
|
+
private readonly sleep;
|
|
607
|
+
constructor(baseUrl: string, options?: {
|
|
608
|
+
fetchImpl?: typeof fetch;
|
|
609
|
+
now?: () => number;
|
|
610
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
611
|
+
});
|
|
612
|
+
login(options: {
|
|
613
|
+
onPrompt: (prompt: DevicePrompt) => void;
|
|
614
|
+
signal?: AbortSignal;
|
|
615
|
+
}): Promise<OAuthLogin>;
|
|
616
|
+
refresh(refreshToken: string, baseUrl: string): Promise<OAuthRefreshResponse>;
|
|
617
|
+
revoke(refreshToken: string, baseUrl: string): Promise<void>;
|
|
618
|
+
private requireSameOrigin;
|
|
619
|
+
private post;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Safe config metadata, never AT/RT. References are explicit per profile and are not inherited. */
|
|
623
|
+
type OAuthProfile = {
|
|
624
|
+
reference: string;
|
|
625
|
+
profile: string;
|
|
626
|
+
configPath: string;
|
|
627
|
+
directory: string;
|
|
628
|
+
baseUrl: string;
|
|
629
|
+
};
|
|
630
|
+
declare function oauthProfile(reference: unknown, configPath: string, profile: string, baseUrl: string): OAuthProfile | undefined;
|
|
631
|
+
/** Shared by CLI, out-of-process hooks and the in-process OpenCode runtime.
|
|
632
|
+
* Re-read the selected reference so a long-lived runtime sees a new login. An endpoint
|
|
633
|
+
* change requires reloading the runtime; no credential is sent to the replacement host.
|
|
634
|
+
*/
|
|
635
|
+
declare class OAuthProfileProvider {
|
|
636
|
+
private readonly selected;
|
|
637
|
+
constructor(selected: OAuthProfile);
|
|
638
|
+
resolve(): Promise<OAuthRequestAuthorization>;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export { type AddOptions, type AddResult, type AddResultItem, type CategoryConfig, CoreDeviceClient, DOCUMENT_EVIDENCE_SECTION_TITLE, type DebugPolicy, type DebugReason, type DetectionResult, DeviceFlowError, type DevicePrompt, EXTERNAL_KNOWLEDGE_PREAMBLE_HEAD, type ExcludedRecallMemory, type ExternalContext, type ExternalContextChunk, GRAPH_CONTEXT_MAX_CHARS, GRAPH_CONTEXT_SECTION_TITLE, GRAPH_CONTEXT_SOURCE_LABEL, GRAPH_CONTEXT_TAG, GRAPH_CONTEXT_TRUNCATION_MARKER, GRAPH_CONTEXT_USAGE_NOTICE, type ListOptions, type Mem0Provider, type MemoryItem, type NormalizedGraphContext, OAuthCredentialError, type OAuthLogin, type OAuthProfile, OAuthProfileProvider, type OAuthRequestAuthorization, type OAuthScope, OAuthSessionStore, OFFICIAL_PRODUCTION_BASE_URLS, type ParsedMessage, RECALLED_MEMORIES_SAFETY_NOTICE, RECALL_TRACE_AGENTS, RECALL_TRACE_FILENAME, RECALL_TRACE_MAX_BYTES, RECALL_TRACE_OUTCOMES, RECALL_TRACE_ROTATED_GENERATIONS, RECALL_TRACE_SCHEMA_VERSION, type ReadRecallTraceOptions, type ReadRecallTraceResult, type RecallFinishTraceInput, type RecallFinishTraceRecord, type RecallMemoryExclusionReason, type RecallMemorySelectionBase, type RecallResult, type RecallSelectionResult, type RecallStartTraceInput, type RecallStartTraceRecord, type RecallTraceAgent, type RecallTraceKind, type RecallTraceOutcome, type RecallTraceRecord, type RecallTraceRequest, type RecallTraceSelection, type RecallTraceStoreOptions, type SearchOptions, type SearchResultWithExternalContext, type SelectTurnOptions, type SelectedRecallMemory, type SkillsConfig, buildExternalKnowledgeBlock, buildExternalKnowledgePreamble, buildNormalizedGraphContext, createRecallFinishRecord, createRecallStartRecord, extractKnowledgeBaseIds, extractKnowledgeChunkContent, extractKnowledgeChunkSourceLabel, extractKnowledgeChunkTags, filterMessagesForExtraction, formatExternalKnowledgeChunks, formatRecalledMemoriesBlock, isGenericAssistantMessage, isGraphContextChunk, isKnowledgeBaseUploadTurn, isNoiseMessage, isOfficialProductionUrl, isSessionSpecificContent, normalizeBaseUrl, normalizeCoreOrigin, oauthProfile, partitionKnowledgeChunks, readRecallTraces, recall, recallTracePath, resolveDebugPolicy, sanitizeChunkContent, sanitizeQuery, sanitizeRecallFailureReason, selectAndFormatRecalledMemories, selectTurnMessages, stripNoiseFromContent, truncateGraphContext, writeRecallTrace };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/kb-detection.ts
|
|
2
2
|
var KB_PATH_FRAGMENT = "/v1/knowledge/";
|
|
3
3
|
var SHELL_TOOL_NAMES = /* @__PURE__ */ new Set(["exec", "bash", "shell", "sh"]);
|
|
4
|
-
var CTXDB_QODER_KB_UPLOAD_RE = /\b(?:(?:rds|qoder)[-_]ctxdb|ctxdb[-_]qoder|ctxdb)\s+kb\s+upload[-_](?:text|file)\b/i;
|
|
4
|
+
var CTXDB_QODER_KB_UPLOAD_RE = /\b(?:(?:rds|qoder)[-_]ctxdb|ctxdb[-_]qoder|ctxdb)\s+kb\s+(?:upload|update)[-_](?:text|file)\b/i;
|
|
5
5
|
var KB_PLUGIN_TOOL_NAMES = /* @__PURE__ */ new Set(["kb_upload_text", "kb_upload_file"]);
|
|
6
6
|
var WRITE_METHOD_RE = /-X\s+(?:POST|PUT|DELETE|PATCH)\b/i;
|
|
7
7
|
var MULTIPART_UPLOAD_RE = /-F\s+["']?(?:file|content)=@/i;
|
|
@@ -32,7 +32,7 @@ function isKnowledgeBaseUploadTurn(messages) {
|
|
|
32
32
|
if (CTXDB_QODER_KB_UPLOAD_RE.test(command)) {
|
|
33
33
|
return {
|
|
34
34
|
detected: true,
|
|
35
|
-
reason: `shell tool "${name}" invoked a ctxdb
|
|
35
|
+
reason: `shell tool "${name}" invoked a ctxdb CLI KB upload/update command (direct KB write via CLI)`
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
if (!command.includes(KB_PATH_FRAGMENT)) continue;
|
|
@@ -483,8 +483,8 @@ function getMemoryImportance(memory) {
|
|
|
483
483
|
};
|
|
484
484
|
return defaults[cat] ?? 0.5;
|
|
485
485
|
}
|
|
486
|
-
function estimateTokens(
|
|
487
|
-
return Math.ceil(
|
|
486
|
+
function estimateTokens(text2) {
|
|
487
|
+
return Math.ceil(text2.length / CHARS_PER_TOKEN);
|
|
488
488
|
}
|
|
489
489
|
function rankMemories(memories, categoryOrder) {
|
|
490
490
|
const orderMap = new Map(categoryOrder.map((cat, i) => [cat, i]));
|
|
@@ -809,9 +809,9 @@ function sanitizeRecallFailureReason(reason, secrets = []) {
|
|
|
809
809
|
}
|
|
810
810
|
function sanitizeTraceString(value, secrets) {
|
|
811
811
|
let sanitized = value.replace(/authorization\s*[:=]\s*[^\r\n]*/gi, "Authorization: [REDACTED]").replace(/(api[_-]?key|access[_-]?token)\s*[:=]\s*[^\s,;]+/gi, "$1=[REDACTED]");
|
|
812
|
-
for (const
|
|
813
|
-
if (!
|
|
814
|
-
sanitized = sanitized.split(
|
|
812
|
+
for (const secret2 of secrets) {
|
|
813
|
+
if (!secret2) continue;
|
|
814
|
+
sanitized = sanitized.split(secret2).join("[REDACTED]");
|
|
815
815
|
}
|
|
816
816
|
return sanitized;
|
|
817
817
|
}
|
|
@@ -946,6 +946,7 @@ function isRecallTraceRecord(value) {
|
|
|
946
946
|
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);
|
|
947
947
|
if (!baseValid) return false;
|
|
948
948
|
if (record.type === "recall.start") return true;
|
|
949
|
+
if (record.type !== "recall.finish") return false;
|
|
949
950
|
const selection = record.selection;
|
|
950
951
|
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";
|
|
951
952
|
}
|
|
@@ -993,8 +994,567 @@ function readRecallTraces(options = {}) {
|
|
|
993
994
|
});
|
|
994
995
|
return { records: records.map(({ record }) => record), malformedLines };
|
|
995
996
|
}
|
|
997
|
+
|
|
998
|
+
// src/device-flow.ts
|
|
999
|
+
import { setTimeout as delay } from "timers/promises";
|
|
1000
|
+
var DeviceFlowError = class extends Error {
|
|
1001
|
+
code;
|
|
1002
|
+
constructor(code) {
|
|
1003
|
+
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.`);
|
|
1004
|
+
this.name = "DeviceFlowError";
|
|
1005
|
+
this.code = code;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
var invalid = () => new DeviceFlowError("invalid_response");
|
|
1009
|
+
var scopes = ["read_only", "read_write", "default"];
|
|
1010
|
+
var clientId = "ctxdb-cli";
|
|
1011
|
+
var grantType = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1012
|
+
function normalizeCoreOrigin(value) {
|
|
1013
|
+
let url;
|
|
1014
|
+
try {
|
|
1015
|
+
url = new URL(value);
|
|
1016
|
+
} catch {
|
|
1017
|
+
throw new Error("Invalid Core server origin");
|
|
1018
|
+
}
|
|
1019
|
+
const local = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
1020
|
+
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");
|
|
1021
|
+
return url.origin;
|
|
1022
|
+
}
|
|
1023
|
+
var CoreDeviceClient = class {
|
|
1024
|
+
baseUrl;
|
|
1025
|
+
fetchImpl;
|
|
1026
|
+
now;
|
|
1027
|
+
sleep;
|
|
1028
|
+
constructor(baseUrl, options = {}) {
|
|
1029
|
+
this.baseUrl = normalizeCoreOrigin(baseUrl);
|
|
1030
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1031
|
+
this.now = options.now ?? Date.now;
|
|
1032
|
+
this.sleep = options.sleep ?? ((ms, signal) => delay(ms, void 0, { signal }));
|
|
1033
|
+
}
|
|
1034
|
+
async login(options) {
|
|
1035
|
+
const started = this.now();
|
|
1036
|
+
const initial = await this.post("/v1/auth/device/authorize", { client_id: clientId }, options.signal);
|
|
1037
|
+
if (!initial.ok) throw new DeviceFlowError("request_rejected");
|
|
1038
|
+
const data = initial.data;
|
|
1039
|
+
const requestId = text(data.request_id, 32), userCode = text(data.user_code, 9), code = text(data.device_code, 43);
|
|
1040
|
+
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();
|
|
1041
|
+
const expiresIn = integer(data.expires_in, 1, 3600);
|
|
1042
|
+
let interval = integer(data.interval, 1, 3600);
|
|
1043
|
+
const deadline = started + expiresIn * 1e3;
|
|
1044
|
+
options.onPrompt({ requestId, userCode, expiresIn, interval });
|
|
1045
|
+
while (true) {
|
|
1046
|
+
if (options.signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
1047
|
+
if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
|
|
1048
|
+
try {
|
|
1049
|
+
await this.sleep(Math.min(interval * 1e3, deadline - this.now()), options.signal);
|
|
1050
|
+
} catch {
|
|
1051
|
+
throw new DeviceFlowError("cancelled");
|
|
1052
|
+
}
|
|
1053
|
+
if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
|
|
1054
|
+
const issuedAt = this.now();
|
|
1055
|
+
const result = await this.post("/v1/auth/device/token", { grant_type: grantType, client_id: clientId, device_code: code }, options.signal);
|
|
1056
|
+
if (result.ok) return loginResult(result.data, this.baseUrl, issuedAt, this.now());
|
|
1057
|
+
const error = result.data.error;
|
|
1058
|
+
if (error === "authorization_pending") continue;
|
|
1059
|
+
if (error === "slow_down") {
|
|
1060
|
+
interval = Math.max(interval + 5, integer(result.data.interval, 1, 3605));
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (error === "access_denied" || error === "expired_token" || error === "invalid_grant") throw new DeviceFlowError(error);
|
|
1064
|
+
throw new DeviceFlowError("request_rejected");
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
async refresh(refreshToken, baseUrl) {
|
|
1068
|
+
this.requireSameOrigin(baseUrl);
|
|
1069
|
+
const result = await this.post("/v1/auth/token/refresh", { refresh_token: secret(refreshToken) });
|
|
1070
|
+
if (!result.ok) throw new DeviceFlowError("request_rejected");
|
|
1071
|
+
return credentialResult(result.data);
|
|
1072
|
+
}
|
|
1073
|
+
async revoke(refreshToken, baseUrl) {
|
|
1074
|
+
this.requireSameOrigin(baseUrl);
|
|
1075
|
+
const result = await this.post("/v1/auth/token/revoke", { refresh_token: secret(refreshToken) });
|
|
1076
|
+
if (!result.ok) throw new DeviceFlowError("request_rejected");
|
|
1077
|
+
}
|
|
1078
|
+
requireSameOrigin(baseUrl) {
|
|
1079
|
+
if (normalizeCoreOrigin(baseUrl) !== this.baseUrl) throw new Error("OAuth credential Core origin cannot change");
|
|
1080
|
+
}
|
|
1081
|
+
async post(path, body, signal) {
|
|
1082
|
+
if (signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
1083
|
+
const controller = new AbortController();
|
|
1084
|
+
const abort = () => controller.abort();
|
|
1085
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1086
|
+
const timer = setTimeout(abort, 3e4);
|
|
1087
|
+
timer.unref?.();
|
|
1088
|
+
try {
|
|
1089
|
+
const response = await this.fetchImpl(this.baseUrl + path, {
|
|
1090
|
+
method: "POST",
|
|
1091
|
+
body: JSON.stringify(body),
|
|
1092
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1093
|
+
redirect: "error",
|
|
1094
|
+
credentials: "omit",
|
|
1095
|
+
cache: "no-store",
|
|
1096
|
+
signal: controller.signal
|
|
1097
|
+
});
|
|
1098
|
+
const box = await boundedJson(response);
|
|
1099
|
+
if (!Number.isInteger(box.errorCode)) throw invalid();
|
|
1100
|
+
const ok = response.ok && box.errorCode === 0;
|
|
1101
|
+
const data = (!ok || path === "/v1/auth/token/revoke") && box.data == null ? {} : object(box.data);
|
|
1102
|
+
if (!ok && response.status >= 500) throw new DeviceFlowError("service_unavailable");
|
|
1103
|
+
return { ok, data };
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
if (signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
1106
|
+
if (error instanceof DeviceFlowError) throw error;
|
|
1107
|
+
throw new DeviceFlowError("network_error");
|
|
1108
|
+
} finally {
|
|
1109
|
+
clearTimeout(timer);
|
|
1110
|
+
signal?.removeEventListener("abort", abort);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
function text(value, maximum) {
|
|
1115
|
+
if (typeof value !== "string" || !value || value.length > maximum || /[\x00-\x1f\x7f]/.test(value)) throw invalid();
|
|
1116
|
+
return value;
|
|
1117
|
+
}
|
|
1118
|
+
function integer(value, min, max) {
|
|
1119
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw invalid();
|
|
1120
|
+
return value;
|
|
1121
|
+
}
|
|
1122
|
+
function secret(value) {
|
|
1123
|
+
const token = text(value, 8192);
|
|
1124
|
+
if (/\s/.test(token)) throw invalid();
|
|
1125
|
+
return token;
|
|
1126
|
+
}
|
|
1127
|
+
function object(value) {
|
|
1128
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid();
|
|
1129
|
+
return value;
|
|
1130
|
+
}
|
|
1131
|
+
function credentialResult(data) {
|
|
1132
|
+
if (data.token_type !== "Bearer" || !scopes.includes(data.scope)) throw invalid();
|
|
1133
|
+
return {
|
|
1134
|
+
access_token: secret(data.access_token),
|
|
1135
|
+
refresh_token: secret(data.refresh_token),
|
|
1136
|
+
token_type: "Bearer",
|
|
1137
|
+
expires_in: integer(data.expires_in, 1, 3600),
|
|
1138
|
+
session_id: text(data.session_id, 64),
|
|
1139
|
+
scope: data.scope
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function loginResult(data, baseUrl, issuedAt, now) {
|
|
1143
|
+
const tokens = credentialResult(data), scope = tokens.scope;
|
|
1144
|
+
if (data.client_id !== clientId || scope !== "default") throw invalid();
|
|
1145
|
+
const expiresAt = issuedAt + tokens.expires_in * 1e3;
|
|
1146
|
+
if (expiresAt <= now) throw invalid();
|
|
1147
|
+
return {
|
|
1148
|
+
baseUrl,
|
|
1149
|
+
workspaceId: text(data.workspace_id, 64),
|
|
1150
|
+
memberId: text(data.member_id, 64),
|
|
1151
|
+
scope,
|
|
1152
|
+
clientId,
|
|
1153
|
+
sessionId: tokens.session_id,
|
|
1154
|
+
accountDisplay: text(data.account_display, 256),
|
|
1155
|
+
issuedAt,
|
|
1156
|
+
expiresAt,
|
|
1157
|
+
accessToken: tokens.access_token,
|
|
1158
|
+
refreshToken: tokens.refresh_token
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
async function boundedJson(response) {
|
|
1162
|
+
if (!response.body || Number(response.headers.get("content-length")) > 65536) throw invalid();
|
|
1163
|
+
const reader = response.body.getReader();
|
|
1164
|
+
const chunks = [];
|
|
1165
|
+
let size = 0;
|
|
1166
|
+
try {
|
|
1167
|
+
while (true) {
|
|
1168
|
+
const { done, value } = await reader.read();
|
|
1169
|
+
if (done) break;
|
|
1170
|
+
size += value.byteLength;
|
|
1171
|
+
if (size > 65536) {
|
|
1172
|
+
await reader.cancel();
|
|
1173
|
+
throw invalid();
|
|
1174
|
+
}
|
|
1175
|
+
chunks.push(value);
|
|
1176
|
+
}
|
|
1177
|
+
try {
|
|
1178
|
+
return object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
1179
|
+
} catch {
|
|
1180
|
+
throw invalid();
|
|
1181
|
+
}
|
|
1182
|
+
} finally {
|
|
1183
|
+
reader.releaseLock();
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/oauth-credentials.ts
|
|
1188
|
+
import {
|
|
1189
|
+
constants,
|
|
1190
|
+
closeSync as closeSync2,
|
|
1191
|
+
existsSync as existsSync2,
|
|
1192
|
+
fstatSync,
|
|
1193
|
+
fsyncSync,
|
|
1194
|
+
lstatSync,
|
|
1195
|
+
mkdirSync as mkdirSync2,
|
|
1196
|
+
openSync as openSync2,
|
|
1197
|
+
readFileSync as readFileSync2,
|
|
1198
|
+
renameSync as renameSync2,
|
|
1199
|
+
unlinkSync,
|
|
1200
|
+
writeFileSync
|
|
1201
|
+
} from "fs";
|
|
1202
|
+
import { randomBytes } from "crypto";
|
|
1203
|
+
import { join as join3 } from "path";
|
|
1204
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
1205
|
+
|
|
1206
|
+
// src/windows-private-storage.ts
|
|
1207
|
+
import { execFileSync } from "child_process";
|
|
1208
|
+
import { join as join2 } from "path";
|
|
1209
|
+
var SCRIPT = `
|
|
1210
|
+
$ErrorActionPreference = 'Stop'
|
|
1211
|
+
$path = $env:CTXDB_PRIVATE_STORAGE_PATH
|
|
1212
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
1213
|
+
$item = Get-Item -LiteralPath $path -Force
|
|
1214
|
+
if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'reparse' }
|
|
1215
|
+
if ($env:CTXDB_PRIVATE_STORAGE_INITIALIZE -eq '1') {
|
|
1216
|
+
if (-not $item.PSIsContainer) { throw 'not directory' }
|
|
1217
|
+
$acl = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
1218
|
+
$acl.SetOwner($sid)
|
|
1219
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
1220
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
|
1221
|
+
$acl.AddAccessRule($rule)
|
|
1222
|
+
Set-Acl -LiteralPath $path -AclObject $acl
|
|
1223
|
+
}
|
|
1224
|
+
$acl = Get-Acl -LiteralPath $path
|
|
1225
|
+
if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'owner' }
|
|
1226
|
+
$self = $false
|
|
1227
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
|
1228
|
+
if ($rule.AccessControlType -eq 'Allow') {
|
|
1229
|
+
if ($rule.IdentityReference.Value -ne $sid.Value) { throw 'other principal' }
|
|
1230
|
+
if (($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl) { $self = $true }
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (-not $self) { throw 'missing owner access' }
|
|
1234
|
+
Write-Output 'private'
|
|
1235
|
+
`;
|
|
1236
|
+
function checkWindowsPrivateStorage(path, initialize, run = execFileSync) {
|
|
1237
|
+
const systemRoot = process.env.SystemRoot;
|
|
1238
|
+
if (!systemRoot) throw new Error("Private Windows credential storage could not be verified.");
|
|
1239
|
+
try {
|
|
1240
|
+
const result = run(
|
|
1241
|
+
join2(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
1242
|
+
["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(SCRIPT, "utf16le").toString("base64")],
|
|
1243
|
+
{
|
|
1244
|
+
env: { ...process.env, CTXDB_PRIVATE_STORAGE_PATH: path, CTXDB_PRIVATE_STORAGE_INITIALIZE: initialize ? "1" : "0" },
|
|
1245
|
+
encoding: "utf8",
|
|
1246
|
+
windowsHide: true,
|
|
1247
|
+
timeout: 1e4,
|
|
1248
|
+
maxBuffer: 4096,
|
|
1249
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1250
|
+
}
|
|
1251
|
+
);
|
|
1252
|
+
if (String(result).trim() !== "private") throw new Error();
|
|
1253
|
+
} catch {
|
|
1254
|
+
throw new Error("Private Windows credential storage could not be verified.");
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/oauth-credentials.ts
|
|
1259
|
+
var OAuthCredentialError = class extends Error {
|
|
1260
|
+
code;
|
|
1261
|
+
constructor(code) {
|
|
1262
|
+
const messages = {
|
|
1263
|
+
login_required: "OAuth session needs login; run ctxdb login for this profile.",
|
|
1264
|
+
credential_busy: "OAuth credential is busy. If its previous process crashed, run ctxdb login for a new credential.",
|
|
1265
|
+
invalid_profile: "OAuth credential does not belong to this profile.",
|
|
1266
|
+
unsafe_storage: "OAuth credential storage is not private or is invalid."
|
|
1267
|
+
};
|
|
1268
|
+
super(messages[code]);
|
|
1269
|
+
this.name = "OAuthCredentialError";
|
|
1270
|
+
this.code = code;
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
var loginRequired = () => new OAuthCredentialError("login_required");
|
|
1274
|
+
var unsafe = () => new OAuthCredentialError("unsafe_storage");
|
|
1275
|
+
var OAuthSessionStore = class {
|
|
1276
|
+
directory;
|
|
1277
|
+
now;
|
|
1278
|
+
lockWaitMs;
|
|
1279
|
+
constructor(directory, options = {}) {
|
|
1280
|
+
this.directory = directory;
|
|
1281
|
+
this.now = options.now ?? Date.now;
|
|
1282
|
+
this.lockWaitMs = options.lockWaitMs ?? 3e4;
|
|
1283
|
+
}
|
|
1284
|
+
saveLogin(profile, session) {
|
|
1285
|
+
requireProfile(profile);
|
|
1286
|
+
validateSession(session);
|
|
1287
|
+
this.prepareDirectory();
|
|
1288
|
+
const ref = randomBytes(16).toString("hex");
|
|
1289
|
+
this.write(ref, { version: 1, profile, state: "ready", session });
|
|
1290
|
+
return ref;
|
|
1291
|
+
}
|
|
1292
|
+
status(ref, profile) {
|
|
1293
|
+
const record = this.read(ref, profile);
|
|
1294
|
+
if (!record) return { state: this.isLoggedOut(ref) ? "logged_out" : "missing" };
|
|
1295
|
+
const s = record.session;
|
|
1296
|
+
return {
|
|
1297
|
+
state: record.state,
|
|
1298
|
+
profile,
|
|
1299
|
+
baseUrl: s.baseUrl,
|
|
1300
|
+
workspaceId: s.workspaceId,
|
|
1301
|
+
memberId: s.memberId,
|
|
1302
|
+
clientId: s.clientId,
|
|
1303
|
+
scope: s.scope,
|
|
1304
|
+
sessionId: s.sessionId,
|
|
1305
|
+
accountDisplay: s.accountDisplay,
|
|
1306
|
+
accessExpiresAt: s.expiresAt
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
async resolve(ref, profile, transport) {
|
|
1310
|
+
const first = this.read(ref, profile);
|
|
1311
|
+
if (!first || first.state === "reauth_required") throw loginRequired();
|
|
1312
|
+
if (first.state === "ready" && !this.needsRefresh(first.session)) return authorization(first.session);
|
|
1313
|
+
return this.withLock(ref, async () => {
|
|
1314
|
+
const current = this.read(ref, profile);
|
|
1315
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1316
|
+
if (!this.needsRefresh(current.session)) return authorization(current.session);
|
|
1317
|
+
const started = this.now();
|
|
1318
|
+
this.write(ref, { ...current, state: "refreshing" });
|
|
1319
|
+
try {
|
|
1320
|
+
const response = await transport.refresh(current.session.refreshToken, current.session.baseUrl);
|
|
1321
|
+
const next = rotated(current.session, response, started);
|
|
1322
|
+
if (next.expiresAt <= this.now()) throw loginRequired();
|
|
1323
|
+
this.write(ref, { ...current, state: "ready", session: next });
|
|
1324
|
+
return authorization(next);
|
|
1325
|
+
} catch {
|
|
1326
|
+
if (!this.isLoggedOut(ref)) this.write(ref, { ...current, state: "reauth_required" });
|
|
1327
|
+
throw loginRequired();
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
async logout(ref, profile, transport) {
|
|
1332
|
+
const current = this.read(ref, profile);
|
|
1333
|
+
let remoteRevoked = false;
|
|
1334
|
+
try {
|
|
1335
|
+
if (current) {
|
|
1336
|
+
await transport.revoke(current.session.refreshToken, current.session.baseUrl);
|
|
1337
|
+
remoteRevoked = true;
|
|
1338
|
+
}
|
|
1339
|
+
} catch {
|
|
1340
|
+
} finally {
|
|
1341
|
+
this.prepareDirectory();
|
|
1342
|
+
this.atomicWrite(this.path(ref, ".logged-out"), "");
|
|
1343
|
+
remove(this.path(ref));
|
|
1344
|
+
}
|
|
1345
|
+
return { remoteRevoked };
|
|
1346
|
+
}
|
|
1347
|
+
needsRefresh(session) {
|
|
1348
|
+
const advance = Math.min(3e4, (session.expiresAt - session.issuedAt) / 2);
|
|
1349
|
+
return this.now() >= session.expiresAt - advance;
|
|
1350
|
+
}
|
|
1351
|
+
path(ref, suffix = ".json") {
|
|
1352
|
+
if (!/^[a-f0-9]{32}$/.test(ref)) throw unsafe();
|
|
1353
|
+
return join3(this.directory, ref + suffix);
|
|
1354
|
+
}
|
|
1355
|
+
isLoggedOut(ref) {
|
|
1356
|
+
return existsSync2(this.path(ref, ".logged-out"));
|
|
1357
|
+
}
|
|
1358
|
+
prepareDirectory() {
|
|
1359
|
+
if (!existsSync2(this.directory)) {
|
|
1360
|
+
mkdirSync2(join3(this.directory, ".."), { recursive: true, mode: 448 });
|
|
1361
|
+
try {
|
|
1362
|
+
mkdirSync2(this.directory, { mode: 448 });
|
|
1363
|
+
if (process.platform === "win32") checkWindowsPrivateStorage(this.directory, true);
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
if (error.code !== "EEXIST") throw unsafe();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
requirePrivate(this.directory, true);
|
|
1369
|
+
}
|
|
1370
|
+
read(ref, profile) {
|
|
1371
|
+
requireProfile(profile);
|
|
1372
|
+
if (this.isLoggedOut(ref)) return void 0;
|
|
1373
|
+
const path = this.path(ref);
|
|
1374
|
+
if (!existsSync2(path)) return void 0;
|
|
1375
|
+
requirePrivate(this.directory, true);
|
|
1376
|
+
requirePrivate(path, false);
|
|
1377
|
+
const fd = openSync2(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1378
|
+
let text2;
|
|
1379
|
+
try {
|
|
1380
|
+
const stat = fstatSync(fd);
|
|
1381
|
+
if (stat.size > 65536 || !stat.isFile()) throw unsafe();
|
|
1382
|
+
text2 = readFileSync2(fd, "utf8");
|
|
1383
|
+
} finally {
|
|
1384
|
+
closeSync2(fd);
|
|
1385
|
+
}
|
|
1386
|
+
let doc;
|
|
1387
|
+
try {
|
|
1388
|
+
doc = JSON.parse(text2);
|
|
1389
|
+
} catch {
|
|
1390
|
+
throw unsafe();
|
|
1391
|
+
}
|
|
1392
|
+
if (!doc || doc.version !== 1 || !["ready", "refreshing", "reauth_required"].includes(doc.state)) throw unsafe();
|
|
1393
|
+
if (doc.profile !== profile) throw new OAuthCredentialError("invalid_profile");
|
|
1394
|
+
validateSession(doc.session);
|
|
1395
|
+
return this.isLoggedOut(ref) ? void 0 : doc;
|
|
1396
|
+
}
|
|
1397
|
+
write(ref, doc) {
|
|
1398
|
+
if (this.isLoggedOut(ref)) throw loginRequired();
|
|
1399
|
+
this.atomicWrite(this.path(ref), JSON.stringify(doc));
|
|
1400
|
+
if (this.isLoggedOut(ref)) {
|
|
1401
|
+
remove(this.path(ref));
|
|
1402
|
+
throw loginRequired();
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
atomicWrite(path, text2) {
|
|
1406
|
+
this.prepareDirectory();
|
|
1407
|
+
const temporary = path + "." + randomBytes(12).toString("hex") + ".tmp";
|
|
1408
|
+
let fd;
|
|
1409
|
+
try {
|
|
1410
|
+
fd = openSync2(temporary, "wx", 384);
|
|
1411
|
+
writeFileSync(fd, text2);
|
|
1412
|
+
fsyncSync(fd);
|
|
1413
|
+
closeSync2(fd);
|
|
1414
|
+
fd = void 0;
|
|
1415
|
+
renameSync2(temporary, path);
|
|
1416
|
+
if (process.platform !== "win32") {
|
|
1417
|
+
const dir = openSync2(this.directory, "r");
|
|
1418
|
+
try {
|
|
1419
|
+
fsyncSync(dir);
|
|
1420
|
+
} finally {
|
|
1421
|
+
closeSync2(dir);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
} finally {
|
|
1425
|
+
if (fd !== void 0) closeSync2(fd);
|
|
1426
|
+
remove(temporary);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async withLock(ref, task) {
|
|
1430
|
+
const path = this.path(ref, ".lock");
|
|
1431
|
+
const deadline = Date.now() + this.lockWaitMs;
|
|
1432
|
+
this.prepareDirectory();
|
|
1433
|
+
while (true) {
|
|
1434
|
+
let fd;
|
|
1435
|
+
try {
|
|
1436
|
+
fd = openSync2(path, "wx", 384);
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
if (error.code !== "EEXIST") throw unsafe();
|
|
1439
|
+
if (Date.now() >= deadline) throw new OAuthCredentialError("credential_busy");
|
|
1440
|
+
await delay2(25);
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
try {
|
|
1444
|
+
writeFileSync(fd, String(process.pid));
|
|
1445
|
+
fsyncSync(fd);
|
|
1446
|
+
} catch {
|
|
1447
|
+
closeSync2(fd);
|
|
1448
|
+
remove(path);
|
|
1449
|
+
throw unsafe();
|
|
1450
|
+
}
|
|
1451
|
+
closeSync2(fd);
|
|
1452
|
+
try {
|
|
1453
|
+
return await task();
|
|
1454
|
+
} finally {
|
|
1455
|
+
remove(path);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
function requireProfile(profile) {
|
|
1461
|
+
if (typeof profile !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(profile)) throw new OAuthCredentialError("invalid_profile");
|
|
1462
|
+
}
|
|
1463
|
+
function requirePrivate(path, directory) {
|
|
1464
|
+
const s = lstatSync(path);
|
|
1465
|
+
if (s.isSymbolicLink() || (directory ? !s.isDirectory() : !s.isFile())) throw unsafe();
|
|
1466
|
+
if (process.platform === "win32") {
|
|
1467
|
+
try {
|
|
1468
|
+
checkWindowsPrivateStorage(path, false);
|
|
1469
|
+
} catch {
|
|
1470
|
+
throw unsafe();
|
|
1471
|
+
}
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if ((s.mode & 63) !== 0 || process.getuid && s.uid !== process.getuid()) throw unsafe();
|
|
1475
|
+
}
|
|
1476
|
+
function remove(path) {
|
|
1477
|
+
try {
|
|
1478
|
+
unlinkSync(path);
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
if (error.code !== "ENOENT") throw unsafe();
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
function validateSession(s) {
|
|
1484
|
+
if (!s || typeof s !== "object" || s.clientId !== "ctxdb-cli" || !["read_only", "read_write", "default"].includes(s.scope) || !Number.isSafeInteger(s.issuedAt) || !Number.isSafeInteger(s.expiresAt) || s.expiresAt - s.issuedAt < 1e3 || s.expiresAt - s.issuedAt > 36e5) throw unsafe();
|
|
1485
|
+
for (const value of [s.workspaceId, s.memberId, s.sessionId, s.accountDisplay]) {
|
|
1486
|
+
if (typeof value !== "string" || !value || value.length > 256 || /[\r\n\x00-\x1f]/.test(value)) throw unsafe();
|
|
1487
|
+
}
|
|
1488
|
+
for (const value of [s.accessToken, s.refreshToken]) {
|
|
1489
|
+
if (typeof value !== "string" || !value || value.length > 8192 || /\s/.test(value)) throw unsafe();
|
|
1490
|
+
}
|
|
1491
|
+
let url;
|
|
1492
|
+
try {
|
|
1493
|
+
url = new URL(s.baseUrl);
|
|
1494
|
+
} catch {
|
|
1495
|
+
throw unsafe();
|
|
1496
|
+
}
|
|
1497
|
+
const loopback = ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname);
|
|
1498
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.protocol === "https:" || loopback && url.protocol === "http:")) throw unsafe();
|
|
1499
|
+
}
|
|
1500
|
+
function rotated(current, response, issuedAt) {
|
|
1501
|
+
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();
|
|
1502
|
+
const next = {
|
|
1503
|
+
...current,
|
|
1504
|
+
accessToken: response.access_token,
|
|
1505
|
+
refreshToken: response.refresh_token,
|
|
1506
|
+
issuedAt,
|
|
1507
|
+
expiresAt: issuedAt + response.expires_in * 1e3
|
|
1508
|
+
};
|
|
1509
|
+
validateSession(next);
|
|
1510
|
+
return next;
|
|
1511
|
+
}
|
|
1512
|
+
function authorization(session) {
|
|
1513
|
+
return { scheme: "Bearer", value: session.accessToken, baseUrl: session.baseUrl, expiresAt: session.expiresAt };
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/oauth-profile.ts
|
|
1517
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1518
|
+
import { dirname as dirname2, join as join4, resolve } from "path";
|
|
1519
|
+
function oauthProfile(reference, configPath, profile, baseUrl) {
|
|
1520
|
+
if (reference === void 0) return void 0;
|
|
1521
|
+
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");
|
|
1522
|
+
const path = resolve(configPath);
|
|
1523
|
+
return {
|
|
1524
|
+
reference,
|
|
1525
|
+
profile,
|
|
1526
|
+
configPath: path,
|
|
1527
|
+
directory: join4(dirname2(path), "oauth-credentials"),
|
|
1528
|
+
baseUrl: normalizeCoreOrigin(baseUrl)
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
var OAuthProfileProvider = class {
|
|
1532
|
+
selected;
|
|
1533
|
+
constructor(selected) {
|
|
1534
|
+
this.selected = selected;
|
|
1535
|
+
}
|
|
1536
|
+
async resolve() {
|
|
1537
|
+
const selected = this.selected;
|
|
1538
|
+
let raw;
|
|
1539
|
+
try {
|
|
1540
|
+
raw = JSON.parse(readFileSync3(selected.configPath, "utf8"));
|
|
1541
|
+
} catch {
|
|
1542
|
+
throw new OAuthCredentialError("login_required");
|
|
1543
|
+
}
|
|
1544
|
+
const profile = raw?.version === 2 ? raw.agents?.[selected.profile] : void 0;
|
|
1545
|
+
if (!profile || !Object.hasOwn(profile, "oauth_credential_ref")) throw new OAuthCredentialError("login_required");
|
|
1546
|
+
const current = oauthProfile(profile.oauth_credential_ref, selected.configPath, selected.profile, profile.base_url);
|
|
1547
|
+
if (!current || current.baseUrl !== selected.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1548
|
+
const store = new OAuthSessionStore(current.directory);
|
|
1549
|
+
const status = store.status(current.reference, current.profile);
|
|
1550
|
+
if (!("baseUrl" in status) || typeof status.baseUrl !== "string" || normalizeCoreOrigin(status.baseUrl) !== current.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1551
|
+
return store.resolve(current.reference, current.profile, new CoreDeviceClient(current.baseUrl));
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
996
1554
|
export {
|
|
1555
|
+
CoreDeviceClient,
|
|
997
1556
|
DOCUMENT_EVIDENCE_SECTION_TITLE,
|
|
1557
|
+
DeviceFlowError,
|
|
998
1558
|
EXTERNAL_KNOWLEDGE_PREAMBLE_HEAD,
|
|
999
1559
|
GRAPH_CONTEXT_MAX_CHARS,
|
|
1000
1560
|
GRAPH_CONTEXT_SECTION_TITLE,
|
|
@@ -1002,6 +1562,9 @@ export {
|
|
|
1002
1562
|
GRAPH_CONTEXT_TAG,
|
|
1003
1563
|
GRAPH_CONTEXT_TRUNCATION_MARKER,
|
|
1004
1564
|
GRAPH_CONTEXT_USAGE_NOTICE,
|
|
1565
|
+
OAuthCredentialError,
|
|
1566
|
+
OAuthProfileProvider,
|
|
1567
|
+
OAuthSessionStore,
|
|
1005
1568
|
OFFICIAL_PRODUCTION_BASE_URLS,
|
|
1006
1569
|
RECALLED_MEMORIES_SAFETY_NOTICE,
|
|
1007
1570
|
RECALL_TRACE_AGENTS,
|
|
@@ -1029,6 +1592,8 @@ export {
|
|
|
1029
1592
|
isOfficialProductionUrl,
|
|
1030
1593
|
isSessionSpecificContent,
|
|
1031
1594
|
normalizeBaseUrl,
|
|
1595
|
+
normalizeCoreOrigin,
|
|
1596
|
+
oauthProfile,
|
|
1032
1597
|
partitionKnowledgeChunks,
|
|
1033
1598
|
readRecallTraces,
|
|
1034
1599
|
recall,
|
package/package.json
CHANGED