gitnexus 1.6.8-rc.13 → 1.6.8-rc.14
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/dist/server/analyze-launch.d.ts +29 -0
- package/dist/server/analyze-launch.js +137 -0
- package/dist/server/analyze-upload.d.ts +33 -0
- package/dist/server/analyze-upload.js +123 -0
- package/dist/server/analyze-worker.d.ts +17 -1
- package/dist/server/api.js +46 -140
- package/dist/server/git-clone.d.ts +1 -0
- package/dist/server/git-clone.js +1 -1
- package/dist/server/middleware.d.ts +11 -0
- package/dist/server/middleware.js +27 -0
- package/dist/server/upload-ingest.d.ts +56 -0
- package/dist/server/upload-ingest.js +276 -0
- package/dist/server/upload-paths.d.ts +31 -0
- package/dist/server/upload-paths.js +51 -0
- package/dist/server/upload-sweep.d.ts +20 -0
- package/dist/server/upload-sweep.js +57 -0
- package/package.json +3 -1
- package/web/assets/{agent-CKPMqImC.js → agent-B0-_Ihgi.js} +1 -1
- package/web/assets/{architectureDiagram-UL44E2DR-D1EXI0zA.js → architectureDiagram-UL44E2DR-PdDPNXTO.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-u4-0IIru.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV--R3bV0KH.js} +1 -1
- package/web/assets/{classDiagram-KGZ6W3CR-Bv93af_b.js → classDiagram-KGZ6W3CR-CLDcI2Tc.js} +1 -1
- package/web/assets/{classDiagram-v2-72OJOZXJ-CLfEqHUa.js → classDiagram-v2-72OJOZXJ-dwefssU_.js} +1 -1
- package/web/assets/{diagram-3NCE3AQN-BqAtKUpW.js → diagram-3NCE3AQN-DMYhnaiQ.js} +1 -1
- package/web/assets/{diagram-GF46GFSD-CaBG6n6o.js → diagram-GF46GFSD-CgWD3Go0.js} +1 -1
- package/web/assets/{diagram-QXG6HAR7-CZ-O3rcV.js → diagram-QXG6HAR7-Bwu9Wi_f.js} +1 -1
- package/web/assets/{diagram-WEQXMOUZ-P4lSL4GH.js → diagram-WEQXMOUZ-CaxeST-3.js} +1 -1
- package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-WlblWt40.js} +1 -1
- package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-CRrYkD90.js} +1 -1
- package/web/assets/index-COMMmbxW.css +2 -0
- package/web/assets/index-D_6rUFGW.js +626 -0
- package/web/assets/{infoDiagram-3YFTVSEB-DmLICZx1.js → infoDiagram-3YFTVSEB-pvT6ArHz.js} +1 -1
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-D_0fBKRi.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-DGABptDC.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-r-bFdhPw.js} +1 -1
- package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-CS3o1Ug4.js} +1 -1
- package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-BMYGXwjH.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-BJnTiLwO.js} +1 -1
- package/web/assets/{stateDiagram-7D4R322I-CB2nABwH.js → stateDiagram-7D4R322I-G35bS8CQ.js} +1 -1
- package/web/assets/{stateDiagram-v2-36443NZ5-COBGd2RL.js → stateDiagram-v2-36443NZ5-BUaZpLs6.js} +1 -1
- package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-B9o1j771.js} +1 -1
- package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-Vz5Sin3H.js} +1 -1
- package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-D42Nu4nI.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-CSImZ1y9.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-BKWA-m7o.css +0 -2
- package/web/assets/index-CG6q8eTs.js +0 -626
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure ingestion of a browser folder upload (multipart/form-data).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the path-injection-prone GET /api/fs/list directory listing. The
|
|
5
|
+
* client streams the selected files plus a JSON `manifest` of their
|
|
6
|
+
* webkitRelativePaths; we write each into an mkdtemp staging dir under
|
|
7
|
+
* UPLOAD_ROOT with PROVABLE containment (resolve-then-contain), hard resource
|
|
8
|
+
* caps, and guaranteed cleanup on every failure/abort path. No client value
|
|
9
|
+
* ever reaches a filesystem READ — the server only writes into a sandbox it
|
|
10
|
+
* created, then hands that sandbox to the analysis pipeline.
|
|
11
|
+
*
|
|
12
|
+
* Security references: CodeQL js/path-injection (resolve + startsWith(root+sep)),
|
|
13
|
+
* OWASP File Upload / Path Traversal.
|
|
14
|
+
*/
|
|
15
|
+
import type { IncomingMessage } from 'http';
|
|
16
|
+
export interface IngestLimits {
|
|
17
|
+
/** Aggregate bytes across all files (busboy has no aggregate limit). */
|
|
18
|
+
maxTotalBytes: number;
|
|
19
|
+
/** Per-file byte cap. */
|
|
20
|
+
maxFileBytes: number;
|
|
21
|
+
/** Maximum number of files. */
|
|
22
|
+
maxFiles: number;
|
|
23
|
+
/** Maximum multipart parts (files + fields). */
|
|
24
|
+
maxParts: number;
|
|
25
|
+
/** Maximum directories created (inode-exhaustion guard). */
|
|
26
|
+
maxDirs: number;
|
|
27
|
+
/** Maximum size of the manifest field. */
|
|
28
|
+
maxFieldBytes: number;
|
|
29
|
+
}
|
|
30
|
+
export declare const DEFAULT_INGEST_LIMITS: IngestLimits;
|
|
31
|
+
export interface IngestResult {
|
|
32
|
+
/** Absolute path to the populated staging directory (realpath-canonical). */
|
|
33
|
+
stageRoot: string;
|
|
34
|
+
fileCount: number;
|
|
35
|
+
totalBytes: number;
|
|
36
|
+
/** First path segment shared by the uploaded tree (the picked folder). */
|
|
37
|
+
topLevelName: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a client-provided relative path to an absolute destination PROVABLY
|
|
41
|
+
* contained within `stageRoot`. Throws BadRequestError on any unsafe input.
|
|
42
|
+
* This is the load-bearing path-traversal-on-write control; keep it pure and
|
|
43
|
+
* unit-tested.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveContainedDest(stageRoot: string, rel: unknown): string;
|
|
46
|
+
export interface IngestOptions {
|
|
47
|
+
/** Override the staging parent dir (defaults to UPLOAD_ROOT; for tests). */
|
|
48
|
+
root?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse and securely write a multipart folder upload into a fresh staging
|
|
52
|
+
* directory under UPLOAD_ROOT. Resolves with the populated staging dir, or
|
|
53
|
+
* rejects with a BadRequestError (status 400/413) after removing the staging
|
|
54
|
+
* dir. The caller owns promotion + cleanup of the returned `stageRoot`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function ingestUpload(req: IncomingMessage, limitsOverride?: Partial<IngestLimits>, opts?: IngestOptions): Promise<IngestResult>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure ingestion of a browser folder upload (multipart/form-data).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the path-injection-prone GET /api/fs/list directory listing. The
|
|
5
|
+
* client streams the selected files plus a JSON `manifest` of their
|
|
6
|
+
* webkitRelativePaths; we write each into an mkdtemp staging dir under
|
|
7
|
+
* UPLOAD_ROOT with PROVABLE containment (resolve-then-contain), hard resource
|
|
8
|
+
* caps, and guaranteed cleanup on every failure/abort path. No client value
|
|
9
|
+
* ever reaches a filesystem READ — the server only writes into a sandbox it
|
|
10
|
+
* created, then hands that sandbox to the analysis pipeline.
|
|
11
|
+
*
|
|
12
|
+
* Security references: CodeQL js/path-injection (resolve + startsWith(root+sep)),
|
|
13
|
+
* OWASP File Upload / Path Traversal.
|
|
14
|
+
*/
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import fsp from 'fs/promises';
|
|
18
|
+
import busboy from 'busboy';
|
|
19
|
+
import { UPLOAD_ROOT, STAGING_PREFIX } from './upload-paths.js';
|
|
20
|
+
import { BadRequestError } from './validation.js';
|
|
21
|
+
export const DEFAULT_INGEST_LIMITS = {
|
|
22
|
+
maxTotalBytes: 250 * 1024 * 1024,
|
|
23
|
+
maxFileBytes: 25 * 1024 * 1024,
|
|
24
|
+
maxFiles: 20000,
|
|
25
|
+
maxParts: 20100,
|
|
26
|
+
maxDirs: 50000,
|
|
27
|
+
maxFieldBytes: 2 * 1024 * 1024,
|
|
28
|
+
};
|
|
29
|
+
const MAX_PATH_DEPTH = 64;
|
|
30
|
+
const MAX_PATH_LENGTH = 4096;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a client-provided relative path to an absolute destination PROVABLY
|
|
33
|
+
* contained within `stageRoot`. Throws BadRequestError on any unsafe input.
|
|
34
|
+
* This is the load-bearing path-traversal-on-write control; keep it pure and
|
|
35
|
+
* unit-tested.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveContainedDest(stageRoot, rel) {
|
|
38
|
+
if (typeof rel !== 'string' || rel.length === 0) {
|
|
39
|
+
throw new BadRequestError('Invalid upload path');
|
|
40
|
+
}
|
|
41
|
+
if (rel.length > MAX_PATH_LENGTH) {
|
|
42
|
+
throw new BadRequestError('Upload path too long');
|
|
43
|
+
}
|
|
44
|
+
// webkitRelativePath is always relative; a leading slash is absolute/hostile.
|
|
45
|
+
if (rel.startsWith('/')) {
|
|
46
|
+
throw new BadRequestError('Invalid upload path');
|
|
47
|
+
}
|
|
48
|
+
// Browsers emit forward slashes only; a NUL byte or backslash is hostile.
|
|
49
|
+
if (rel.includes('\u0000') || rel.includes('\\')) {
|
|
50
|
+
throw new BadRequestError('Invalid upload path');
|
|
51
|
+
}
|
|
52
|
+
const rawSegments = rel.split('/').filter((s) => s.length > 0);
|
|
53
|
+
if (rawSegments.length === 0 || rawSegments.length > MAX_PATH_DEPTH) {
|
|
54
|
+
throw new BadRequestError('Invalid upload path');
|
|
55
|
+
}
|
|
56
|
+
const segments = [];
|
|
57
|
+
for (const seg of rawSegments) {
|
|
58
|
+
// Normalize so NFC/NFD variants don't collide silently on case/unicode
|
|
59
|
+
// -folding filesystems (macOS/Windows).
|
|
60
|
+
const s = seg.normalize('NFC');
|
|
61
|
+
if (s === '.' || s === '..') {
|
|
62
|
+
throw new BadRequestError('Upload path must not contain traversal segments');
|
|
63
|
+
}
|
|
64
|
+
segments.push(s);
|
|
65
|
+
}
|
|
66
|
+
const dest = path.resolve(stageRoot, segments.join(path.sep));
|
|
67
|
+
// Suffix path.sep so a sibling prefix (/sandbox-evil vs /sandbox) can't pass.
|
|
68
|
+
if (dest !== stageRoot && !dest.startsWith(stageRoot + path.sep)) {
|
|
69
|
+
throw new BadRequestError('Upload path escapes the sandbox');
|
|
70
|
+
}
|
|
71
|
+
return dest;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create the parent directories of `destFile` one segment at a time, asserting
|
|
75
|
+
* after each `mkdir` that the segment is a real directory (not a symlink
|
|
76
|
+
* swapped in mid-stream) still inside `stageRoot`. Counts created dirs against
|
|
77
|
+
* the inode-exhaustion cap.
|
|
78
|
+
*/
|
|
79
|
+
function mkdirContained(stageRoot, destFile, state) {
|
|
80
|
+
const parent = path.dirname(destFile);
|
|
81
|
+
const relParent = path.relative(stageRoot, parent);
|
|
82
|
+
if (relParent === '' || relParent === '.')
|
|
83
|
+
return;
|
|
84
|
+
const segs = relParent.split(path.sep).filter(Boolean);
|
|
85
|
+
let cur = stageRoot;
|
|
86
|
+
for (const seg of segs) {
|
|
87
|
+
cur = path.join(cur, seg);
|
|
88
|
+
let made = false;
|
|
89
|
+
try {
|
|
90
|
+
fs.mkdirSync(cur);
|
|
91
|
+
made = true;
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err.code !== 'EEXIST')
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
const st = fs.lstatSync(cur);
|
|
98
|
+
if (st.isSymbolicLink() || !st.isDirectory()) {
|
|
99
|
+
throw new BadRequestError('Upload path escapes the sandbox');
|
|
100
|
+
}
|
|
101
|
+
if (made) {
|
|
102
|
+
state.dirCount++;
|
|
103
|
+
if (state.dirCount > state.limits.maxDirs) {
|
|
104
|
+
throw new BadRequestError('Too many directories in upload', 413);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Parse and securely write a multipart folder upload into a fresh staging
|
|
111
|
+
* directory under UPLOAD_ROOT. Resolves with the populated staging dir, or
|
|
112
|
+
* rejects with a BadRequestError (status 400/413) after removing the staging
|
|
113
|
+
* dir. The caller owns promotion + cleanup of the returned `stageRoot`.
|
|
114
|
+
*/
|
|
115
|
+
export async function ingestUpload(req, limitsOverride, opts = {}) {
|
|
116
|
+
const limits = { ...DEFAULT_INGEST_LIMITS, ...limitsOverride };
|
|
117
|
+
const uploadRoot = opts.root ?? UPLOAD_ROOT;
|
|
118
|
+
await fsp.mkdir(uploadRoot, { recursive: true });
|
|
119
|
+
// mkdtemp creates the dir mode 0o700 (owner-only); realpath canonicalizes
|
|
120
|
+
// the root so the containment prefix check is exact.
|
|
121
|
+
const stageRoot = await fsp.realpath(await fsp.mkdtemp(path.join(uploadRoot, STAGING_PREFIX)));
|
|
122
|
+
let cleaned = false;
|
|
123
|
+
const cleanup = async () => {
|
|
124
|
+
if (cleaned)
|
|
125
|
+
return;
|
|
126
|
+
cleaned = true;
|
|
127
|
+
await fsp.rm(stageRoot, { recursive: true, force: true }).catch(() => { });
|
|
128
|
+
};
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
let settled = false;
|
|
131
|
+
let manifest = null;
|
|
132
|
+
let fileIndex = 0;
|
|
133
|
+
let fileCount = 0;
|
|
134
|
+
let totalBytes = 0;
|
|
135
|
+
let topLevelName = '';
|
|
136
|
+
const dirState = { dirCount: 0, limits };
|
|
137
|
+
const writePromises = [];
|
|
138
|
+
const bb = busboy({
|
|
139
|
+
headers: req.headers,
|
|
140
|
+
limits: {
|
|
141
|
+
fileSize: limits.maxFileBytes,
|
|
142
|
+
files: limits.maxFiles,
|
|
143
|
+
parts: limits.maxParts,
|
|
144
|
+
fields: 10,
|
|
145
|
+
fieldNameSize: 200,
|
|
146
|
+
fieldSize: limits.maxFieldBytes,
|
|
147
|
+
headerPairs: 2000,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
const fail = (err) => {
|
|
151
|
+
if (settled)
|
|
152
|
+
return;
|
|
153
|
+
settled = true;
|
|
154
|
+
try {
|
|
155
|
+
req.unpipe(bb);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
/* ignore */
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
req.resume(); // drain remaining body so the socket isn't left hanging
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* ignore */
|
|
165
|
+
}
|
|
166
|
+
void cleanup().finally(() => reject(err));
|
|
167
|
+
};
|
|
168
|
+
bb.on('field', (name, val) => {
|
|
169
|
+
if (name !== 'manifest')
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(val);
|
|
173
|
+
if (!Array.isArray(parsed) || !parsed.every((p) => typeof p === 'string')) {
|
|
174
|
+
return fail(new BadRequestError('Invalid manifest'));
|
|
175
|
+
}
|
|
176
|
+
manifest = parsed;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
fail(new BadRequestError('Invalid manifest'));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
bb.on('file', (_name, stream, _info) => {
|
|
183
|
+
if (settled) {
|
|
184
|
+
stream.resume();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (manifest === null) {
|
|
188
|
+
// The manifest field MUST arrive before any file part.
|
|
189
|
+
stream.resume();
|
|
190
|
+
return fail(new BadRequestError('Manifest must precede file parts'));
|
|
191
|
+
}
|
|
192
|
+
const idx = fileIndex++;
|
|
193
|
+
const rel = manifest[idx];
|
|
194
|
+
let dest;
|
|
195
|
+
try {
|
|
196
|
+
dest = resolveContainedDest(stageRoot, rel);
|
|
197
|
+
// A folder upload is exactly one top-level directory: every entry must
|
|
198
|
+
// have ≥2 segments and share the same first segment. This rejects a
|
|
199
|
+
// bare file at the root (which would make the promote target a file)
|
|
200
|
+
// and a multi-top manifest (which would silently drop all but the
|
|
201
|
+
// first folder). Validated here, before any job is created.
|
|
202
|
+
const segs = String(rel)
|
|
203
|
+
.split('/')
|
|
204
|
+
.filter((s) => s.length > 0);
|
|
205
|
+
const firstSeg = (segs[0] ?? '').normalize('NFC');
|
|
206
|
+
if (!topLevelName) {
|
|
207
|
+
topLevelName = firstSeg;
|
|
208
|
+
}
|
|
209
|
+
if (segs.length < 2 || firstSeg !== topLevelName) {
|
|
210
|
+
throw new BadRequestError('Upload must be a single folder of files');
|
|
211
|
+
}
|
|
212
|
+
mkdirContained(stageRoot, dest, dirState);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
stream.resume();
|
|
216
|
+
return fail(err);
|
|
217
|
+
}
|
|
218
|
+
fileCount++;
|
|
219
|
+
const ws = fs.createWriteStream(dest, { flags: 'wx' });
|
|
220
|
+
const p = new Promise((res, rej) => {
|
|
221
|
+
stream.on('data', (chunk) => {
|
|
222
|
+
totalBytes += chunk.length;
|
|
223
|
+
if (totalBytes > limits.maxTotalBytes) {
|
|
224
|
+
stream.unpipe(ws);
|
|
225
|
+
ws.destroy();
|
|
226
|
+
rej(new BadRequestError('Upload exceeds total size limit', 413));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
stream.on('limit', () => {
|
|
230
|
+
ws.destroy();
|
|
231
|
+
rej(new BadRequestError('File exceeds size limit', 413));
|
|
232
|
+
});
|
|
233
|
+
stream.on('error', rej);
|
|
234
|
+
ws.on('error', rej);
|
|
235
|
+
ws.on('finish', () => res());
|
|
236
|
+
stream.pipe(ws);
|
|
237
|
+
});
|
|
238
|
+
writePromises.push(p);
|
|
239
|
+
p.catch(fail);
|
|
240
|
+
});
|
|
241
|
+
bb.on('filesLimit', () => fail(new BadRequestError('Too many files in upload', 413)));
|
|
242
|
+
bb.on('partsLimit', () => fail(new BadRequestError('Too many parts in upload', 413)));
|
|
243
|
+
bb.on('fieldsLimit', () => fail(new BadRequestError('Too many fields in upload')));
|
|
244
|
+
bb.on('error', (err) => fail(err instanceof Error ? err : new BadRequestError('Upload parse error')));
|
|
245
|
+
bb.on('close', () => {
|
|
246
|
+
if (settled)
|
|
247
|
+
return;
|
|
248
|
+
Promise.all(writePromises)
|
|
249
|
+
.then(() => {
|
|
250
|
+
if (settled)
|
|
251
|
+
return;
|
|
252
|
+
if (manifest === null)
|
|
253
|
+
return fail(new BadRequestError('Missing manifest'));
|
|
254
|
+
if (fileCount === 0)
|
|
255
|
+
return fail(new BadRequestError('Empty upload'));
|
|
256
|
+
if (fileCount !== manifest.length) {
|
|
257
|
+
return fail(new BadRequestError('Manifest/file count mismatch'));
|
|
258
|
+
}
|
|
259
|
+
if (!topLevelName) {
|
|
260
|
+
return fail(new BadRequestError('Could not determine upload folder name'));
|
|
261
|
+
}
|
|
262
|
+
settled = true;
|
|
263
|
+
resolve({ stageRoot, fileCount, totalBytes, topLevelName });
|
|
264
|
+
})
|
|
265
|
+
.catch(() => {
|
|
266
|
+
/* a write rejected → fail() already invoked via p.catch */
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
req.on('aborted', () => {
|
|
270
|
+
if (!settled)
|
|
271
|
+
fail(new BadRequestError('Upload aborted'));
|
|
272
|
+
});
|
|
273
|
+
req.on('error', (err) => fail(err instanceof Error ? err : new BadRequestError('Request error')));
|
|
274
|
+
req.pipe(bb);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload working-directory paths.
|
|
3
|
+
*
|
|
4
|
+
* Browser folder uploads are written into ~/.gitnexus/uploads/{name}/ — a
|
|
5
|
+
* sibling of the clone root (git-clone.ts CLONE_ROOT) — so an uploaded repo
|
|
6
|
+
* persists and behaves like a cloned one (the graph UI's /api/file reads its
|
|
7
|
+
* files after analysis, and DELETE /api/repo removes it). Staging happens in
|
|
8
|
+
* an mkdtemp dir *under* UPLOAD_ROOT so the promote rename stays on one
|
|
9
|
+
* filesystem and remains atomic (a rename from os.tmpdir() could trip EXDEV —
|
|
10
|
+
* the exact Docker case this feature targets; see bridge-db.ts for the same
|
|
11
|
+
* anchored-staging pattern).
|
|
12
|
+
*/
|
|
13
|
+
/** Root directory for all uploaded repositories. Targets must resolve inside this. */
|
|
14
|
+
export declare const UPLOAD_ROOT: string;
|
|
15
|
+
/** Prefix for per-upload staging directories created under UPLOAD_ROOT. */
|
|
16
|
+
export declare const STAGING_PREFIX = ".staging-";
|
|
17
|
+
/**
|
|
18
|
+
* Get the upload target directory for a repo name.
|
|
19
|
+
*
|
|
20
|
+
* Re-validates at the boundary (callers may derive the name from an untrusted
|
|
21
|
+
* manifest). Rejects `.`, `..`, the `'unknown'` sentinel that sanitizeRepoName
|
|
22
|
+
* emits for un-nameable inputs, names beginning with `.` (which would collide
|
|
23
|
+
* with the `.staging-` prefix), and anything outside the safe charset.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getUploadDir(repoName: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Derive a filesystem-safe upload directory name from the manifest's
|
|
28
|
+
* top-level folder. Returns null when the name is un-nameable (so the caller
|
|
29
|
+
* rejects with 400 rather than colliding everyone on `UPLOAD_ROOT/unknown`).
|
|
30
|
+
*/
|
|
31
|
+
export declare function deriveUploadName(topLevelName: string): string | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload working-directory paths.
|
|
3
|
+
*
|
|
4
|
+
* Browser folder uploads are written into ~/.gitnexus/uploads/{name}/ — a
|
|
5
|
+
* sibling of the clone root (git-clone.ts CLONE_ROOT) — so an uploaded repo
|
|
6
|
+
* persists and behaves like a cloned one (the graph UI's /api/file reads its
|
|
7
|
+
* files after analysis, and DELETE /api/repo removes it). Staging happens in
|
|
8
|
+
* an mkdtemp dir *under* UPLOAD_ROOT so the promote rename stays on one
|
|
9
|
+
* filesystem and remains atomic (a rename from os.tmpdir() could trip EXDEV —
|
|
10
|
+
* the exact Docker case this feature targets; see bridge-db.ts for the same
|
|
11
|
+
* anchored-staging pattern).
|
|
12
|
+
*/
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import os from 'os';
|
|
15
|
+
import { sanitizeRepoName } from '../storage/git.js';
|
|
16
|
+
import { REPO_NAME_PATTERN } from './git-clone.js';
|
|
17
|
+
/** Root directory for all uploaded repositories. Targets must resolve inside this. */
|
|
18
|
+
export const UPLOAD_ROOT = path.resolve(path.join(os.homedir(), '.gitnexus', 'uploads'));
|
|
19
|
+
/** Prefix for per-upload staging directories created under UPLOAD_ROOT. */
|
|
20
|
+
export const STAGING_PREFIX = '.staging-';
|
|
21
|
+
/**
|
|
22
|
+
* Get the upload target directory for a repo name.
|
|
23
|
+
*
|
|
24
|
+
* Re-validates at the boundary (callers may derive the name from an untrusted
|
|
25
|
+
* manifest). Rejects `.`, `..`, the `'unknown'` sentinel that sanitizeRepoName
|
|
26
|
+
* emits for un-nameable inputs, names beginning with `.` (which would collide
|
|
27
|
+
* with the `.staging-` prefix), and anything outside the safe charset.
|
|
28
|
+
*/
|
|
29
|
+
export function getUploadDir(repoName) {
|
|
30
|
+
if (!repoName ||
|
|
31
|
+
repoName === '.' ||
|
|
32
|
+
repoName === '..' ||
|
|
33
|
+
repoName === 'unknown' ||
|
|
34
|
+
repoName.startsWith('.') ||
|
|
35
|
+
!REPO_NAME_PATTERN.test(repoName)) {
|
|
36
|
+
throw new Error('Invalid repository name');
|
|
37
|
+
}
|
|
38
|
+
return path.join(UPLOAD_ROOT, repoName);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Derive a filesystem-safe upload directory name from the manifest's
|
|
42
|
+
* top-level folder. Returns null when the name is un-nameable (so the caller
|
|
43
|
+
* rejects with 400 rather than colliding everyone on `UPLOAD_ROOT/unknown`).
|
|
44
|
+
*/
|
|
45
|
+
export function deriveUploadName(topLevelName) {
|
|
46
|
+
const safe = sanitizeRepoName(topLevelName);
|
|
47
|
+
if (safe === 'unknown' || safe === '.' || safe === '..' || safe.startsWith('.')) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return safe;
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backstop cleanup for abandoned upload staging directories.
|
|
3
|
+
*
|
|
4
|
+
* A crashed/killed process can leave a `.staging-*` directory under
|
|
5
|
+
* UPLOAD_ROOT (the normal path removes it on success/failure/abort). This
|
|
6
|
+
* sweep, run once at server startup, removes staging dirs older than a
|
|
7
|
+
* threshold. Promoted upload dirs are persistent registered repos (like
|
|
8
|
+
* clones) and are NOT touched here — they are removed via DELETE /api/repo.
|
|
9
|
+
*/
|
|
10
|
+
export interface SweepOptions {
|
|
11
|
+
/** Remove staging dirs older than this (default 6h). */
|
|
12
|
+
maxAgeMs?: number;
|
|
13
|
+
/** Override the root to sweep (defaults to UPLOAD_ROOT; for tests). */
|
|
14
|
+
root?: string;
|
|
15
|
+
/** Clock injection for tests. */
|
|
16
|
+
now?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function sweepStaleUploads(opts?: SweepOptions): Promise<{
|
|
19
|
+
removed: string[];
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backstop cleanup for abandoned upload staging directories.
|
|
3
|
+
*
|
|
4
|
+
* A crashed/killed process can leave a `.staging-*` directory under
|
|
5
|
+
* UPLOAD_ROOT (the normal path removes it on success/failure/abort). This
|
|
6
|
+
* sweep, run once at server startup, removes staging dirs older than a
|
|
7
|
+
* threshold. Promoted upload dirs are persistent registered repos (like
|
|
8
|
+
* clones) and are NOT touched here — they are removed via DELETE /api/repo.
|
|
9
|
+
*/
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fsp from 'fs/promises';
|
|
12
|
+
import { UPLOAD_ROOT, STAGING_PREFIX } from './upload-paths.js';
|
|
13
|
+
export async function sweepStaleUploads(opts = {}) {
|
|
14
|
+
const maxAgeMs = opts.maxAgeMs ?? 6 * 60 * 60 * 1000;
|
|
15
|
+
const root = opts.root ?? UPLOAD_ROOT;
|
|
16
|
+
const now = opts.now ?? Date.now();
|
|
17
|
+
const removed = [];
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await fsp.readdir(root, { withFileTypes: true });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { removed }; // root does not exist yet — nothing to sweep
|
|
24
|
+
}
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (!entry.isDirectory())
|
|
27
|
+
continue;
|
|
28
|
+
const full = path.join(root, entry.name);
|
|
29
|
+
try {
|
|
30
|
+
const st = await fsp.stat(full);
|
|
31
|
+
if (now - st.mtimeMs <= maxAgeMs)
|
|
32
|
+
continue; // recent — keep
|
|
33
|
+
if (entry.name.startsWith(STAGING_PREFIX)) {
|
|
34
|
+
// Transient staging dir orphaned by a crash — always removable.
|
|
35
|
+
await fsp.rm(full, { recursive: true, force: true }).catch(() => { });
|
|
36
|
+
removed.push(full);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Promoted upload dir. A successfully-analyzed (registered) repo always
|
|
40
|
+
// has a `.gitnexus` index inside it; a stale promoted dir WITHOUT one is
|
|
41
|
+
// an orphan from an analysis that failed before registering — remove it.
|
|
42
|
+
const hasIndex = await fsp
|
|
43
|
+
.access(path.join(full, '.gitnexus'))
|
|
44
|
+
.then(() => true)
|
|
45
|
+
.catch(() => false);
|
|
46
|
+
if (!hasIndex) {
|
|
47
|
+
await fsp.rm(full, { recursive: true, force: true }).catch(() => { });
|
|
48
|
+
removed.push(full);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* stat race — skip */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { removed };
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.8-rc.
|
|
3
|
+
"version": "1.6.8-rc.14",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@ladybugdb/core": "^0.17.0",
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
61
61
|
"@scarf/scarf": "^1.4.0",
|
|
62
|
+
"busboy": "^1.6.0",
|
|
62
63
|
"cli-progress": "^3.12.0",
|
|
63
64
|
"commander": "^14.0.3",
|
|
64
65
|
"cors": "^2.8.5",
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
"uuid": "^14.0.0"
|
|
94
95
|
},
|
|
95
96
|
"devDependencies": {
|
|
97
|
+
"@types/busboy": "^1.5.4",
|
|
96
98
|
"@types/cli-progress": "^3.11.6",
|
|
97
99
|
"@types/cors": "^2.8.17",
|
|
98
100
|
"@types/express": "^5.0.6",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{r as e,t}from"./chunk-CilyBKbf.js";import{o as n,s as r}from"./index-
|
|
1
|
+
import{r as e,t}from"./chunk-CilyBKbf.js";import{o as n,s as r}from"./index-D_6rUFGW.js";import{buildDynamicSystemPrompt as i}from"./context-builder-_HS0v2ma.js";var a=Object.defineProperty,o=(e,t)=>{let n={};for(var r in e)a(n,r,{get:e[r],enumerable:!0});return t||a(n,Symbol.toStringTag,{value:`Module`}),n};function s(e){let t=Symbol.for(e);return{brand(n,r){let i=r?Symbol.for(`${e}.${r}`):t;class a extends n{[i]=!0;constructor(...e){super(...e)}static isInstance(e){return typeof e==`object`&&!!e&&i in e&&e[i]===!0}}return Object.defineProperty(a,`name`,{value:n.name}),a},sub(t){return s(`${e}.${t}`)},isInstance(e){return typeof e==`object`&&!!e&&t in e&&e[t]===!0}}}var c=s(`langchain`),l=o({ContextOverflowError:()=>m,LangChainError:()=>f,ModelAbortError:()=>p,addLangChainErrorFields:()=>u,ns:()=>d});function u(e,t){return e.lc_error_code=t,e.message=`${e.message}\n\nTroubleshooting URL: https://docs.langchain.com/oss/javascript/langchain/errors/${t}/\n`,e}var d=c.sub(`error`),f=class extends d.brand(Error){name=`LangChainError`;constructor(e){super(e),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},p=class extends d.brand(f,`model-abort`){name=`ModelAbortError`;partialOutput;constructor(e,t){super(e),this.partialOutput=t}},m=class e extends d.brand(f,`context-overflow`){name=`ContextOverflowError`;cause;constructor(e){super(e??`Input exceeded the model's context window.`)}static fromError(t){let n=new e(t.message);return n.cause=t,n}};function h(e){return!!(e&&typeof e==`object`&&`type`in e&&e.type===`tool_call`)}function g(e){return!!(e&&typeof e==`object`&&`toolCall`in e&&e.toolCall!=null&&typeof e.toolCall==`object`&&`id`in e.toolCall&&typeof e.toolCall.id==`string`)}var _=class extends Error{output;constructor(e,t){super(e),this.output=t}};function v(e,t=b){e=e.trim();let n=e.indexOf("```");if(n===-1)return t(e);let r=e.substring(n+3);r.startsWith(`json
|
|
2
2
|
`)?r=r.substring(5):r.startsWith(`json`)?r=r.substring(4):r.startsWith(`
|
|
3
3
|
`)&&(r=r.substring(1));let i=r.indexOf("```"),a=r;return i!==-1&&(a=r.substring(0,i)),t(a.trim())}function y(e){try{return JSON.parse(e)}catch{}let t=e.trim();if(t.length===0)throw Error(`Unexpected end of JSON input`);let n=0;function r(){for(;n<t.length&&/\s/.test(t[n]);)n+=1}function i(){if(t[n]!==`"`)throw Error(`Expected '"' at position ${n}, got '${t[n]}'`);n+=1;let e=``,r=!1;for(;n<t.length;){let i=t[n];if(r){if(i===`n`)e+=`
|
|
4
4
|
`;else if(i===`t`)e+=` `;else if(i===`r`)e+=`\r`;else if(i===`\\`)e+=`\\`;else if(i===`"`)e+=`"`;else if(i===`b`)e+=`\b`;else if(i===`f`)e+=`\f`;else if(i===`/`)e+=`/`;else if(i===`u`){let r=t.substring(n+1,n+5);if(/^[0-9A-Fa-f]{0,4}$/.test(r))r.length===4?e+=String.fromCharCode(Number.parseInt(r,16)):e+=`u${r}`,n+=r.length;else throw Error(`Invalid unicode escape sequence '\\u${r}' at position ${n}`)}else throw Error(`Invalid escape sequence '\\${i}' at position ${n}`);r=!1}else if(i===`\\`)r=!0;else if(i===`"`)return n+=1,e;else e+=i;n+=1}return r&&(e+=`\\`),e}function a(){let e=n,r=``;if(t[n]===`-`&&(r+=`-`,n+=1),n<t.length&&t[n]===`0`&&(r+=`0`,n+=1,t[n]>=`0`&&t[n]<=`9`))throw Error(`Invalid number at position ${e}`);if(n<t.length&&t[n]>=`1`&&t[n]<=`9`)for(;n<t.length&&t[n]>=`0`&&t[n]<=`9`;)r+=t[n],n+=1;if(n<t.length&&t[n]===`.`)for(r+=`.`,n+=1;n<t.length&&t[n]>=`0`&&t[n]<=`9`;)r+=t[n],n+=1;if(n<t.length&&(t[n]===`e`||t[n]===`E`))for(r+=t[n],n+=1,n<t.length&&(t[n]===`+`||t[n]===`-`)&&(r+=t[n],n+=1);n<t.length&&t[n]>=`0`&&t[n]<=`9`;)r+=t[n],n+=1;if(r===`-`)return-0;let i=Number.parseFloat(r);if(Number.isNaN(i))throw n=e,Error(`Invalid number '${r}' at position ${e}`);return i}function o(){if(r(),n>=t.length)throw Error(`Unexpected end of input at position ${n}`);let e=t[n];if(e===`{`)return c();if(e===`[`)return s();if(e===`"`)return i();if(`null`.startsWith(t.substring(n,n+4)))return n+=Math.min(4,t.length-n),null;if(`true`.startsWith(t.substring(n,n+4)))return n+=Math.min(4,t.length-n),!0;if(`false`.startsWith(t.substring(n,n+5)))return n+=Math.min(5,t.length-n),!1;if(e===`-`||e>=`0`&&e<=`9`)return a();throw Error(`Unexpected character '${e}' at position ${n}`)}function s(){if(t[n]!==`[`)throw Error(`Expected '[' at position ${n}, got '${t[n]}'`);let e=[];if(n+=1,r(),n>=t.length)return e;if(t[n]===`]`)return n+=1,e;for(;n<t.length;){if(r(),n>=t.length||(e.push(o()),r(),n>=t.length))return e;if(t[n]===`]`)return n+=1,e;if(t[n]===`,`){n+=1;continue}throw Error(`Expected ',' or ']' at position ${n}, got '${t[n]}'`)}return e}function c(){if(t[n]!==`{`)throw Error(`Expected '{' at position ${n}, got '${t[n]}'`);let e={};if(n+=1,r(),n>=t.length)return e;if(t[n]===`}`)return n+=1,e;for(;n<t.length;){if(r(),n>=t.length)return e;let a=i();if(r(),n>=t.length)return e;if(t[n]!==`:`)throw Error(`Expected ':' at position ${n}, got '${t[n]}'`);if(n+=1,r(),n>=t.length||(e[a]=o(),r(),n>=t.length))return e;if(t[n]===`}`)return n+=1,e;if(t[n]===`,`){n+=1;continue}throw Error(`Expected ',' or '}' at position ${n}, got '${t[n]}'`)}return e}let l=o();if(r(),n<t.length)throw Error(`Unexpected character '${t[n]}' at position ${n}`);return l}function b(e){try{return e===void 0?null:y(e)}catch{return null}}var ee=/([A-Z]+)([A-Z][a-z0-9]+)/g,te=/([a-z0-9])([A-Z])/g,ne=/[-_\s]+/g;function x(e){return e.replace(ee,`$1_$2`).replace(te,`$1_$2`).replace(ne,`_`).toLowerCase()}function re(e){let t=e.trim();return/[-_\s]/.test(t)?t.replace(ne,`_`).toLowerCase().replace(/_+([a-z0-9])/g,(e,t)=>t.toUpperCase()):t}function ie(e,t){return t?.[e]||x(e)}function ae(e,t){return t?.[e]||re(e)}function oe(e,t,n){let r={};for(let i in e)Object.hasOwn(e,i)&&(r[t(i,n)]=e[i]);return r}var se=`__lc_escaped__`;function ce(e){return`lc`in e||Object.keys(e).length===1&&`__lc_escaped__`in e}function le(e){return{[se]:e}}function S(e){return Object.keys(e).length===1&&`__lc_escaped__`in e}function ue(e){return typeof e==`object`&&!!e&&`lc_serializable`in e&&typeof e.toJSON==`function`}function de(e){let t;return t=typeof e==`object`&&e?`lc_id`in e&&Array.isArray(e.lc_id)?e.lc_id:[e.constructor?.name??`Object`]:[typeof e],{lc:1,type:`not_implemented`,id:t}}function fe(e,t=new WeakSet){if(typeof e==`object`&&e&&!Array.isArray(e)){if(t.has(e))return de(e);if(ue(e))return e;t.add(e);let n=e;if(ce(n))return t.delete(e),le(n);let r={};for(let[e,i]of Object.entries(n))r[e]=fe(i,t);return t.delete(e),r}return Array.isArray(e)?e.map(e=>fe(e,t)):e}function pe(e){if(typeof e==`object`&&e&&!Array.isArray(e)){let t=e;if(S(t))return t[se];let n={};for(let[e,r]of Object.entries(t))n[e]=pe(r);return n}return Array.isArray(e)?e.map(e=>pe(e)):e}var me=o({Serializable:()=>ve,get_lc_unique_name:()=>_e});function he(e){return Array.isArray(e)?[...e]:{...e}}function ge(e,t){let n=he(e);for(let[e,r]of Object.entries(t)){let[t,...i]=e.split(`.`).reverse(),a=n;for(let e of i.reverse()){if(a[e]===void 0)break;a[e]=he(a[e]),a=a[e]}a[t]!==void 0&&(a[t]={lc:1,type:`secret`,id:[r]})}return n}function _e(e){let t=Object.getPrototypeOf(e);return typeof e.lc_name==`function`&&(typeof t.lc_name!=`function`||e.lc_name()!==t.lc_name())?e.lc_name():e.name}var ve=class e{lc_serializable=!1;lc_kwargs;static lc_name(){return this.name}get lc_id(){return[...this.lc_namespace,_e(this.constructor)]}get lc_secrets(){}get lc_attributes(){}get lc_aliases(){}get lc_serializable_keys(){}constructor(e,...t){this.lc_serializable_keys===void 0?this.lc_kwargs=e??{}:this.lc_kwargs=Object.fromEntries(Object.entries(e||{}).filter(([e])=>this.lc_serializable_keys?.includes(e)))}toJSON(){if(!this.lc_serializable||this.lc_kwargs instanceof e||typeof this.lc_kwargs!=`object`||Array.isArray(this.lc_kwargs))return this.toJSONNotImplemented();let t={},n={},r=Object.keys(this.lc_kwargs).reduce((e,t)=>(e[t]=t in this?this[t]:this.lc_kwargs[t],e),{});for(let e=Object.getPrototypeOf(this);e;e=Object.getPrototypeOf(e))Object.assign(t,Reflect.get(e,`lc_aliases`,this)),Object.assign(n,Reflect.get(e,`lc_secrets`,this)),Object.assign(r,Reflect.get(e,`lc_attributes`,this));Object.keys(n).forEach(e=>{let t=this,n=r,[i,...a]=e.split(`.`).reverse();for(let e of a.reverse()){if(!(e in t)||t[e]===void 0)return;(!(e in n)||n[e]===void 0)&&(typeof t[e]==`object`&&t[e]!=null?n[e]={}:Array.isArray(t[e])&&(n[e]=[])),t=t[e],n=n[e]}i in t&&t[i]!==void 0&&(n[i]=n[i]||t[i])});let i={},a=new WeakSet;a.add(this);for(let[e,t]of Object.entries(r))i[e]=fe(t,a);let o=oe(Object.keys(n).length?ge(i,n):i,ie,t);return{lc:1,type:`constructor`,id:this.lc_id,kwargs:o}}toJSONNotImplemented(){return{lc:1,type:`not_implemented`,id:this.lc_id}}};function ye(e){return typeof e==`object`&&!!e&&`type`in e&&typeof e.type==`string`&&`source_type`in e&&(e.source_type===`url`||e.source_type===`base64`||e.source_type===`text`||e.source_type===`id`)}function be(e){return ye(e)&&e.source_type===`url`&&`url`in e&&typeof e.url==`string`}function xe(e){return ye(e)&&e.source_type===`base64`&&`data`in e&&typeof e.data==`string`}function Se(e){return ye(e)&&e.source_type===`text`&&`text`in e&&typeof e.text==`string`}function Ce(e){return ye(e)&&e.source_type===`id`&&`id`in e&&typeof e.id==`string`}function we(e){if(ye(e)){if(e.source_type===`url`)return{type:`image_url`,image_url:{url:e.url}};if(e.source_type===`base64`){if(!e.mime_type)throw Error(`mime_type key is required for base64 data.`);return{type:`image_url`,image_url:{url:`data:${e.mime_type};base64,${e.data}`}}}}throw Error(`Unsupported source type. Only 'url' and 'base64' are supported.`)}function Te(e){let t=e.split(`;`)[0].split(`/`);if(t.length!==2)throw Error(`Invalid mime type: "${e}" - does not match type/subtype format.`);let n=t[0].trim(),r=t[1].trim();if(n===``||r===``)throw Error(`Invalid mime type: "${e}" - type or subtype is empty.`);let i={};for(let t of e.split(`;`).slice(1)){let n=t.split(`=`);if(n.length!==2)throw Error(`Invalid parameter syntax in mime type: "${e}".`);let r=n[0].trim(),a=n[1].trim();if(r===``)throw Error(`Invalid parameter syntax in mime type: "${e}".`);i[r]=a}return{type:n,subtype:r,parameters:i}}function Ee({dataUrl:e,asTypedArray:t=!1}){let n=e.match(/^data:(\w+\/\w+);base64,([A-Za-z0-9+/]+=*)$/),r;if(n){r=n[1].toLowerCase();let e=t?Uint8Array.from(atob(n[2]),e=>e.charCodeAt(0)):n[2];return{mime_type:r,data:e}}}function De(e,t){if(e.type===`text`){if(!t.fromStandardTextBlock)throw Error(`Converter for ${t.providerName} does not implement \`fromStandardTextBlock\` method.`);return t.fromStandardTextBlock(e)}if(e.type===`image`){if(!t.fromStandardImageBlock)throw Error(`Converter for ${t.providerName} does not implement \`fromStandardImageBlock\` method.`);return t.fromStandardImageBlock(e)}if(e.type===`audio`){if(!t.fromStandardAudioBlock)throw Error(`Converter for ${t.providerName} does not implement \`fromStandardAudioBlock\` method.`);return t.fromStandardAudioBlock(e)}if(e.type===`file`){if(!t.fromStandardFileBlock)throw Error(`Converter for ${t.providerName} does not implement \`fromStandardFileBlock\` method.`);return t.fromStandardFileBlock(e)}throw Error(`Unable to convert content block type '${e.type}' to provider-specific format: not recognized.`)}function C(e,t){return w(e)&&e.type===t}function w(e){return typeof e==`object`&&!!e}function Oe(e){return Array.isArray(e)}function T(e){return typeof e==`string`}function ke(e){return typeof e==`number`}function E(e){return e instanceof Uint8Array}function Ae(e){try{return JSON.parse(e)}catch{return}}var je=e=>e();function Me(e){if(e.type===`char_location`&&T(e.document_title)&&ke(e.start_char_index)&&ke(e.end_char_index)&&T(e.cited_text)){let{document_title:t,start_char_index:n,end_char_index:r,cited_text:i,...a}=e;return{...a,type:`citation`,source:`char`,title:t??void 0,startIndex:n,endIndex:r,citedText:i}}if(e.type===`page_location`&&T(e.document_title)&&ke(e.start_page_number)&&ke(e.end_page_number)&&T(e.cited_text)){let{document_title:t,start_page_number:n,end_page_number:r,cited_text:i,...a}=e;return{...a,type:`citation`,source:`page`,title:t??void 0,startIndex:n,endIndex:r,citedText:i}}if(e.type===`content_block_location`&&T(e.document_title)&&ke(e.start_block_index)&&ke(e.end_block_index)&&T(e.cited_text)){let{document_title:t,start_block_index:n,end_block_index:r,cited_text:i,...a}=e;return{...a,type:`citation`,source:`block`,title:t??void 0,startIndex:n,endIndex:r,citedText:i}}if(e.type===`web_search_result_location`&&T(e.url)&&T(e.title)&&T(e.encrypted_index)&&T(e.cited_text)){let{url:t,title:n,encrypted_index:r,cited_text:i,...a}=e;return{...a,type:`citation`,source:`url`,url:t,title:n,startIndex:Number(r),endIndex:Number(r),citedText:i}}if(e.type===`search_result_location`&&T(e.source)&&T(e.title)&&ke(e.start_block_index)&&ke(e.end_block_index)&&T(e.cited_text)){let{source:t,title:n,start_block_index:r,end_block_index:i,cited_text:a,...o}=e;return{...o,type:`citation`,source:`search`,url:t,title:n??void 0,startIndex:r,endIndex:i,citedText:a}}}function Ne(e){if(C(e,`document`)&&w(e.source)&&`type`in e.source){if(e.source.type===`base64`&&T(e.source.media_type)&&T(e.source.data))return{type:`file`,mimeType:e.source.media_type,data:e.source.data};if(e.source.type===`url`&&T(e.source.url))return{type:`file`,url:e.source.url};if(e.source.type===`file`&&T(e.source.file_id))return{type:`file`,fileId:e.source.file_id};if(e.source.type===`text`&&T(e.source.data))return{type:`file`,mimeType:String(e.source.media_type??`text/plain`),data:e.source.data}}else if(C(e,`image`)&&w(e.source)&&`type`in e.source){if(e.source.type===`base64`&&T(e.source.media_type)&&T(e.source.data))return{type:`image`,mimeType:e.source.media_type,data:e.source.data};if(e.source.type===`url`&&T(e.source.url))return{type:`image`,url:e.source.url};if(e.source.type===`file`&&T(e.source.file_id))return{type:`image`,fileId:e.source.file_id}}}function Pe(e){function*t(){for(let t of e){let e=Ne(t);e?yield e:yield t}}return Array.from(t())}function Fe(e){function*t(){let t=typeof e.content==`string`?[{type:`text`,text:e.content}]:e.content;for(let n of t){if(C(n,`text`)&&T(n.text)){let{text:e,citations:t,...r}=n;if(Oe(t)&&t.length){let n=t.reduce((e,t)=>{let n=Me(t);return n?[...e,n]:e},[]);yield{...r,type:`text`,text:e,annotations:n};continue}else{yield{...r,type:`text`,text:e};continue}}else if(C(n,`thinking`)&&T(n.thinking)){let{thinking:e,signature:t,...r}=n;yield{...r,type:`reasoning`,reasoning:e,signature:t};continue}else if(C(n,`redacted_thinking`)){yield{type:`non_standard`,value:n};continue}else if(C(n,`tool_use`)&&T(n.name)&&T(n.id)){yield{type:`tool_call`,id:n.id,name:n.name,args:n.input};continue}else if(C(n,`input_json_delta`)){if(Le(e)&&e.tool_call_chunks?.length){let t=e.tool_call_chunks[0];yield{type:`tool_call_chunk`,id:t.id,name:t.name,args:t.args,index:t.index};continue}}else if(C(n,`server_tool_use`)&&T(n.name)&&T(n.id)){let{name:e,id:t}=n;if(e===`web_search`){yield{id:t,type:`server_tool_call`,name:`web_search`,args:{query:je(()=>{if(typeof n.input==`string`)return n.input;if(w(n.input)&&T(n.input.query))return n.input.query;if(T(n.partial_json)){let e=Ae(n.partial_json);if(e?.query)return e.query}return``})}};continue}else if(n.name===`code_execution`){yield{id:t,type:`server_tool_call`,name:`code_execution`,args:{code:je(()=>{if(typeof n.input==`string`)return n.input;if(w(n.input)&&T(n.input.code))return n.input.code;if(T(n.partial_json)){let e=Ae(n.partial_json);if(e?.code)return e.code}return``})}};continue}}else if(C(n,`web_search_tool_result`)&&T(n.tool_use_id)&&Oe(n.content)){let{content:e,tool_use_id:t}=n;yield{type:`server_tool_call_result`,name:`web_search`,toolCallId:t,status:`success`,output:{urls:e.reduce((e,t)=>C(t,`web_search_result`)?[...e,t.url]:e,[])}};continue}else if(C(n,`code_execution_tool_result`)&&T(n.tool_use_id)&&w(n.content)){yield{type:`server_tool_call_result`,name:`code_execution`,toolCallId:n.tool_use_id,status:`success`,output:n.content};continue}else if(C(n,`mcp_tool_use`)){yield{id:n.id,type:`server_tool_call`,name:`mcp_tool_use`,args:n.input};continue}else if(C(n,`mcp_tool_result`)&&T(n.tool_use_id)&&w(n.content)){yield{type:`server_tool_call_result`,name:`mcp_tool_use`,toolCallId:n.tool_use_id,status:`success`,output:n.content};continue}else if(C(n,`container_upload`)){yield{type:`server_tool_call`,name:`container_upload`,args:n.input};continue}else if(C(n,`search_result`)){yield{id:n.id,type:`non_standard`,value:n};continue}else if(C(n,`tool_result`)){yield{id:n.id,type:`non_standard`,value:n};continue}else{let e=Ne(n);if(e){yield e;continue}}yield{type:`non_standard`,value:n}}}return Array.from(t())}var Ie={translateContent:Fe,translateContentChunk:Fe};function Le(e){return typeof e?._getType==`function`&&typeof e.concat==`function`&&e._getType()===`ai`}function Re(e){return be(e)?{type:e.type,mimeType:e.mime_type,url:e.url,metadata:e.metadata}:xe(e)?{type:e.type,mimeType:e.mime_type??`application/octet-stream`,data:e.data,metadata:e.metadata}:Ce(e)?{type:e.type,mimeType:e.mime_type,fileId:e.id,metadata:e.metadata}:e}function ze(e){return e.map(Re)}function Be(e){return!!(C(e,`image_url`)&&w(e.image_url)||C(e,`input_audio`)&&w(e.input_audio)||C(e,`file`)&&w(e.file))}function Ve(e){if(C(e,`image_url`)&&w(e.image_url)&&T(e.image_url.url)){let t=Ee({dataUrl:e.image_url.url});return t?{type:`image`,mimeType:t.mime_type,data:t.data}:{type:`image`,url:e.image_url.url}}else if(C(e,`input_audio`)&&w(e.input_audio)&&T(e.input_audio.data)&&T(e.input_audio.format))return{type:`audio`,data:e.input_audio.data,mimeType:`audio/${e.input_audio.format}`};else if(C(e,`file`)&&w(e.file)&&T(e.file.data)){let t=Ee({dataUrl:e.file.data});if(t)return{type:`file`,data:t.data,mimeType:t.mime_type};if(T(e.file.file_id))return{type:`file`,fileId:e.file.file_id}}return e}function He(e){let t=[];typeof e.content==`string`?e.content.length>0&&t.push({type:`text`,text:e.content}):t.push(...We(e.content));for(let n of e.tool_calls??[])t.push({type:`tool_call`,id:n.id,name:n.name,args:n.args});return t}function Ue(e){let t=[];typeof e.content==`string`?e.content.length>0&&t.push({type:`text`,text:e.content}):t.push(...We(e.content));for(let n of e.tool_calls??[])t.push({type:`tool_call`,id:n.id,name:n.name,args:n.args});return t}function We(e){let t=[];for(let n of e)Be(n)?t.push(Ve(n)):t.push(n);return t}function Ge(e){if(e.type===`url_citation`){let{url:t,title:n,start_index:r,end_index:i}=e;return{type:`citation`,url:t,title:n,startIndex:r,endIndex:i}}if(e.type===`file_citation`){let{file_id:t,filename:n,index:r}=e;return{type:`citation`,title:n,startIndex:r,endIndex:r,fileId:t}}return e}function Ke(e){function*t(){w(e.additional_kwargs?.reasoning)&&Oe(e.additional_kwargs.reasoning.summary)&&(yield{type:`reasoning`,reasoning:e.additional_kwargs.reasoning.summary.reduce((e,t)=>w(t)&&T(t.text)?`${e}${t.text}`:e,``)});let t=typeof e.content==`string`?[{type:`text`,text:e.content}]:e.content;for(let e of t)if(C(e,`text`)){let{text:t,annotations:n,phase:r,extras:i,...a}=e,o=w(i)?{...i}:{};T(r)&&(o.phase=r);let s=Object.keys(o).length>0?{extras:o}:{};Array.isArray(n)?yield{...a,...s,type:`text`,text:String(t),annotations:n.map(Ge)}:yield{...a,...s,type:`text`,text:String(t)}}for(let t of e.tool_calls??[])yield{type:`tool_call`,id:t.id,name:t.name,args:t.args};if(w(e.additional_kwargs)&&Oe(e.additional_kwargs.tool_outputs))for(let t of e.additional_kwargs.tool_outputs){if(C(t,`web_search_call`)){let e={};if(w(t.action)&&T(t.action.query)&&(e.query=t.action.query),yield{id:t.id,type:`server_tool_call`,name:`web_search`,args:e},t.status===`completed`||t.status===`failed`){let e={};w(t.action)&&(e.action=t.action),yield{type:`server_tool_call_result`,toolCallId:T(t.id)?t.id:``,status:t.status===`completed`?`success`:`error`,output:e}}continue}else if(C(t,`file_search_call`)){yield{id:t.id,type:`server_tool_call`,name:`file_search`,args:{queries:Oe(t.queries)?t.queries:[]}},(t.status===`completed`||t.status===`failed`)&&(yield{type:`server_tool_call_result`,toolCallId:T(t.id)?t.id:``,status:t.status===`completed`?`success`:`error`,output:Oe(t.results)?{results:t.results}:{}});continue}else if(C(t,`computer_call`)){yield{type:`non_standard`,value:t};continue}else if(C(t,`code_interpreter_call`)){if(T(t.code)&&(yield{id:t.id,type:`server_tool_call`,name:`code_interpreter`,args:{code:t.code}}),Oe(t.outputs)){let e=je(()=>{if(t.status!==`in_progress`){if(t.status===`completed`)return 0;if(t.status===`incomplete`)return 127;if(t.status!==`interpreting`&&t.status===`failed`)return 1}});for(let n of t.outputs)if(C(n,`logs`)){yield{type:`server_tool_call_result`,toolCallId:t.id??``,status:`success`,output:{type:`code_interpreter_output`,returnCode:e??0,stderr:[0,void 0].includes(e)?void 0:String(n.logs),stdout:[0,void 0].includes(e)?String(n.logs):void 0}};continue}}continue}else if(C(t,`mcp_call`)){yield{id:t.id,type:`server_tool_call`,name:`mcp_call`,args:t.input};continue}else if(C(t,`mcp_list_tools`)){yield{id:t.id,type:`server_tool_call`,name:`mcp_list_tools`,args:t.input};continue}else if(C(t,`mcp_approval_request`)){yield{type:`non_standard`,value:t};continue}else if(C(t,`tool_search_call`)){let e={};w(t.arguments)&&Object.assign(e,t.arguments);let n={};T(t.execution)&&(n.execution=t.execution),T(t.status)&&(n.status=t.status),T(t.call_id)&&(n.call_id=t.call_id),yield{id:T(t.id)?t.id:``,type:`server_tool_call`,name:`tool_search`,args:e,...Object.keys(n).length>0?{extras:n}:{}};continue}else if(C(t,`tool_search_output`)){let e={name:`tool_search`};T(t.execution)&&(e.execution=t.execution),yield{type:`server_tool_call_result`,toolCallId:T(t.id)?t.id:``,status:t.status===`completed`?`success`:t.status===`failed`?`error`:`success`,output:{tools:Oe(t.tools)?t.tools:[]},extras:e};continue}else if(C(t,`image_generation_call`)){T(t.result)&&(yield{type:`image`,mimeType:`image/png`,data:t.result,id:T(t.id)?t.id:void 0,metadata:{status:T(t.status)?t.status:void 0}}),yield{type:`non_standard`,value:t};continue}w(t)&&(yield{type:`non_standard`,value:t})}}return Array.from(t())}function qe(e){function*t(){yield*Ke(e);for(let t of e.tool_call_chunks??[])yield{type:`tool_call_chunk`,id:t.id,name:t.name,args:t.args}}return Array.from(t())}var Je={translateContent:e=>typeof e.content==`string`?He(e):Ke(e),translateContentChunk:e=>typeof e.content==`string`?Ue(e):qe(e)};function Ye(e){return typeof e==`object`&&!!e&&`type`in e&&`content`in e&&(typeof e.content==`string`||Array.isArray(e.content))}function Xe(e,t=`pretty`){return t===`pretty`?Ze(e):JSON.stringify(e)}function Ze(e){let t=[],n=` ${e.type.charAt(0).toUpperCase()+e.type.slice(1)} Message `,r=Math.floor((80-n.length)/2),i=`=`.repeat(r),a=n.length%2==0?i:`${i}=`;if(t.push(`${i}${n}${a}`),e.type===`ai`){let n=e;if(n.tool_calls&&n.tool_calls.length>0){t.push(`Tool Calls:`);for(let e of n.tool_calls){t.push(` ${e.name} (${e.id})`),t.push(` Call ID: ${e.id}`),t.push(` Args:`);for(let[n,r]of Object.entries(e.args))t.push(` ${n}: ${typeof r==`object`?JSON.stringify(r):r}`)}}}if(e.type===`tool`){let n=e;n.name&&t.push(`Name: ${n.name}`)}return typeof e.content==`string`&&e.content.trim()&&(t.length>1&&t.push(``),t.push(e.content)),t.join(`
|