dsh-reference-anything 0.3.0 → 0.3.2
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/LICENSE +22 -22
- package/NOTICE.md +52 -25
- package/README.md +339 -286
- package/cordis.patch.yml +89 -18
- package/lib/client.js +13538 -591
- package/lib/contract.d.ts +155 -16
- package/lib/contract.js +68 -7
- package/lib/download-directory.d.ts +18 -0
- package/lib/download-directory.js +55 -0
- package/lib/errors.d.ts +4 -0
- package/lib/host.d.ts +96 -8
- package/lib/host.js +88 -19
- package/lib/index.d.ts +22 -2
- package/lib/index.js +57 -7
- package/lib/opencli.d.ts +5 -3
- package/lib/opencli.js +13 -6
- package/lib/openlist/host.d.ts +157 -0
- package/lib/openlist/host.js +873 -0
- package/lib/openlist/index.d.ts +84 -0
- package/lib/openlist/index.js +507 -0
- package/lib/render.d.ts +12 -7
- package/lib/render.js +27 -21
- package/lib/sources/cloud-drive/cache.d.ts +60 -0
- package/lib/sources/cloud-drive/cache.js +107 -0
- package/lib/sources/cloud-drive/index.d.ts +190 -0
- package/lib/sources/cloud-drive/index.js +526 -0
- package/lib/sources/cloud-drive/providers/http.d.ts +27 -0
- package/lib/sources/cloud-drive/providers/http.js +73 -0
- package/lib/sources/cloud-drive/providers/openlist.d.ts +59 -0
- package/lib/sources/cloud-drive/providers/openlist.js +535 -0
- package/lib/sources/cloud-drive/registry.d.ts +53 -0
- package/lib/sources/cloud-drive/registry.js +101 -0
- package/lib/sources/cloud-drive/types.d.ts +169 -0
- package/lib/sources/cloud-drive/types.js +17 -0
- package/lib/sources/file.d.ts +2 -0
- package/lib/sources/file.js +2 -0
- package/lib/sources/local-agent/adapters/claude-code.d.ts +16 -0
- package/lib/sources/local-agent/adapters/claude-code.js +171 -0
- package/lib/sources/local-agent/adapters/codex.d.ts +20 -0
- package/lib/sources/local-agent/adapters/codex.js +204 -0
- package/lib/sources/local-agent/adapters/cursor.d.ts +23 -0
- package/lib/sources/local-agent/adapters/cursor.js +121 -0
- package/lib/sources/local-agent/adapters/gemini-cli.d.ts +20 -0
- package/lib/sources/local-agent/adapters/gemini-cli.js +141 -0
- package/lib/sources/local-agent/adapters/grokbuild.d.ts +22 -0
- package/lib/sources/local-agent/adapters/grokbuild.js +76 -0
- package/lib/sources/local-agent/adapters/hermes.d.ts +19 -0
- package/lib/sources/local-agent/adapters/hermes.js +85 -0
- package/lib/sources/local-agent/adapters/kimi.d.ts +28 -0
- package/lib/sources/local-agent/adapters/kimi.js +261 -0
- package/lib/sources/local-agent/adapters/openclaw.d.ts +21 -0
- package/lib/sources/local-agent/adapters/openclaw.js +120 -0
- package/lib/sources/local-agent/adapters/opencode.d.ts +44 -0
- package/lib/sources/local-agent/adapters/opencode.js +236 -0
- package/lib/sources/local-agent/adapters/pi.d.ts +26 -0
- package/lib/sources/local-agent/adapters/pi.js +242 -0
- package/lib/sources/local-agent/adapters/qoder.d.ts +19 -0
- package/lib/sources/local-agent/adapters/qoder.js +93 -0
- package/lib/sources/local-agent/adapters/reasonix.d.ts +23 -0
- package/lib/sources/local-agent/adapters/reasonix.js +90 -0
- package/lib/sources/local-agent/adapters/shared.d.ts +163 -0
- package/lib/sources/local-agent/adapters/shared.js +415 -0
- package/lib/sources/local-agent/adapters/sqlite-shared.d.ts +92 -0
- package/lib/sources/local-agent/adapters/sqlite-shared.js +215 -0
- package/lib/sources/local-agent/adapters/zcode.d.ts +41 -0
- package/lib/sources/local-agent/adapters/zcode.js +159 -0
- package/lib/sources/local-agent/index.d.ts +222 -0
- package/lib/sources/local-agent/index.js +656 -0
- package/lib/sources/local-agent/page.d.ts +55 -0
- package/lib/sources/local-agent/page.js +83 -0
- package/lib/sources/local-agent/path.d.ts +8 -0
- package/lib/sources/local-agent/path.js +43 -0
- package/lib/sources/local-agent/registry.d.ts +68 -0
- package/lib/sources/local-agent/registry.js +116 -0
- package/lib/sources/local-agent/scan.d.ts +175 -0
- package/lib/sources/local-agent/scan.js +472 -0
- package/lib/sources/local-agent/sqlite.d.ts +45 -0
- package/lib/sources/local-agent/sqlite.js +113 -0
- package/lib/sources/local-agent/store.d.ts +93 -0
- package/lib/sources/local-agent/store.js +113 -0
- package/lib/sources/local-agent/types.d.ts +338 -0
- package/lib/sources/local-agent/types.js +20 -0
- package/lib/sources/web-chat/index.d.ts +5 -1
- package/lib/sources/web-chat/index.js +47 -8
- package/lib/store/spec.d.ts +50 -4
- package/lib/store/spec.js +2 -2
- package/lib/sync/index.d.ts +1 -0
- package/lib/sync/index.js +28 -8
- package/lib/tool.d.ts +5 -0
- package/lib/tool.js +418 -45
- package/lib/typert.js +13 -2
- package/lib/types.d.ts +28 -2
- package/lib/update.d.ts +6 -0
- package/lib/update.js +29 -1
- package/lib/web.js +5 -1
- package/lib/wire.d.ts +80 -9
- package/lib/wire.js +30 -4
- package/opencli-plugin/chatgpt.js +30 -27
- package/opencli-plugin/claude.js +12 -8
- package/opencli-plugin/common.js +266 -187
- package/opencli-plugin/deepseek.js +8 -5
- package/opencli-plugin/gemini.js +42 -38
- package/opencli-plugin/grok.js +22 -21
- package/opencli-plugin/kimi.js +34 -31
- package/opencli-plugin/opencli-plugin.json +6 -6
- package/opencli-plugin/package.json +8 -8
- package/package.json +182 -167
- package/lib/profile-mode.d.ts +0 -30
- package/lib/profile-mode.js +0 -90
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type OpenListCredentials } from '../../../openlist/host.ts';
|
|
2
|
+
import type { DriveEntry, DriveProvider, DriveProviderOptions, DriveReadResult } from '../types.ts';
|
|
3
|
+
/** Construction seam intentionally kept host-side; never place this in plugin config or wire types. */
|
|
4
|
+
export interface OpenListProviderOptions extends DriveProviderOptions {
|
|
5
|
+
readonly endpoint?: string;
|
|
6
|
+
readonly username?: string;
|
|
7
|
+
readonly password?: string;
|
|
8
|
+
readonly token?: string;
|
|
9
|
+
/** Host-local credential access; this is intentionally not configuration. */
|
|
10
|
+
readonly credentials?: (refresh?: boolean) => Promise<OpenListCredentials | undefined>;
|
|
11
|
+
/** Limits the safe fallback when an external index/search backend fails. */
|
|
12
|
+
readonly walkDirectories?: number;
|
|
13
|
+
/** DNS seam used to enforce raw-download SSRF policy in tests and production. */
|
|
14
|
+
readonly resolveHost?: (hostname: string) => Promise<readonly {
|
|
15
|
+
readonly address: string;
|
|
16
|
+
}[]>;
|
|
17
|
+
readonly rawRequest?: (url: URL, address: string, init: RequestInit) => Promise<Response>;
|
|
18
|
+
}
|
|
19
|
+
/** Lists and reads a configured OpenList instance through its file-system API. */
|
|
20
|
+
export declare class OpenListDriveProvider implements DriveProvider {
|
|
21
|
+
readonly kind: "openlist";
|
|
22
|
+
readonly displayName = "OpenList";
|
|
23
|
+
supportsRange: boolean | undefined;
|
|
24
|
+
private endpoint;
|
|
25
|
+
private readonly root;
|
|
26
|
+
private readonly fetchImpl;
|
|
27
|
+
private token;
|
|
28
|
+
private readonly username;
|
|
29
|
+
private readonly password;
|
|
30
|
+
private readonly credentialPath;
|
|
31
|
+
private readonly credentialSource;
|
|
32
|
+
private readonly maxWalkDirectories;
|
|
33
|
+
private readonly resolveHost;
|
|
34
|
+
private readonly rawRequest;
|
|
35
|
+
private readonly entries;
|
|
36
|
+
private readonly rawUrls;
|
|
37
|
+
private credentialFingerprint;
|
|
38
|
+
constructor(options?: OpenListProviderOptions);
|
|
39
|
+
credentialed(): Promise<boolean>;
|
|
40
|
+
list(query: string, limit: number, signal?: AbortSignal): Promise<readonly DriveEntry[]>;
|
|
41
|
+
describe(id: string, signal?: AbortSignal): Promise<DriveEntry | undefined>;
|
|
42
|
+
extractedText(): Promise<string | undefined>;
|
|
43
|
+
read(id: string, start: number, end: number, signal?: AbortSignal): Promise<DriveReadResult>;
|
|
44
|
+
private listDirectory;
|
|
45
|
+
private walkFor;
|
|
46
|
+
private resolveRawUrl;
|
|
47
|
+
private fetchRaw;
|
|
48
|
+
private api;
|
|
49
|
+
private nodes;
|
|
50
|
+
private remember;
|
|
51
|
+
private prepareCredentials;
|
|
52
|
+
}
|
|
53
|
+
export declare function validateRawDownloadUrl(value: string, resolveHost?: (hostname: string) => Promise<readonly {
|
|
54
|
+
readonly address: string;
|
|
55
|
+
}[]>, trustedEndpoint?: string): Promise<URL>;
|
|
56
|
+
/** Conservative IANA-special-purpose exclusions; unknown unicast stays usable. */
|
|
57
|
+
export declare function isGlobalUnicastIPv6(value: string): boolean;
|
|
58
|
+
/** Connect to the already-validated address while authenticating the original TLS hostname. */
|
|
59
|
+
export declare function pinnedRawRequest(url: URL, address: string, init: RequestInit): Promise<Response>;
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/** OpenList's private HTTP transport. URLs and credentials never leave this module. */
|
|
2
|
+
import { ReferenceAnythingError } from "../../../errors.js";
|
|
3
|
+
import { lookup } from 'node:dns/promises';
|
|
4
|
+
import { isIP } from 'node:net';
|
|
5
|
+
import { request as httpRequest } from 'node:http';
|
|
6
|
+
import { request as httpsRequest } from 'node:https';
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
import { authenticateOpenList, readOpenListCredentials, validateOpenListEndpoint } from "../../../openlist/host.js";
|
|
9
|
+
import { normalizeOpenListPath } from "../registry.js";
|
|
10
|
+
import { drain, totalFromResponse } from "./http.js";
|
|
11
|
+
/** Lists and reads a configured OpenList instance through its file-system API. */
|
|
12
|
+
export class OpenListDriveProvider {
|
|
13
|
+
kind = 'openlist';
|
|
14
|
+
displayName = 'OpenList';
|
|
15
|
+
supportsRange = undefined;
|
|
16
|
+
endpoint;
|
|
17
|
+
root;
|
|
18
|
+
fetchImpl;
|
|
19
|
+
token;
|
|
20
|
+
username;
|
|
21
|
+
password;
|
|
22
|
+
credentialPath;
|
|
23
|
+
credentialSource;
|
|
24
|
+
maxWalkDirectories;
|
|
25
|
+
resolveHost;
|
|
26
|
+
rawRequest;
|
|
27
|
+
entries = new Map();
|
|
28
|
+
rawUrls = new Map();
|
|
29
|
+
credentialFingerprint;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.endpoint = options.endpoint === undefined ? undefined : validateOpenListEndpoint(options.endpoint);
|
|
32
|
+
this.root = normalizeOpenListPath(options.root ?? '/');
|
|
33
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
34
|
+
this.resolveHost = options.resolveHost ?? (options.fetch === undefined ? async (hostname) => await lookup(hostname, { all: true }) : async () => [{ address: '203.0.113.1' }]);
|
|
35
|
+
this.rawRequest = options.rawRequest ?? (options.fetch === undefined ? pinnedRawRequest : async (url, _address, init) => await this.fetchImpl(url.href, init));
|
|
36
|
+
this.maxWalkDirectories = Math.max(1, Math.min(100, options.walkDirectories ?? 24));
|
|
37
|
+
this.token = options.token;
|
|
38
|
+
this.username = options.username;
|
|
39
|
+
this.password = options.password;
|
|
40
|
+
this.credentialPath = options.configPath;
|
|
41
|
+
this.credentialSource = options.credentials;
|
|
42
|
+
}
|
|
43
|
+
async credentialed() {
|
|
44
|
+
await this.prepareCredentials();
|
|
45
|
+
return this.endpoint !== undefined && this.token !== undefined;
|
|
46
|
+
}
|
|
47
|
+
async list(query, limit, signal) {
|
|
48
|
+
const bounded = Math.max(0, Math.min(100, Math.trunc(limit)));
|
|
49
|
+
if (bounded === 0)
|
|
50
|
+
return [];
|
|
51
|
+
const needle = query.trim();
|
|
52
|
+
if (needle.startsWith('/'))
|
|
53
|
+
return this.listDirectory(needle.replace(/\/+$/u, '') || '/', bounded, signal);
|
|
54
|
+
if (needle === '')
|
|
55
|
+
return this.listDirectory(this.root, bounded, signal);
|
|
56
|
+
try {
|
|
57
|
+
const data = await this.api('/api/fs/search', {
|
|
58
|
+
parent: this.root, keywords: needle, scope: 0, case_sensitive: false, page: 1, per_page: bounded,
|
|
59
|
+
}, signal);
|
|
60
|
+
return this.nodes(data).map(node => this.remember(node, typeof node.parent === 'string' ? node.parent : this.root)).slice(0, bounded);
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
if (cause instanceof ReferenceAnythingError && cause.code === 'REFERENCE_CANCELLED')
|
|
64
|
+
throw cause;
|
|
65
|
+
if (!(cause instanceof OpenListIndexUnavailable))
|
|
66
|
+
throw cause;
|
|
67
|
+
// Search commonly delegates to an optional external index. A failed index
|
|
68
|
+
// is not a failed drive: bounded local walking returns useful partial hits.
|
|
69
|
+
return this.walkFor(needle, bounded, signal);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async describe(id, signal) {
|
|
73
|
+
const path = normalizeOpenListPath(id);
|
|
74
|
+
const cached = this.entries.get(path);
|
|
75
|
+
if (cached !== undefined)
|
|
76
|
+
return cached;
|
|
77
|
+
try {
|
|
78
|
+
const data = await this.api('/api/fs/get', { path }, signal);
|
|
79
|
+
const node = object(data);
|
|
80
|
+
return this.remember(node, parentOf(path));
|
|
81
|
+
}
|
|
82
|
+
catch (cause) {
|
|
83
|
+
if (cause instanceof ReferenceAnythingError && cause.code === 'REFERENCE_NOT_FOUND')
|
|
84
|
+
return undefined;
|
|
85
|
+
throw cause;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async extractedText() { return undefined; }
|
|
89
|
+
async read(id, start, end, signal) {
|
|
90
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end <= start) {
|
|
91
|
+
throw new ReferenceAnythingError('openlist: invalid byte range', 'REFERENCE_INVALID_URI');
|
|
92
|
+
}
|
|
93
|
+
await this.prepareCredentials();
|
|
94
|
+
if (this.endpoint === undefined || this.token === undefined)
|
|
95
|
+
throw new ReferenceAnythingError('OpenList is not connected', 'SOURCE_UNAVAILABLE');
|
|
96
|
+
const path = normalizeOpenListPath(id);
|
|
97
|
+
let url = this.rawUrls.get(path) ?? await this.resolveRawUrl(path, signal);
|
|
98
|
+
let response = await this.fetchRaw(url, start, end, signal);
|
|
99
|
+
if ([400, 401, 403, 404].includes(response.status) && this.rawUrls.has(path)) {
|
|
100
|
+
await cancelResponse(response);
|
|
101
|
+
this.rawUrls.delete(path);
|
|
102
|
+
url = await this.resolveRawUrl(path, signal);
|
|
103
|
+
response = await this.fetchRaw(url, start, end, signal);
|
|
104
|
+
}
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
await cancelResponse(response);
|
|
107
|
+
throw new ReferenceAnythingError(`openlist: raw download failed with HTTP ${response.status}`, 'REFERENCE_READ_FAILED');
|
|
108
|
+
}
|
|
109
|
+
const ranged = response.status === 206;
|
|
110
|
+
this.supportsRange = ranged;
|
|
111
|
+
return { bytes: await drain(response, end - start), ranged, ...totalFromResponse(response) === undefined ? {} : { totalSize: totalFromResponse(response) } };
|
|
112
|
+
}
|
|
113
|
+
async listDirectory(path, limit, signal) {
|
|
114
|
+
const data = await this.api('/api/fs/list', { path, page: 1, per_page: limit, refresh: false }, signal);
|
|
115
|
+
return this.nodes(data).map(node => this.remember(node, path)).slice(0, limit);
|
|
116
|
+
}
|
|
117
|
+
async walkFor(query, limit, signal, markIncomplete = true) {
|
|
118
|
+
const queue = [this.root];
|
|
119
|
+
const visited = new Set();
|
|
120
|
+
const hits = [];
|
|
121
|
+
while (queue.length > 0 && visited.size < this.maxWalkDirectories && hits.length < limit) {
|
|
122
|
+
const directory = queue.shift();
|
|
123
|
+
if (visited.has(directory))
|
|
124
|
+
continue;
|
|
125
|
+
visited.add(directory);
|
|
126
|
+
let children;
|
|
127
|
+
try {
|
|
128
|
+
children = await this.listDirectory(directory, 100, signal);
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
if (cause instanceof ReferenceAnythingError && cause.code === 'REFERENCE_CANCELLED')
|
|
132
|
+
throw cause;
|
|
133
|
+
if (directory === this.root)
|
|
134
|
+
throw cause;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
for (const child of children) {
|
|
138
|
+
if (child.isDirectory && !visited.has(child.id))
|
|
139
|
+
queue.push(child.id);
|
|
140
|
+
if (!child.isDirectory && child.name.toLocaleLowerCase().includes(query.toLocaleLowerCase()))
|
|
141
|
+
hits.push(child);
|
|
142
|
+
if (hits.length >= limit)
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return markIncomplete ? hits.map(entry => ({ ...entry, searchIncomplete: true })) : hits;
|
|
147
|
+
}
|
|
148
|
+
async resolveRawUrl(path, signal) {
|
|
149
|
+
const data = object(await this.api('/api/fs/get', { path }, signal));
|
|
150
|
+
const raw = typeof data.raw_url === 'string' ? data.raw_url : typeof data.rawUrl === 'string' ? data.rawUrl : undefined;
|
|
151
|
+
if (raw === undefined || !/^https?:\/\//i.test(raw))
|
|
152
|
+
throw new ReferenceAnythingError('openlist: file has no raw download URL', 'REFERENCE_READ_FAILED');
|
|
153
|
+
this.rawUrls.set(path, raw);
|
|
154
|
+
this.remember(data, parentOf(path));
|
|
155
|
+
return raw;
|
|
156
|
+
}
|
|
157
|
+
async fetchRaw(url, start, end, signal) {
|
|
158
|
+
try {
|
|
159
|
+
let current = url;
|
|
160
|
+
for (let hop = 0; hop <= 5; hop += 1) {
|
|
161
|
+
const target = await resolveRawDownloadTarget(current, this.resolveHost, this.endpoint);
|
|
162
|
+
let response;
|
|
163
|
+
for (const address of target.addresses) {
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
try {
|
|
166
|
+
response = await this.rawRequest(target.url, address, { redirect: 'manual', headers: { Range: `bytes=${start}-${end - 1}` }, signal });
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
if (signal?.aborted)
|
|
171
|
+
throw signal.reason;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (response === undefined)
|
|
175
|
+
throw new Error('all validated addresses failed');
|
|
176
|
+
if (![301, 302, 303, 307, 308].includes(response.status))
|
|
177
|
+
return response;
|
|
178
|
+
await response.body?.cancel().catch(() => undefined);
|
|
179
|
+
if (hop === 5)
|
|
180
|
+
throw new Error('redirect limit');
|
|
181
|
+
const location = response.headers.get('location');
|
|
182
|
+
if (location === null)
|
|
183
|
+
throw new Error('redirect missing location');
|
|
184
|
+
const next = new URL(location, current);
|
|
185
|
+
if (new URL(current).protocol === 'https:' && next.protocol === 'http:')
|
|
186
|
+
throw new Error('redirect downgrade');
|
|
187
|
+
current = next.href;
|
|
188
|
+
}
|
|
189
|
+
throw new Error('redirect limit');
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
if (signal?.aborted)
|
|
193
|
+
throw new ReferenceAnythingError('openlist: request cancelled', 'REFERENCE_CANCELLED');
|
|
194
|
+
throw new ReferenceAnythingError('openlist: raw download failed', 'REFERENCE_READ_FAILED');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async api(path, body, signal) {
|
|
198
|
+
await this.prepareCredentials();
|
|
199
|
+
if (this.endpoint === undefined || this.token === undefined)
|
|
200
|
+
throw new ReferenceAnythingError('OpenList is not connected', 'SOURCE_UNAVAILABLE');
|
|
201
|
+
try {
|
|
202
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
203
|
+
if (this.endpoint === undefined || this.token === undefined)
|
|
204
|
+
throw new ReferenceAnythingError('OpenList is not connected', 'SOURCE_UNAVAILABLE');
|
|
205
|
+
const response = await this.fetchImpl(`${this.endpoint}${path}`, { method: 'POST', signal, headers: { Authorization: this.token, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
206
|
+
try {
|
|
207
|
+
return await classifyApiResponse(response, path);
|
|
208
|
+
}
|
|
209
|
+
catch (cause) {
|
|
210
|
+
if (!(cause instanceof OpenListAuthRequired) || attempt !== 0 || this.credentialSource === undefined)
|
|
211
|
+
throw cause;
|
|
212
|
+
await this.prepareCredentials(true);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
throw new ReferenceAnythingError('openlist: API request failed', 'REFERENCE_READ_FAILED');
|
|
216
|
+
}
|
|
217
|
+
catch (cause) {
|
|
218
|
+
if (cause instanceof OpenListIndexUnavailable)
|
|
219
|
+
throw cause;
|
|
220
|
+
if (cause instanceof ReferenceAnythingError)
|
|
221
|
+
throw cause;
|
|
222
|
+
if (signal?.aborted)
|
|
223
|
+
throw new ReferenceAnythingError('openlist: request cancelled', 'REFERENCE_CANCELLED');
|
|
224
|
+
throw new ReferenceAnythingError('openlist: API request failed', 'REFERENCE_READ_FAILED');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
nodes(data) {
|
|
228
|
+
const row = object(data);
|
|
229
|
+
const value = Array.isArray(row.content) ? row.content : Array.isArray(data) ? data : [];
|
|
230
|
+
return value.filter((node) => node !== null && typeof node === 'object');
|
|
231
|
+
}
|
|
232
|
+
remember(node, parent) {
|
|
233
|
+
const name = typeof node.name === 'string' && node.name !== '' ? node.name : 'unnamed';
|
|
234
|
+
const path = normalizeOpenListPath(typeof node.path === 'string' ? node.path : `${parent}/${name}`);
|
|
235
|
+
const modified = modifiedAt(node.modified) ?? modifiedAt(node.updated_at);
|
|
236
|
+
const entry = { kind: 'openlist', id: path, path, name, size: finite(node.size), isDirectory: node.is_dir === true || node.isDir === true, ...modified === undefined ? {} : { modifiedAt: modified } };
|
|
237
|
+
this.entries.set(path, entry);
|
|
238
|
+
return entry;
|
|
239
|
+
}
|
|
240
|
+
async prepareCredentials(refresh = false) {
|
|
241
|
+
// A manager source is authoritative and read on every operation. A
|
|
242
|
+
// disconnect or token rotation must invalidate cached signed URLs before
|
|
243
|
+
// the provider ever makes another filesystem request.
|
|
244
|
+
if (this.credentialSource !== undefined) {
|
|
245
|
+
const supplied = await this.credentialSource(refresh);
|
|
246
|
+
if (supplied === undefined || supplied.token === undefined) {
|
|
247
|
+
this.endpoint = undefined;
|
|
248
|
+
this.token = undefined;
|
|
249
|
+
this.credentialFingerprint = undefined;
|
|
250
|
+
this.rawUrls.clear();
|
|
251
|
+
this.entries.clear();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const endpoint = validateOpenListEndpoint(supplied.endpoint);
|
|
255
|
+
const fingerprint = `${endpoint}\0${supplied.token}\0${supplied.generation ?? 0}`;
|
|
256
|
+
if (this.credentialFingerprint !== fingerprint) {
|
|
257
|
+
this.rawUrls.clear();
|
|
258
|
+
this.entries.clear();
|
|
259
|
+
this.credentialFingerprint = fingerprint;
|
|
260
|
+
}
|
|
261
|
+
this.endpoint = endpoint;
|
|
262
|
+
this.token = supplied.token;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (this.endpoint !== undefined && this.token !== undefined)
|
|
266
|
+
return;
|
|
267
|
+
const credentials = await readOpenListCredentials(this.credentialPath);
|
|
268
|
+
if (credentials !== undefined) {
|
|
269
|
+
this.endpoint = validateOpenListEndpoint(credentials.endpoint);
|
|
270
|
+
this.token = credentials.token;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (this.endpoint !== undefined && this.username !== undefined)
|
|
274
|
+
this.token = await authenticateOpenList(this.endpoint, this.username, this.password ?? '', this.fetchImpl);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
class OpenListIndexUnavailable extends Error {
|
|
278
|
+
constructor() { super('OpenList search index is unavailable'); }
|
|
279
|
+
}
|
|
280
|
+
class OpenListAuthRequired extends Error {
|
|
281
|
+
}
|
|
282
|
+
async function classifyApiResponse(response, path) {
|
|
283
|
+
if (response.status === 401) {
|
|
284
|
+
await cancelResponse(response);
|
|
285
|
+
throw new OpenListAuthRequired();
|
|
286
|
+
}
|
|
287
|
+
if (response.status === 404) {
|
|
288
|
+
await cancelResponse(response);
|
|
289
|
+
throw new ReferenceAnythingError('openlist: file not found', 'REFERENCE_NOT_FOUND');
|
|
290
|
+
}
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
if (path === '/api/fs/search' && (response.status === 501 || response.status === 503)) {
|
|
293
|
+
await cancelResponse(response);
|
|
294
|
+
throw new OpenListIndexUnavailable();
|
|
295
|
+
}
|
|
296
|
+
if (path === '/api/fs/search' && response.status === 500) {
|
|
297
|
+
const message = await response.text().catch(() => '');
|
|
298
|
+
if (message.trim() === '' || /(search\s+not\s+available|index.{0,24}(none|unavailable|disabled|not\s+(available|configured|found))|no.{0,12}index)/i.test(message))
|
|
299
|
+
throw new OpenListIndexUnavailable();
|
|
300
|
+
}
|
|
301
|
+
else
|
|
302
|
+
await cancelResponse(response);
|
|
303
|
+
throw new ReferenceAnythingError(`openlist: API failed with HTTP ${response.status}`, 'REFERENCE_READ_FAILED');
|
|
304
|
+
}
|
|
305
|
+
const payload = await response.json();
|
|
306
|
+
const envelope = object(payload);
|
|
307
|
+
if (envelope.code === 401)
|
|
308
|
+
throw new OpenListAuthRequired();
|
|
309
|
+
if (typeof envelope.code === 'number' && envelope.code !== 200) {
|
|
310
|
+
if (path === '/api/fs/search' && typeof envelope.message === 'string' && /(search\s+not\s+available|index.{0,24}(none|unavailable|disabled|not\s+(available|configured|found))|no.{0,12}index)/i.test(envelope.message))
|
|
311
|
+
throw new OpenListIndexUnavailable();
|
|
312
|
+
throw new ReferenceAnythingError('openlist: API request failed', envelope.code === 404 ? 'REFERENCE_NOT_FOUND' : 'REFERENCE_READ_FAILED');
|
|
313
|
+
}
|
|
314
|
+
return envelope.data ?? payload;
|
|
315
|
+
}
|
|
316
|
+
export async function validateRawDownloadUrl(value, resolveHost = async (hostname) => await lookup(hostname, { all: true }), trustedEndpoint) {
|
|
317
|
+
return (await resolveRawDownloadTarget(value, resolveHost, trustedEndpoint)).url;
|
|
318
|
+
}
|
|
319
|
+
async function resolveRawDownloadTarget(value, resolveHost, trustedEndpoint) {
|
|
320
|
+
let url;
|
|
321
|
+
try {
|
|
322
|
+
url = new URL(value);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
throw new ReferenceAnythingError('openlist: raw download URL rejected', 'REFERENCE_READ_FAILED');
|
|
326
|
+
}
|
|
327
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username !== '' || url.password !== '')
|
|
328
|
+
throw new ReferenceAnythingError('openlist: raw download URL rejected', 'REFERENCE_READ_FAILED');
|
|
329
|
+
const trustedOrigin = trustedEndpoint === undefined ? undefined : new URL(validateOpenListEndpoint(trustedEndpoint)).origin;
|
|
330
|
+
const trustedHttp = url.protocol === 'http:' && trustedOrigin !== undefined && url.origin === trustedOrigin && isTrustedLoopbackOrigin(url);
|
|
331
|
+
if (url.protocol !== 'https:' && !trustedHttp)
|
|
332
|
+
throw new ReferenceAnythingError('openlist: raw download URL rejected', 'REFERENCE_READ_FAILED');
|
|
333
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, '').toLowerCase().replace(/\.$/, '');
|
|
334
|
+
if (!trustedHttp && (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === 'metadata' || hostname === 'instance-data' || hostname === 'metadata.google.internal' || hostname === 'metadata.google' || hostname === 'metadata.azure.internal'))
|
|
335
|
+
throw new ReferenceAnythingError('openlist: raw download URL rejected', 'REFERENCE_READ_FAILED');
|
|
336
|
+
let addresses;
|
|
337
|
+
try {
|
|
338
|
+
addresses = isIP(hostname) ? [{ address: hostname }] : await resolveHost(hostname);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
throw new ReferenceAnythingError('openlist: raw download URL rejected', 'REFERENCE_READ_FAILED');
|
|
342
|
+
}
|
|
343
|
+
if (addresses.length === 0 || addresses.some(row => trustedHttp ? !isLoopbackAddress(row.address) : unsafeAddress(row.address)))
|
|
344
|
+
throw new ReferenceAnythingError('openlist: raw download URL rejected', 'REFERENCE_READ_FAILED');
|
|
345
|
+
return { url, addresses: [...new Set(addresses.map(row => row.address))] };
|
|
346
|
+
}
|
|
347
|
+
function unsafeAddress(address) {
|
|
348
|
+
const value = address.toLowerCase().split('%')[0];
|
|
349
|
+
if (isIP(value) === 4) {
|
|
350
|
+
const parts = value.split('.').map(Number);
|
|
351
|
+
const [a, b, c] = parts;
|
|
352
|
+
return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127 || a === 198 && (b === 18 || b === 19) || a >= 224 || a === 192 && b === 0 && c === 0;
|
|
353
|
+
}
|
|
354
|
+
if (isIP(value) === 6) {
|
|
355
|
+
if (!isGlobalUnicastIPv6(value))
|
|
356
|
+
return true;
|
|
357
|
+
const mapped = value.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
|
|
358
|
+
if (mapped !== undefined)
|
|
359
|
+
return unsafeAddress(mapped);
|
|
360
|
+
const hexMapped = value.match(/::ffff:([a-f0-9]{1,4}):([a-f0-9]{1,4})$/);
|
|
361
|
+
if (hexMapped !== null) {
|
|
362
|
+
const high = Number.parseInt(hexMapped[1], 16);
|
|
363
|
+
const low = Number.parseInt(hexMapped[2], 16);
|
|
364
|
+
return unsafeAddress(`${high >>> 8}.${high & 255}.${low >>> 8}.${low & 255}`);
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
/** Conservative IANA-special-purpose exclusions; unknown unicast stays usable. */
|
|
371
|
+
export function isGlobalUnicastIPv6(value) {
|
|
372
|
+
const address = value.toLowerCase().split('%')[0];
|
|
373
|
+
if (isIP(address) !== 6)
|
|
374
|
+
return false;
|
|
375
|
+
const words = ipv6Words(address);
|
|
376
|
+
if (words === undefined)
|
|
377
|
+
return false;
|
|
378
|
+
const [a, b, c, d] = words;
|
|
379
|
+
// The entire IPv4-compatible ::/96 family has legacy platform-dependent
|
|
380
|
+
// routing semantics and must never be treated as public IPv6.
|
|
381
|
+
if (words.slice(0, 6).every(word => word === 0))
|
|
382
|
+
return false;
|
|
383
|
+
if (words.slice(0, 7).every(word => word === 0) && words[7] <= 1)
|
|
384
|
+
return false;
|
|
385
|
+
if ((a & 0xff00) === 0xff00 || (a & 0xfe00) === 0xfc00 || (a & 0xff80) === 0xfe80)
|
|
386
|
+
return false;
|
|
387
|
+
// IANA special-purpose ranges that are not globally forwardable: discard,
|
|
388
|
+
// SRv6 SIDs, documentation and benchmarking assignments.
|
|
389
|
+
if (a === 0x0100 && b === 0 && c === 0 && d === 0 || a === 0x5f00 || a === 0x3fff && (b & 0xf000) === 0)
|
|
390
|
+
return false;
|
|
391
|
+
if (a === 0x2001 && (b === 0 || b === 2 && c === 0 || b === 4 && c === 0x0112 || b === 0x0db8 || (b & 0xfff0) === 0x0010 || (b & 0xfff0) === 0x0020))
|
|
392
|
+
return false;
|
|
393
|
+
// Translation/mapped forms and 6to4 can smuggle a non-global IPv4 address.
|
|
394
|
+
if (address.includes('::ffff:') || /^64:ff9b(?::|$)/.test(address) || /^64:ff9b:1(?::|$)/.test(address))
|
|
395
|
+
return false;
|
|
396
|
+
if (/^2002:/.test(address)) {
|
|
397
|
+
const match = address.match(/^2002:([a-f0-9]{1,4}):([a-f0-9]{1,4})/);
|
|
398
|
+
if (match === null)
|
|
399
|
+
return false;
|
|
400
|
+
const high = Number.parseInt(match[1], 16);
|
|
401
|
+
const low = Number.parseInt(match[2], 16);
|
|
402
|
+
return !unsafeAddress(`${high >>> 8}.${high & 255}.${low >>> 8}.${low & 255}`);
|
|
403
|
+
}
|
|
404
|
+
// Discard-only, ORCHID and benchmarking/documentation assignments.
|
|
405
|
+
if (/^100::/.test(address) || /^2001:(?:0|10|20)(?::|$)/.test(address))
|
|
406
|
+
return false;
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
function isLoopbackAddress(address) { const value = address.toLowerCase().split('%')[0]; return value === '::1' || value === '127.0.0.1'; }
|
|
410
|
+
function isTrustedLoopbackOrigin(url) { const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase(); return host === 'localhost' || isLoopbackAddress(host); }
|
|
411
|
+
function ipv6Words(address) {
|
|
412
|
+
const halves = address.split('::');
|
|
413
|
+
if (halves.length > 2)
|
|
414
|
+
return undefined;
|
|
415
|
+
const parse = (part) => {
|
|
416
|
+
if (part === '')
|
|
417
|
+
return [];
|
|
418
|
+
const result = [];
|
|
419
|
+
for (const token of part.split(':')) {
|
|
420
|
+
if (token.includes('.')) {
|
|
421
|
+
const octets = token.split('.').map(Number);
|
|
422
|
+
if (octets.length !== 4 || octets.some(octet => !Number.isInteger(octet) || octet < 0 || octet > 255))
|
|
423
|
+
return undefined;
|
|
424
|
+
result.push(octets[0] * 256 + octets[1], octets[2] * 256 + octets[3]);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
const word = Number.parseInt(token, 16);
|
|
428
|
+
if (!/^[a-f0-9]{1,4}$/i.test(token) || !Number.isInteger(word))
|
|
429
|
+
return undefined;
|
|
430
|
+
result.push(word);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
};
|
|
435
|
+
const left = parse(halves[0]);
|
|
436
|
+
const right = parse(halves[1] ?? '');
|
|
437
|
+
if (left === undefined || right === undefined)
|
|
438
|
+
return undefined;
|
|
439
|
+
const missing = 8 - left.length - right.length;
|
|
440
|
+
if (halves.length === 1 ? missing !== 0 : missing < 1)
|
|
441
|
+
return undefined;
|
|
442
|
+
return [...left, ...Array.from({ length: missing }, () => 0), ...right];
|
|
443
|
+
}
|
|
444
|
+
/** Connect to the already-validated address while authenticating the original TLS hostname. */
|
|
445
|
+
export function pinnedRawRequest(url, address, init) {
|
|
446
|
+
return new Promise((resolve, reject) => {
|
|
447
|
+
let settled = false;
|
|
448
|
+
let responseStream;
|
|
449
|
+
const headers = {};
|
|
450
|
+
new Headers(init.headers).forEach((value, name) => { headers[name] = value; });
|
|
451
|
+
headers.host = url.host;
|
|
452
|
+
const request = (url.protocol === 'https:' ? httpsRequest : httpRequest)({
|
|
453
|
+
protocol: url.protocol, hostname: address, port: url.port === '' ? undefined : Number(url.port), path: `${url.pathname}${url.search}`, method: 'GET', headers,
|
|
454
|
+
...(url.protocol === 'https:' ? { servername: url.hostname, rejectUnauthorized: true } : {}), signal: init.signal ?? undefined, agent: false,
|
|
455
|
+
}, response => {
|
|
456
|
+
responseStream = response;
|
|
457
|
+
try {
|
|
458
|
+
const status = response.statusCode;
|
|
459
|
+
if (typeof status !== 'number' || !Number.isInteger(status) || status < 200 || status > 599) {
|
|
460
|
+
response.destroy();
|
|
461
|
+
fail(new Error('invalid upstream status'));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const responseHeaders = new Headers();
|
|
465
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
466
|
+
if (Array.isArray(value))
|
|
467
|
+
for (const item of value)
|
|
468
|
+
responseHeaders.append(name, item);
|
|
469
|
+
else if (value !== undefined)
|
|
470
|
+
responseHeaders.set(name, String(value));
|
|
471
|
+
}
|
|
472
|
+
const noBody = status === 204 || status === 205 || status === 304;
|
|
473
|
+
if (noBody)
|
|
474
|
+
response.destroy();
|
|
475
|
+
else {
|
|
476
|
+
const clear = () => clearTimeout(totalTimeout);
|
|
477
|
+
response.once('end', clear);
|
|
478
|
+
response.once('close', clear);
|
|
479
|
+
}
|
|
480
|
+
succeed(new Response(noBody ? null : Readable.toWeb(response), { status, statusText: response.statusMessage, headers: responseHeaders }), noBody);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
response.destroy();
|
|
484
|
+
fail(new Error('invalid upstream response'));
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
const totalTimeout = setTimeout(() => {
|
|
488
|
+
const error = new Error('raw request timed out');
|
|
489
|
+
responseStream?.destroy(error);
|
|
490
|
+
request.destroy(error);
|
|
491
|
+
}, 10_000);
|
|
492
|
+
totalTimeout.unref?.();
|
|
493
|
+
const finish = (clearTimer = true) => {
|
|
494
|
+
if (settled)
|
|
495
|
+
return false;
|
|
496
|
+
settled = true;
|
|
497
|
+
if (clearTimer)
|
|
498
|
+
clearTimeout(totalTimeout);
|
|
499
|
+
return true;
|
|
500
|
+
};
|
|
501
|
+
const succeed = (response, clearTimer) => { if (finish(clearTimer))
|
|
502
|
+
resolve(response); };
|
|
503
|
+
const fail = (cause) => {
|
|
504
|
+
if (!finish())
|
|
505
|
+
return;
|
|
506
|
+
// Parser failures (for example an out-of-range status) can leave the
|
|
507
|
+
// underlying socket open even though ClientRequest emitted `error`.
|
|
508
|
+
// Close both handles before rejecting so callers and their servers do
|
|
509
|
+
// not retain an unusable connection until the peer times out.
|
|
510
|
+
request.destroy();
|
|
511
|
+
request.socket?.destroy();
|
|
512
|
+
const error = cause instanceof Error ? cause : new Error('invalid upstream response');
|
|
513
|
+
const parserCode = typeof error.code === 'string' ? error.code : '';
|
|
514
|
+
reject(parserCode === 'HPE_INVALID_STATUS' || /invalid response status/i.test(error.message) ? new Error('invalid upstream status') : error);
|
|
515
|
+
};
|
|
516
|
+
request.once('error', fail);
|
|
517
|
+
request.end();
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
async function cancelResponse(response) {
|
|
521
|
+
if (response.body === null)
|
|
522
|
+
return;
|
|
523
|
+
await Promise.race([
|
|
524
|
+
response.body.cancel().catch(() => undefined),
|
|
525
|
+
new Promise(resolve => { const timer = setTimeout(resolve, 100); timer.unref?.(); }),
|
|
526
|
+
]);
|
|
527
|
+
}
|
|
528
|
+
function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
|
|
529
|
+
function finite(value) { return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0; }
|
|
530
|
+
function parentOf(path) { const cut = path.lastIndexOf('/'); return cut <= 0 ? '/' : path.slice(0, cut); }
|
|
531
|
+
function modifiedAt(value) { if (typeof value === 'number' && Number.isFinite(value))
|
|
532
|
+
return value > 10_000_000_000 ? value : value * 1000; if (typeof value === 'string') {
|
|
533
|
+
const parsed = Date.parse(value);
|
|
534
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
535
|
+
} return undefined; }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which drives this package can talk to, and how a reference names one.
|
|
3
|
+
*
|
|
4
|
+
* Kept pure — no cordis, no `node:fs` — so the routing rules can be tested
|
|
5
|
+
* without mounting a plugin or reaching a network.
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-reference-anything/cloud-drive/registry
|
|
8
|
+
*/
|
|
9
|
+
import type { DriveKind, DriveProvider, DriveProviderOptions } from './types.ts';
|
|
10
|
+
/** Every drive the package supports. */
|
|
11
|
+
export declare const DRIVE_KINDS: readonly DriveKind[];
|
|
12
|
+
/**
|
|
13
|
+
* Constructors for the drives that actually work.
|
|
14
|
+
*
|
|
15
|
+
* `Partial` keeps startup validation explicit if a future declared drive has no
|
|
16
|
+
* implementation.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DRIVE_PROVIDERS: Partial<Record<DriveKind, (options?: DriveProviderOptions) => DriveProvider>>;
|
|
19
|
+
/**
|
|
20
|
+
* Build the provider for one drive.
|
|
21
|
+
*
|
|
22
|
+
* @param kind - which drive.
|
|
23
|
+
* @param options - transport and clock seams passed through to the provider.
|
|
24
|
+
* @returns the provider, or `undefined` when that drive has no implementation.
|
|
25
|
+
*/
|
|
26
|
+
export declare function providerFor(kind: DriveKind, options?: DriveProviderOptions): DriveProvider | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Compose the `id` half of a reference to one drive file.
|
|
29
|
+
*
|
|
30
|
+
* The drive name is carried in the id rather than in the source id so that all
|
|
31
|
+
* drives share one `ReferenceSource`, one grant gate, and one menu group.
|
|
32
|
+
*
|
|
33
|
+
* @param kind - which drive owns the file.
|
|
34
|
+
* @param fileId - the provider's own opaque id.
|
|
35
|
+
*/
|
|
36
|
+
export declare function encodeDriveId(kind: DriveKind, fileId: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Split a reference id back into a drive and the provider's file id.
|
|
39
|
+
*
|
|
40
|
+
* Splits on the *first* separator only: a provider's id may itself contain
|
|
41
|
+
* one.
|
|
42
|
+
*
|
|
43
|
+
* @param id - the `id` half of a `dsh-ref:` reference this source owns.
|
|
44
|
+
* @throws ReferenceAnythingError when the id names no known drive.
|
|
45
|
+
*/
|
|
46
|
+
export declare function decodeDriveId(id: string): {
|
|
47
|
+
kind: DriveKind;
|
|
48
|
+
fileId: string;
|
|
49
|
+
};
|
|
50
|
+
/** Whether a string is one of the declared drive names. */
|
|
51
|
+
export declare function isDriveKind(value: string): value is DriveKind;
|
|
52
|
+
/** Normalize absolute OpenList paths and reject traversal rather than resolving it. */
|
|
53
|
+
export declare function normalizeOpenListPath(value: string): string;
|