@qoder-ai/qmind-cli 1.1.2 → 2.0.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/CHANGELOG.md +16 -0
- package/bin/cli.js +6 -1
- package/dist/qmind.js +1604 -1490
- package/package.json +5 -3
package/dist/qmind.js
CHANGED
|
@@ -3,17 +3,17 @@ import { z } from "zod";
|
|
|
3
3
|
import { Command, CommanderError, Option } from "commander";
|
|
4
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
5
5
|
import { chmod, copyFile, link, lstat, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
-
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
7
|
-
import { createReadStream, createWriteStream } from "node:fs";
|
|
8
6
|
import { homedir } from "node:os";
|
|
9
|
-
import
|
|
10
|
-
import picomatch from "picomatch";
|
|
11
|
-
import open from "open";
|
|
7
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
12
8
|
import lockfile from "proper-lockfile";
|
|
9
|
+
import open from "open";
|
|
10
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
13
11
|
import { Readable } from "node:stream";
|
|
14
12
|
import { pipeline } from "node:stream/promises";
|
|
15
13
|
import { createParser } from "eventsource-parser";
|
|
16
14
|
import { Agent, EnvHttpProxyAgent, request } from "undici";
|
|
15
|
+
import pLimit from "p-limit";
|
|
16
|
+
import picomatch from "picomatch";
|
|
17
17
|
import { decodeHTML } from "entities";
|
|
18
18
|
//#region ../sdk/src/errors.ts
|
|
19
19
|
var QMindError = class extends Error {
|
|
@@ -47,12 +47,12 @@ const credentialHeaders = /* @__PURE__ */ new Set([
|
|
|
47
47
|
"x-csrf-token",
|
|
48
48
|
"x-xsrf-token"
|
|
49
49
|
]);
|
|
50
|
-
function isRecord$
|
|
50
|
+
function isRecord$5(value) {
|
|
51
51
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
52
|
}
|
|
53
53
|
function nestedRecord(value, key) {
|
|
54
54
|
const nested = value[key];
|
|
55
|
-
return isRecord$
|
|
55
|
+
return isRecord$5(nested) ? nested : void 0;
|
|
56
56
|
}
|
|
57
57
|
function firstString(records, keys) {
|
|
58
58
|
for (const record of records) {
|
|
@@ -78,7 +78,7 @@ function errorCodeForStatus$1(status) {
|
|
|
78
78
|
}
|
|
79
79
|
function httpError(access, operation, profile, response) {
|
|
80
80
|
const body = profile.unwrapError(response.body);
|
|
81
|
-
const root = isRecord$
|
|
81
|
+
const root = isRecord$5(body) ? body : void 0;
|
|
82
82
|
const records = [
|
|
83
83
|
root,
|
|
84
84
|
root === void 0 ? void 0 : nestedRecord(root, "error"),
|
|
@@ -106,7 +106,7 @@ function httpError(access, operation, profile, response) {
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
function isAbortLike(cause) {
|
|
109
|
-
return isRecord$
|
|
109
|
+
return isRecord$5(cause) && (cause.name === "AbortError" && typeof cause.name === "string" || cause.code === "ABORT_ERR");
|
|
110
110
|
}
|
|
111
111
|
function transportError(access, operation, request, cause) {
|
|
112
112
|
if (cause instanceof QMindError) return cause;
|
|
@@ -121,7 +121,11 @@ function requestWasAborted(request) {
|
|
|
121
121
|
return request.signal?.aborted === true;
|
|
122
122
|
}
|
|
123
123
|
function assertWireRequestInvariant(request) {
|
|
124
|
-
|
|
124
|
+
for (const [field, value] of [
|
|
125
|
+
["connectTimeoutMs", request.connectTimeoutMs],
|
|
126
|
+
["idleTimeoutMs", request.idleTimeoutMs],
|
|
127
|
+
["timeoutMs", request.timeoutMs]
|
|
128
|
+
]) if (!Number.isFinite(value ?? 1) || (value ?? 1) <= 0) throw new QMindError("INVALID_ARGUMENT", `${field} must be a positive finite number`, { details: { field } });
|
|
125
129
|
if (request.destination.kind === "host") {
|
|
126
130
|
if (!request.destination.path.startsWith("/") || request.destination.path.startsWith("//")) throw new QMindError("HOST_POLICY_ERROR", "host destination must be a root-relative path", { details: { destination: request.destination.kind } });
|
|
127
131
|
return;
|
|
@@ -307,6 +311,10 @@ function normalizeQMindTaskRunStatus(status) {
|
|
|
307
311
|
}
|
|
308
312
|
//#endregion
|
|
309
313
|
//#region ../sdk/src/schemas.ts
|
|
314
|
+
function isRecord$4(value) {
|
|
315
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
316
|
+
}
|
|
317
|
+
const wirePageNumberSchema = z.number().int().nonnegative().optional();
|
|
310
318
|
const timestampSchema = z.union([z.string(), z.object({
|
|
311
319
|
nanos: z.number().int().optional(),
|
|
312
320
|
seconds: z.union([z.number(), z.string()])
|
|
@@ -347,26 +355,39 @@ const notebookSchema = z.object({
|
|
|
347
355
|
userId: z.string().default("")
|
|
348
356
|
});
|
|
349
357
|
const notebookListSchema = z.object({
|
|
350
|
-
currentPage:
|
|
351
|
-
notebooks: z.array(notebookSchema),
|
|
352
|
-
pageSize:
|
|
358
|
+
currentPage: wirePageNumberSchema,
|
|
359
|
+
notebooks: z.array(notebookSchema).nullish().transform((notebooks) => notebooks ?? []),
|
|
360
|
+
pageSize: wirePageNumberSchema,
|
|
353
361
|
totalSize: z.number().int().nonnegative().default(0)
|
|
354
362
|
});
|
|
355
|
-
const wikiDocumentSchema = z.
|
|
363
|
+
const wikiDocumentSchema = z.preprocess((value) => isRecord$4(value) ? {
|
|
364
|
+
...value,
|
|
365
|
+
contentHash: value.contentHash ?? value.content_hash,
|
|
366
|
+
contentUrl: value.contentUrl ?? value.content_url
|
|
367
|
+
} : value, z.object({
|
|
356
368
|
contentHash: z.string().default(""),
|
|
357
369
|
contentUrl: z.string().min(1),
|
|
358
370
|
language: z.string().default("")
|
|
359
|
-
});
|
|
360
|
-
const wikiBranchSchema = z.
|
|
371
|
+
}));
|
|
372
|
+
const wikiBranchSchema = z.preprocess((value) => isRecord$4(value) ? {
|
|
373
|
+
...value,
|
|
374
|
+
commitTimestamp: value.commitTimestamp ?? value.commit_timestamp,
|
|
375
|
+
knowledgeCount: value.knowledgeCount ?? value.knowledge_count,
|
|
376
|
+
repoUrl: value.repoUrl ?? value.repo_url,
|
|
377
|
+
resultVersion: value.resultVersion ?? value.result_version,
|
|
378
|
+
updatedAt: value.updatedAt ?? value.updated_at
|
|
379
|
+
} : value, z.object({
|
|
361
380
|
branch: z.string().min(1),
|
|
362
381
|
commitTimestamp: z.number().optional(),
|
|
363
|
-
documents: z.array(wikiDocumentSchema).
|
|
382
|
+
documents: z.array(wikiDocumentSchema).nullish().transform((documents) => documents ?? []),
|
|
383
|
+
exists: z.boolean().optional(),
|
|
364
384
|
knowledgeCount: z.number().int().nonnegative().default(0),
|
|
365
385
|
repo: z.string().min(1),
|
|
366
386
|
repoUrl: z.string().optional(),
|
|
367
|
-
resultVersion: z.string().default("")
|
|
368
|
-
|
|
369
|
-
|
|
387
|
+
resultVersion: z.string().default(""),
|
|
388
|
+
updatedAt: timestampSchema.optional()
|
|
389
|
+
}));
|
|
390
|
+
const wikiBranchListSchema = z.object({ branches: z.array(wikiBranchSchema).nullish().transform((branches) => branches ?? []) });
|
|
370
391
|
const cardLinkSchema = z.object({
|
|
371
392
|
description: z.string().optional(),
|
|
372
393
|
linkType: z.string().default(""),
|
|
@@ -374,6 +395,7 @@ const cardLinkSchema = z.object({
|
|
|
374
395
|
targetTitle: z.string().optional()
|
|
375
396
|
});
|
|
376
397
|
const cardSchema = z.object({
|
|
398
|
+
branch: z.string().optional(),
|
|
377
399
|
cardType: z.string().optional(),
|
|
378
400
|
category: z.string().optional(),
|
|
379
401
|
content: z.string().default(""),
|
|
@@ -382,6 +404,7 @@ const cardSchema = z.object({
|
|
|
382
404
|
id: z.string().min(1),
|
|
383
405
|
keywords: z.array(z.string()).default([]),
|
|
384
406
|
knowledgeId: z.string().optional(),
|
|
407
|
+
language: z.string().optional(),
|
|
385
408
|
links: z.array(cardLinkSchema).default([]),
|
|
386
409
|
notebookId: z.string().optional(),
|
|
387
410
|
orgId: z.string().optional(),
|
|
@@ -391,35 +414,58 @@ const cardSchema = z.object({
|
|
|
391
414
|
title: z.string().default(""),
|
|
392
415
|
updatedAt: timestampSchema.optional()
|
|
393
416
|
});
|
|
417
|
+
const cardCategoryCountSchema = z.object({
|
|
418
|
+
category: z.string(),
|
|
419
|
+
count: z.number().int().nonnegative()
|
|
420
|
+
});
|
|
421
|
+
const cardLanguageCountSchema = z.object({
|
|
422
|
+
count: z.number().int().nonnegative(),
|
|
423
|
+
language: z.string()
|
|
424
|
+
});
|
|
425
|
+
const cardAppliedFiltersSchema = z.object({
|
|
426
|
+
branch: z.string().optional(),
|
|
427
|
+
category: z.string().optional(),
|
|
428
|
+
language: z.string().optional(),
|
|
429
|
+
query: z.string().optional(),
|
|
430
|
+
repo: z.string().optional(),
|
|
431
|
+
source: z.string().optional()
|
|
432
|
+
});
|
|
394
433
|
const cardListSchema = z.object({
|
|
434
|
+
appliedFilters: cardAppliedFiltersSchema.optional(),
|
|
395
435
|
cards: z.array(cardSchema).nullish().transform((cards) => cards ?? []),
|
|
396
|
-
|
|
397
|
-
|
|
436
|
+
categoryCounts: z.array(cardCategoryCountSchema).nullish().transform((counts) => counts ?? []),
|
|
437
|
+
currentPage: wirePageNumberSchema,
|
|
438
|
+
languageCounts: z.array(cardLanguageCountSchema).nullish().transform((counts) => counts ?? []),
|
|
439
|
+
pageSize: wirePageNumberSchema,
|
|
398
440
|
totalSize: z.number().int().nonnegative().default(0)
|
|
399
441
|
});
|
|
400
442
|
const sourcePageInfoSchema = z.object({
|
|
401
|
-
currentPage:
|
|
402
|
-
lastPage:
|
|
403
|
-
nextPage:
|
|
443
|
+
currentPage: wirePageNumberSchema,
|
|
444
|
+
lastPage: wirePageNumberSchema,
|
|
445
|
+
nextPage: wirePageNumberSchema,
|
|
404
446
|
nextToken: z.string().optional(),
|
|
405
|
-
pageSize:
|
|
406
|
-
prevPage:
|
|
447
|
+
pageSize: wirePageNumberSchema,
|
|
448
|
+
prevPage: wirePageNumberSchema,
|
|
407
449
|
totalSize: z.number().int().nonnegative().optional()
|
|
408
450
|
});
|
|
409
451
|
const sourceSchema = z.object({
|
|
410
452
|
childrenCount: z.number().int().nonnegative().optional(),
|
|
411
453
|
createdAt: timestampSchema.optional(),
|
|
412
454
|
errorMessage: z.string().default(""),
|
|
455
|
+
filename: z.string().optional(),
|
|
413
456
|
hasChildren: z.boolean().optional(),
|
|
414
457
|
id: z.string().min(1),
|
|
415
458
|
isDir: z.boolean().default(false),
|
|
416
459
|
metadata: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
460
|
+
mimeType: z.string().optional(),
|
|
461
|
+
name: z.string().optional(),
|
|
417
462
|
notebookId: z.string().default(""),
|
|
418
463
|
orgId: z.string().default(""),
|
|
419
464
|
originUrl: z.string().optional(),
|
|
420
465
|
parentSourceId: z.string().default(""),
|
|
421
466
|
path: z.string().default(""),
|
|
422
467
|
sourceType: z.string().default(""),
|
|
468
|
+
size: z.number().int().nonnegative().optional(),
|
|
423
469
|
status: z.string().default(""),
|
|
424
470
|
title: z.string().default(""),
|
|
425
471
|
updatedAt: timestampSchema.optional(),
|
|
@@ -427,10 +473,10 @@ const sourceSchema = z.object({
|
|
|
427
473
|
userId: z.string().default("")
|
|
428
474
|
});
|
|
429
475
|
const sourceListSchema = z.object({
|
|
430
|
-
currentPage:
|
|
431
|
-
pageSize:
|
|
476
|
+
currentPage: wirePageNumberSchema,
|
|
477
|
+
pageSize: wirePageNumberSchema,
|
|
432
478
|
pages: sourcePageInfoSchema.optional(),
|
|
433
|
-
sources: z.array(sourceSchema),
|
|
479
|
+
sources: z.array(sourceSchema).nullish().transform((sources) => sources ?? []),
|
|
434
480
|
totalSize: z.number().int().nonnegative().optional()
|
|
435
481
|
});
|
|
436
482
|
const sourceTreeNodeSchema = z.lazy(() => z.object({
|
|
@@ -449,6 +495,7 @@ const sourceTreeSchema = z.object({
|
|
|
449
495
|
const sourceContentSchema = z.object({
|
|
450
496
|
content: z.string(),
|
|
451
497
|
contentType: z.string().default("text/markdown"),
|
|
498
|
+
size: z.number().int().nonnegative().optional(),
|
|
452
499
|
sourceId: z.string().default("")
|
|
453
500
|
});
|
|
454
501
|
const sourceImportResultSchema = z.object({
|
|
@@ -474,10 +521,39 @@ const imageUrlsSchema = z.object({
|
|
|
474
521
|
expiresAt: z.string().optional(),
|
|
475
522
|
urls: z.record(z.string(), httpsUrlSchema)
|
|
476
523
|
});
|
|
524
|
+
const notebookCitationSchema = z.preprocess((value) => isRecord$4(value) ? {
|
|
525
|
+
...value,
|
|
526
|
+
chunkId: value.chunkId ?? value.chunk_id,
|
|
527
|
+
chunkIndex: value.chunkIndex ?? value.chunk_index,
|
|
528
|
+
originUrl: value.originUrl ?? value.origin_url,
|
|
529
|
+
sourceId: value.sourceId ?? value.source_id,
|
|
530
|
+
sourceTitle: value.sourceTitle ?? value.source_title,
|
|
531
|
+
sourceUri: value.sourceUri ?? value.source_uri
|
|
532
|
+
} : value, z.object({
|
|
533
|
+
chunkId: z.string().default(""),
|
|
534
|
+
chunkIndex: z.number().int().nonnegative().default(0),
|
|
535
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
536
|
+
originUrl: z.string().optional(),
|
|
537
|
+
snippet: z.string().default(""),
|
|
538
|
+
sourceId: z.string().default(""),
|
|
539
|
+
sourceTitle: z.string().default(""),
|
|
540
|
+
sourceUri: z.string().default("")
|
|
541
|
+
}));
|
|
477
542
|
const chatMessageSchema = z.object({
|
|
543
|
+
chatSessionId: z.string().optional(),
|
|
544
|
+
citations: z.array(notebookCitationSchema).nullish().transform((citations) => citations === null ? [] : citations),
|
|
478
545
|
content: z.string().default(""),
|
|
546
|
+
createdAt: timestampSchema.optional(),
|
|
479
547
|
id: z.string().min(1),
|
|
480
|
-
|
|
548
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
549
|
+
model: z.string().optional(),
|
|
550
|
+
notebookId: z.string().optional(),
|
|
551
|
+
orgId: z.string().optional(),
|
|
552
|
+
role: z.string().default(""),
|
|
553
|
+
sceneType: z.string().optional(),
|
|
554
|
+
sequence: z.number().int().nonnegative().optional(),
|
|
555
|
+
updatedAt: timestampSchema.optional(),
|
|
556
|
+
userId: z.string().optional()
|
|
481
557
|
});
|
|
482
558
|
const streamWireEventSchema = z.object({
|
|
483
559
|
assistantMessage: chatMessageSchema.optional(),
|
|
@@ -522,16 +598,6 @@ const chatSessionSchema = z.object({
|
|
|
522
598
|
updatedAt: timestampSchema.optional(),
|
|
523
599
|
userId: z.string().default("")
|
|
524
600
|
});
|
|
525
|
-
const notebookCitationSchema = z.object({
|
|
526
|
-
chunkId: z.string().default(""),
|
|
527
|
-
chunkIndex: z.number().int().nonnegative().default(0),
|
|
528
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
529
|
-
originUrl: z.string().optional(),
|
|
530
|
-
snippet: z.string().default(""),
|
|
531
|
-
sourceId: z.string().default(""),
|
|
532
|
-
sourceTitle: z.string().default(""),
|
|
533
|
-
sourceUri: z.string().default("")
|
|
534
|
-
});
|
|
535
601
|
const notebookContextChunkSchema = z.object({
|
|
536
602
|
chunkId: z.string().default(""),
|
|
537
603
|
chunkIndex: z.number().int().nonnegative().default(0),
|
|
@@ -586,6 +652,7 @@ const taskRunSchema = z.object({
|
|
|
586
652
|
reportMarkdown: z.string().optional(),
|
|
587
653
|
sceneType: z.string().default(""),
|
|
588
654
|
scheduledTaskId: z.string().optional(),
|
|
655
|
+
sourceId: z.string().optional(),
|
|
589
656
|
startedAt: timestampSchema.optional(),
|
|
590
657
|
stats: z.record(z.string(), z.unknown()).optional(),
|
|
591
658
|
status: z.string().default(""),
|
|
@@ -598,9 +665,9 @@ const taskRunSchema = z.object({
|
|
|
598
665
|
status: normalizeQMindTaskRunStatus(value.status)
|
|
599
666
|
}));
|
|
600
667
|
const taskRunListSchema = z.object({
|
|
601
|
-
currentPage:
|
|
602
|
-
pageSize:
|
|
603
|
-
runs: z.array(taskRunSchema).
|
|
668
|
+
currentPage: wirePageNumberSchema,
|
|
669
|
+
pageSize: wirePageNumberSchema,
|
|
670
|
+
runs: z.array(taskRunSchema).nullish().transform((runs) => runs ?? []),
|
|
604
671
|
totalSize: z.number().int().nonnegative().default(0)
|
|
605
672
|
});
|
|
606
673
|
//#endregion
|
|
@@ -806,6 +873,8 @@ function taskRunBody(input) {
|
|
|
806
873
|
config: optionalRecord(input.config, "config"),
|
|
807
874
|
parentRunId: optionalTrimmedString(input.parentRunId, "parentRunId"),
|
|
808
875
|
sceneType: requiredString$2(input.sceneType, "sceneType"),
|
|
876
|
+
settings: optionalRecord(input.settings, "settings"),
|
|
877
|
+
sourceId: optionalTrimmedString(input.sourceId, "sourceId"),
|
|
809
878
|
scheduledTaskId: optionalTrimmedString(input.scheduledTaskId, "scheduledTaskId"),
|
|
810
879
|
trigger: optionalTrimmedString(input.trigger, "trigger")
|
|
811
880
|
});
|
|
@@ -826,6 +895,7 @@ function taskRunListPath(context, notebookId, input = {}) {
|
|
|
826
895
|
["scheduled_task_id", optionalTrimmedString(input.scheduledTaskId, "scheduledTaskId")],
|
|
827
896
|
["parent_run_id", optionalTrimmedString(input.parentRunId, "parentRunId")],
|
|
828
897
|
["scene_type", optionalTrimmedString(input.sceneType, "sceneType")],
|
|
898
|
+
["source_id", optionalTrimmedString(input.sourceId, "sourceId")],
|
|
829
899
|
["trigger", optionalTrimmedString(input.trigger, "trigger")],
|
|
830
900
|
["status", status],
|
|
831
901
|
["page", normalizedPage.page],
|
|
@@ -853,6 +923,13 @@ function callOptions(options) {
|
|
|
853
923
|
...options?.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
854
924
|
};
|
|
855
925
|
}
|
|
926
|
+
function streamCallOptions(options) {
|
|
927
|
+
return {
|
|
928
|
+
...callOptions(options),
|
|
929
|
+
...options?.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: options.connectTimeoutMs },
|
|
930
|
+
...options?.idleTimeoutMs === void 0 ? {} : { idleTimeoutMs: options.idleTimeoutMs }
|
|
931
|
+
};
|
|
932
|
+
}
|
|
856
933
|
function createAgentOperations(context) {
|
|
857
934
|
const createChatSession = (notebookId, input, options) => {
|
|
858
935
|
const notebook = requiredString$2(notebookId, "notebookId");
|
|
@@ -885,7 +962,7 @@ function createAgentOperations(context) {
|
|
|
885
962
|
},
|
|
886
963
|
idempotent: false,
|
|
887
964
|
method: "POST",
|
|
888
|
-
...
|
|
965
|
+
...streamCallOptions(options)
|
|
889
966
|
},
|
|
890
967
|
transport: context.transport
|
|
891
968
|
});
|
|
@@ -1056,8 +1133,7 @@ function createNotebookOperations(context) {
|
|
|
1056
1133
|
client: context.client,
|
|
1057
1134
|
description,
|
|
1058
1135
|
settings,
|
|
1059
|
-
title
|
|
1060
|
-
userId: context.userId.length === 0 ? void 0 : context.userId
|
|
1136
|
+
title
|
|
1061
1137
|
}),
|
|
1062
1138
|
callOptions: options,
|
|
1063
1139
|
capability: "notebooks.write",
|
|
@@ -1197,15 +1273,18 @@ function buildSourceTree(sources) {
|
|
|
1197
1273
|
function sourceBody(input) {
|
|
1198
1274
|
return compactUndefined({
|
|
1199
1275
|
isDir: optionalBoolean(input?.isDir, "isDir"),
|
|
1276
|
+
metadata: optionalRecord(input?.metadata, "metadata"),
|
|
1200
1277
|
parentSourceId: optionalString$1(input?.parentSourceId, "parentSourceId"),
|
|
1201
1278
|
path: optionalString$1(input?.path, "path"),
|
|
1202
1279
|
sourceType: optionalTrimmedString(input?.sourceType, "sourceType"),
|
|
1203
1280
|
status: optionalTrimmedString(input?.status, "status"),
|
|
1204
|
-
title: requiredString$2(input?.title, "title")
|
|
1281
|
+
title: requiredString$2(input?.title, "title"),
|
|
1282
|
+
uri: optionalString$1(input?.uri, "uri")
|
|
1205
1283
|
});
|
|
1206
1284
|
}
|
|
1207
1285
|
function sourcePatch(input) {
|
|
1208
1286
|
const patch = compactUndefined({
|
|
1287
|
+
content: optionalString$1(input?.content, "content"),
|
|
1209
1288
|
errorMessage: optionalString$1(input?.errorMessage, "errorMessage"),
|
|
1210
1289
|
isDir: optionalBoolean(input?.isDir, "isDir"),
|
|
1211
1290
|
metadata: optionalRecord(input?.metadata, "metadata"),
|
|
@@ -1271,6 +1350,7 @@ function createSourceOperations(context) {
|
|
|
1271
1350
|
const readSourcePage = async (notebookId, input = {}, options, operation = "sources.list") => {
|
|
1272
1351
|
const notebook = requiredString$2(notebookId, "notebookId");
|
|
1273
1352
|
const normalizedPage = pagination$1(input);
|
|
1353
|
+
const isDir = optionalBoolean(input.isDir, "isDir");
|
|
1274
1354
|
const view = input.view ?? "list";
|
|
1275
1355
|
if (view !== "list" && view !== "children") throw invalidArgument$1("view must be either \"list\" or \"children\"", "view");
|
|
1276
1356
|
const parentSourceId = optionalString$1(input.parentSourceId, "parentSourceId");
|
|
@@ -1286,6 +1366,7 @@ function createSourceOperations(context) {
|
|
|
1286
1366
|
operation,
|
|
1287
1367
|
path: queryPath(context.profile.apiPath("notebooks", notebook, "sources"), [
|
|
1288
1368
|
["view", view === "list" ? void 0 : view],
|
|
1369
|
+
["is_dir", isDir === void 0 ? void 0 : String(isDir)],
|
|
1289
1370
|
["parent_source_id", parentSourceId],
|
|
1290
1371
|
["page", normalizedPage.page],
|
|
1291
1372
|
["page_size", normalizedPage.pageSize],
|
|
@@ -1703,21 +1784,21 @@ function createTransferOperations(context) {
|
|
|
1703
1784
|
//#endregion
|
|
1704
1785
|
//#region ../sdk/src/capabilities.ts
|
|
1705
1786
|
const dashboardCapabilities = Object.freeze({
|
|
1706
|
-
botGateway:
|
|
1787
|
+
botGateway: false,
|
|
1707
1788
|
"cards.read": true,
|
|
1708
1789
|
"cards.write": true,
|
|
1709
1790
|
chat: true,
|
|
1710
|
-
"featuredNotebooks.read":
|
|
1791
|
+
"featuredNotebooks.read": false,
|
|
1711
1792
|
"images.presignUpload": false,
|
|
1712
1793
|
"images.presignUrls": false,
|
|
1713
|
-
members:
|
|
1794
|
+
members: false,
|
|
1714
1795
|
"notebooks.filterByClient": false,
|
|
1715
1796
|
"notebooks.read": true,
|
|
1716
1797
|
"notebooks.write": true,
|
|
1717
|
-
"notes.read":
|
|
1718
|
-
"notes.write":
|
|
1798
|
+
"notes.read": false,
|
|
1799
|
+
"notes.write": false,
|
|
1719
1800
|
retrieval: true,
|
|
1720
|
-
scheduledTasks:
|
|
1801
|
+
scheduledTasks: false,
|
|
1721
1802
|
"sources.children": true,
|
|
1722
1803
|
"sources.importWeb": true,
|
|
1723
1804
|
"sources.read": true,
|
|
@@ -1734,17 +1815,17 @@ const sashCapabilities = Object.freeze({
|
|
|
1734
1815
|
"cards.read": true,
|
|
1735
1816
|
"cards.write": true,
|
|
1736
1817
|
chat: true,
|
|
1737
|
-
"featuredNotebooks.read":
|
|
1818
|
+
"featuredNotebooks.read": false,
|
|
1738
1819
|
"images.presignUpload": true,
|
|
1739
1820
|
"images.presignUrls": true,
|
|
1740
|
-
members:
|
|
1821
|
+
members: false,
|
|
1741
1822
|
"notebooks.filterByClient": true,
|
|
1742
1823
|
"notebooks.read": true,
|
|
1743
1824
|
"notebooks.write": true,
|
|
1744
|
-
"notes.read":
|
|
1745
|
-
"notes.write":
|
|
1825
|
+
"notes.read": false,
|
|
1826
|
+
"notes.write": false,
|
|
1746
1827
|
retrieval: true,
|
|
1747
|
-
scheduledTasks:
|
|
1828
|
+
scheduledTasks: false,
|
|
1748
1829
|
"sources.children": false,
|
|
1749
1830
|
"sources.importWeb": false,
|
|
1750
1831
|
"sources.read": true,
|
|
@@ -1958,8 +2039,7 @@ function createQMindClient(options) {
|
|
|
1958
2039
|
const context = {
|
|
1959
2040
|
client: optionalTrimmedString(options.client, "client") ?? "qmind",
|
|
1960
2041
|
profile,
|
|
1961
|
-
transport: options.transport
|
|
1962
|
-
userId: optionalTrimmedString(options.userId, "userId") ?? ""
|
|
2042
|
+
transport: options.transport
|
|
1963
2043
|
};
|
|
1964
2044
|
return Object.freeze({
|
|
1965
2045
|
access: profile.access,
|
|
@@ -2024,7 +2104,7 @@ function requestsJsonOutput(argv) {
|
|
|
2024
2104
|
}
|
|
2025
2105
|
//#endregion
|
|
2026
2106
|
//#region src/build-info.ts
|
|
2027
|
-
const QMIND_CLI_VERSION = "
|
|
2107
|
+
const QMIND_CLI_VERSION = "2.0.0";
|
|
2028
2108
|
const QMIND_CLI_BUILD_TIME = "development";
|
|
2029
2109
|
const QMIND_CLI_UPDATE_BASE_URL = typeof __QMIND_CLI_UPDATE_BASE_URL__ === "string" ? __QMIND_CLI_UPDATE_BASE_URL__ : "https://qoder-ide-cn.oss-cn-hangzhou.aliyuncs.com/qmind/cli/";
|
|
2030
2110
|
function qmindCliPlatform() {
|
|
@@ -2034,1562 +2114,1598 @@ function qmindCliArchitecture() {
|
|
|
2034
2114
|
return process.arch === "x64" ? "amd64" : process.arch;
|
|
2035
2115
|
}
|
|
2036
2116
|
//#endregion
|
|
2037
|
-
//#region src/
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2117
|
+
//#region ../node-host/src/auth.ts
|
|
2118
|
+
const responseRecordSchema = z.record(z.string(), z.unknown());
|
|
2119
|
+
const personalTokenExchangeSchema = z.object({ token: z.string().min(1) });
|
|
2120
|
+
const devicePollSchema = z.object({
|
|
2121
|
+
expires_at: z.string().default(""),
|
|
2122
|
+
refresh_token: z.string().default(""),
|
|
2123
|
+
refresh_token_expires_at: z.string().default(""),
|
|
2124
|
+
token: z.string().min(1)
|
|
2125
|
+
});
|
|
2126
|
+
const refreshSchema = z.object({
|
|
2127
|
+
device_token: z.string().min(1),
|
|
2128
|
+
expires_at: z.string().default(""),
|
|
2129
|
+
refresh_token: z.string().optional(),
|
|
2130
|
+
refresh_token_expires_at: z.string().optional()
|
|
2131
|
+
});
|
|
2132
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
2133
|
+
const LOGIN_TIMEOUT_MS = 3e5;
|
|
2134
|
+
const EXPIRY_SKEW_MS = 3e4;
|
|
2135
|
+
function base64Url(bytes) {
|
|
2136
|
+
return Buffer.from(bytes).toString("base64url");
|
|
2046
2137
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
return;
|
|
2055
|
-
}
|
|
2056
|
-
if (process.platform !== "win32") {
|
|
2057
|
-
await rename(temporary, path);
|
|
2058
|
-
return;
|
|
2059
|
-
}
|
|
2060
|
-
const backup = `${path}.${randomBytes(8).toString("hex")}.old`;
|
|
2061
|
-
let backedUp = false;
|
|
2062
|
-
try {
|
|
2063
|
-
try {
|
|
2064
|
-
await rename(path, backup);
|
|
2065
|
-
backedUp = true;
|
|
2066
|
-
} catch (error) {
|
|
2067
|
-
if (error.code !== "ENOENT") throw error;
|
|
2068
|
-
}
|
|
2069
|
-
await rename(temporary, path);
|
|
2070
|
-
if (backedUp) await rm(backup, { force: true });
|
|
2071
|
-
} catch (error) {
|
|
2072
|
-
if (backedUp) await rename(backup, path).catch(() => void 0);
|
|
2073
|
-
throw error;
|
|
2074
|
-
}
|
|
2138
|
+
function createPkce() {
|
|
2139
|
+
const verifier = base64Url(randomBytes(32));
|
|
2140
|
+
return {
|
|
2141
|
+
challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
2142
|
+
nonce: base64Url(randomBytes(16)),
|
|
2143
|
+
verifier
|
|
2144
|
+
};
|
|
2075
2145
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
mode: 420
|
|
2084
|
-
});
|
|
2085
|
-
await commitQMindTemporaryFile(temporary, path, overwrite);
|
|
2086
|
-
} catch (error) {
|
|
2087
|
-
await rm(temporary, { force: true });
|
|
2088
|
-
throw error;
|
|
2089
|
-
}
|
|
2146
|
+
function errorCodeForStatus(status) {
|
|
2147
|
+
if (status === 401 || status === 403) return "AUTH_REQUIRED";
|
|
2148
|
+
if (status === 404) return "NOT_FOUND";
|
|
2149
|
+
if (status === 409) return "CONFLICT";
|
|
2150
|
+
if (status === 429) return "RATE_LIMITED";
|
|
2151
|
+
if (status >= 400 && status < 500) return "INVALID_ARGUMENT";
|
|
2152
|
+
return "REMOTE_ERROR";
|
|
2090
2153
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
const QMIND_FOLDER_SYNC_DEFAULT_EXTENSIONS = Object.freeze([
|
|
2094
|
-
".mdx",
|
|
2095
|
-
".md",
|
|
2096
|
-
".pdf",
|
|
2097
|
-
".txt",
|
|
2098
|
-
".docx",
|
|
2099
|
-
".pptx",
|
|
2100
|
-
".png",
|
|
2101
|
-
".jpg",
|
|
2102
|
-
".jpeg",
|
|
2103
|
-
".gif",
|
|
2104
|
-
".svg",
|
|
2105
|
-
".webp"
|
|
2106
|
-
]);
|
|
2107
|
-
function posix(path) {
|
|
2108
|
-
return path.split(sep).join("/");
|
|
2154
|
+
function record$1(value) {
|
|
2155
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
2109
2156
|
}
|
|
2110
|
-
function
|
|
2111
|
-
|
|
2157
|
+
function responseString(response, ...keys) {
|
|
2158
|
+
const body = record$1(response.body);
|
|
2159
|
+
const nestedError = record$1(body?.error);
|
|
2160
|
+
for (const source of [body, nestedError]) for (const key of keys) {
|
|
2161
|
+
const value = source?.[key];
|
|
2162
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
2163
|
+
}
|
|
2112
2164
|
}
|
|
2113
|
-
function
|
|
2114
|
-
|
|
2165
|
+
function header(response, name) {
|
|
2166
|
+
const normalized = name.toLowerCase();
|
|
2167
|
+
for (const [key, value] of Object.entries(response.headers)) if (key.toLowerCase() === normalized && value.length > 0) return value;
|
|
2115
2168
|
}
|
|
2116
|
-
function
|
|
2117
|
-
|
|
2118
|
-
const
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2169
|
+
function authResponseError(response, operation) {
|
|
2170
|
+
const requestId = responseString(response, "requestId", "request_id") ?? header(response, "x-request-id");
|
|
2171
|
+
const upstreamCode = responseString(response, "errorCode", "error_code", "code");
|
|
2172
|
+
return new QMindError(errorCodeForStatus(response.status), `${operation} failed`, {
|
|
2173
|
+
details: { operation },
|
|
2174
|
+
...requestId === void 0 ? {} : { requestId },
|
|
2175
|
+
status: response.status,
|
|
2176
|
+
...upstreamCode === void 0 ? {} : { upstreamCode }
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
async function authRequest(transport, method, path, operation, body) {
|
|
2180
|
+
const request = {
|
|
2181
|
+
...body === void 0 ? {} : { body: {
|
|
2182
|
+
kind: "json",
|
|
2183
|
+
value: body
|
|
2184
|
+
} },
|
|
2185
|
+
destination: {
|
|
2186
|
+
kind: "host",
|
|
2187
|
+
path
|
|
2188
|
+
},
|
|
2189
|
+
idempotent: method === "GET",
|
|
2190
|
+
method
|
|
2191
|
+
};
|
|
2192
|
+
let response;
|
|
2193
|
+
try {
|
|
2194
|
+
response = await transport.request(request);
|
|
2195
|
+
} catch (cause) {
|
|
2196
|
+
if (cause instanceof QMindError) throw cause;
|
|
2197
|
+
throw new QMindError("NETWORK_ERROR", `${operation} failed`);
|
|
2122
2198
|
}
|
|
2123
|
-
|
|
2199
|
+
if (response.status < 200 || response.status >= 300) throw authResponseError(response, operation);
|
|
2200
|
+
const parsed = responseRecordSchema.safeParse(response.body);
|
|
2201
|
+
if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", `${operation} returned an invalid response`, {
|
|
2202
|
+
details: { operation },
|
|
2203
|
+
status: response.status
|
|
2204
|
+
});
|
|
2205
|
+
return parsed.data;
|
|
2124
2206
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2207
|
+
function credentialsFromLogin(value, sashOrigin) {
|
|
2208
|
+
return {
|
|
2209
|
+
deviceToken: value.token,
|
|
2210
|
+
expiresAt: value.expires_at,
|
|
2211
|
+
refreshToken: value.refresh_token,
|
|
2212
|
+
refreshTokenExpiresAt: value.refresh_token_expires_at,
|
|
2213
|
+
sashOrigin,
|
|
2214
|
+
savedAt: "",
|
|
2215
|
+
schemaVersion: 2
|
|
2216
|
+
};
|
|
2129
2217
|
}
|
|
2130
|
-
function
|
|
2218
|
+
function isExpired(timestamp, now, skew = 0) {
|
|
2219
|
+
if (timestamp.length === 0) return false;
|
|
2220
|
+
const value = Date.parse(timestamp);
|
|
2221
|
+
return Number.isFinite(value) && value <= now + skew;
|
|
2222
|
+
}
|
|
2223
|
+
function aborted(signal) {
|
|
2224
|
+
return signal?.aborted === true ? new QMindError("ABORTED", "QMind authorization was aborted") : void 0;
|
|
2225
|
+
}
|
|
2226
|
+
function createSystemClock() {
|
|
2131
2227
|
return Object.freeze({
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
if (error instanceof QMindError) throw error;
|
|
2140
|
-
throw new QMindError("INVALID_ARGUMENT", `unable to read directory ID mapping: ${path}`);
|
|
2141
|
-
}
|
|
2142
|
-
},
|
|
2143
|
-
async scan(baseDirectory, options) {
|
|
2144
|
-
const directories = [];
|
|
2145
|
-
const files = [];
|
|
2146
|
-
let skipped = 0;
|
|
2147
|
-
const isIgnored = options.ignorePatterns.length === 0 ? () => false : picomatch([...options.ignorePatterns], { dot: true });
|
|
2148
|
-
const visit = async (directory) => {
|
|
2149
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
2150
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
2151
|
-
for (const entry of entries) {
|
|
2152
|
-
const absolute = join(directory, entry.name);
|
|
2153
|
-
const path = posix(relative(baseDirectory, absolute));
|
|
2154
|
-
if (entry.name.startsWith(".") || isIgnored(path)) {
|
|
2155
|
-
skipped += entry.isFile() ? 1 : 0;
|
|
2156
|
-
continue;
|
|
2157
|
-
}
|
|
2158
|
-
if (entry.isDirectory()) {
|
|
2159
|
-
directories.push(path);
|
|
2160
|
-
await visit(absolute);
|
|
2161
|
-
} else if (entry.isFile()) {
|
|
2162
|
-
if (options.extensions.has(extname(entry.name).toLowerCase())) files.push(path);
|
|
2163
|
-
else skipped += 1;
|
|
2164
|
-
}
|
|
2228
|
+
now: Date.now,
|
|
2229
|
+
sleep(milliseconds, signal) {
|
|
2230
|
+
return new Promise((resolve, reject) => {
|
|
2231
|
+
const alreadyAborted = aborted(signal);
|
|
2232
|
+
if (alreadyAborted !== void 0) {
|
|
2233
|
+
reject(alreadyAborted);
|
|
2234
|
+
return;
|
|
2165
2235
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
sha256,
|
|
2177
|
-
async writeMapping(path, mapping) {
|
|
2178
|
-
await mkdir(dirname(path), { recursive: true });
|
|
2179
|
-
await writeFile(path, `${JSON.stringify(mapping, null, 2)}\n`, { mode: 384 });
|
|
2236
|
+
const onAbort = () => {
|
|
2237
|
+
clearTimeout(timer);
|
|
2238
|
+
reject(new QMindError("ABORTED", "QMind authorization was aborted"));
|
|
2239
|
+
};
|
|
2240
|
+
const timer = setTimeout(() => {
|
|
2241
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2242
|
+
resolve();
|
|
2243
|
+
}, milliseconds);
|
|
2244
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2245
|
+
});
|
|
2180
2246
|
}
|
|
2181
2247
|
});
|
|
2182
2248
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2249
|
+
function createQMindCliAuthManager(options) {
|
|
2250
|
+
let credentials = options.credentials;
|
|
2251
|
+
let explicitToken = options.explicitToken;
|
|
2252
|
+
let token = "";
|
|
2253
|
+
let refreshPromise;
|
|
2254
|
+
const login = async (loginOptions = {}) => {
|
|
2255
|
+
if (options.config.nonInteractive) throw new QMindError("AUTH_REQUIRED", "login requires an interactive terminal");
|
|
2256
|
+
const pkce = createPkce();
|
|
2257
|
+
const browserUrl = new URL("/device/selectAccounts", options.config.dashboardOrigin);
|
|
2258
|
+
browserUrl.searchParams.set("nonce", pkce.nonce);
|
|
2259
|
+
browserUrl.searchParams.set("challenge", pkce.challenge);
|
|
2260
|
+
browserUrl.searchParams.set("challenge_method", "S256");
|
|
2261
|
+
browserUrl.searchParams.set("machine_id", pkce.nonce);
|
|
2262
|
+
if (loginOptions.clientId !== void 0 && loginOptions.clientId.trim().length > 0) browserUrl.searchParams.set("client_id", loginOptions.clientId.trim());
|
|
2263
|
+
options.reporter.info(`Opening browser for authorization. If it does not open, visit:\n${browserUrl.toString()}`);
|
|
2264
|
+
if (loginOptions.openBrowser !== false) try {
|
|
2265
|
+
await options.browser.open(browserUrl.toString());
|
|
2266
|
+
} catch {
|
|
2267
|
+
options.reporter.warn("warning: unable to open a browser; use the authorization URL above");
|
|
2268
|
+
}
|
|
2269
|
+
options.reporter.info("Waiting for authorization (timeout: 5m)...");
|
|
2270
|
+
const deadline = options.clock.now() + LOGIN_TIMEOUT_MS;
|
|
2271
|
+
while (options.clock.now() < deadline) {
|
|
2272
|
+
const abortError = aborted(loginOptions.signal);
|
|
2273
|
+
if (abortError !== void 0) throw abortError;
|
|
2274
|
+
await options.clock.sleep(POLL_INTERVAL_MS, loginOptions.signal);
|
|
2275
|
+
const pollPath = `/api/v1/deviceToken/poll?${new URLSearchParams({
|
|
2276
|
+
challenge_method: "S256",
|
|
2277
|
+
nonce: pkce.nonce,
|
|
2278
|
+
verifier: pkce.verifier
|
|
2279
|
+
}).toString()}`;
|
|
2280
|
+
try {
|
|
2281
|
+
const raw = await authRequest(options.transport, "GET", pollPath, "device authorization polling");
|
|
2282
|
+
const parsed = devicePollSchema.safeParse(raw);
|
|
2283
|
+
if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "device authorization returned an invalid response");
|
|
2284
|
+
credentials = credentialsFromLogin(parsed.data, options.config.sashOrigin);
|
|
2285
|
+
await options.vault.save(credentials);
|
|
2286
|
+
explicitToken = "";
|
|
2287
|
+
token = credentials.deviceToken;
|
|
2288
|
+
return {
|
|
2289
|
+
expiresAt: credentials.expiresAt,
|
|
2290
|
+
sashOrigin: credentials.sashOrigin
|
|
2291
|
+
};
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
if (error instanceof QMindError && error.status === 404) continue;
|
|
2294
|
+
throw error;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
throw new QMindError("AUTH_REQUIRED", "authorization timed out after 5m");
|
|
2298
|
+
};
|
|
2299
|
+
const refreshInternal = async (reason) => {
|
|
2300
|
+
if (credentials === void 0 || credentials.refreshToken.length === 0) {
|
|
2301
|
+
token = "";
|
|
2302
|
+
throw new QMindError("AUTH_REQUIRED", `credentials cannot be refreshed after ${reason}`);
|
|
2303
|
+
}
|
|
2304
|
+
if (isExpired(credentials.refreshTokenExpiresAt, options.clock.now())) {
|
|
2305
|
+
await options.vault.deleteIf(credentials);
|
|
2306
|
+
credentials = void 0;
|
|
2307
|
+
token = "";
|
|
2308
|
+
throw new QMindError("AUTH_REQUIRED", "stored refresh credentials have expired");
|
|
2309
|
+
}
|
|
2310
|
+
const raw = await authRequest(options.transport, "POST", "/api/v1/deviceToken/refresh", "credential refresh", { refresh_token: credentials.refreshToken });
|
|
2311
|
+
const parsed = refreshSchema.safeParse(raw);
|
|
2312
|
+
if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "credential refresh returned an invalid response");
|
|
2313
|
+
const basis = credentials;
|
|
2314
|
+
const legacy = basis.schemaVersion !== 2;
|
|
2315
|
+
const refreshed = {
|
|
2316
|
+
...basis,
|
|
2317
|
+
deviceToken: parsed.data.device_token,
|
|
2318
|
+
expiresAt: parsed.data.expires_at,
|
|
2319
|
+
refreshToken: parsed.data.refresh_token ?? basis.refreshToken,
|
|
2320
|
+
refreshTokenExpiresAt: parsed.data.refresh_token_expires_at ?? basis.refreshTokenExpiresAt,
|
|
2321
|
+
sashOrigin: options.config.sashOrigin,
|
|
2322
|
+
schemaVersion: 2
|
|
2323
|
+
};
|
|
2324
|
+
const current = await options.vault.compareAndSwap(basis, refreshed, { migrateLegacy: legacy }) ? refreshed : await options.vault.load();
|
|
2325
|
+
if (current === void 0) {
|
|
2326
|
+
credentials = void 0;
|
|
2327
|
+
token = "";
|
|
2328
|
+
throw new QMindError("AUTH_REQUIRED", "credentials changed during refresh");
|
|
2329
|
+
}
|
|
2330
|
+
credentials = current;
|
|
2331
|
+
token = current.deviceToken;
|
|
2332
|
+
return token;
|
|
2333
|
+
};
|
|
2334
|
+
const manager = {
|
|
2335
|
+
async getToken() {
|
|
2336
|
+
if (token.length > 0) return token;
|
|
2337
|
+
const candidate = explicitToken || credentials?.deviceToken || "";
|
|
2338
|
+
if (candidate.length === 0) {
|
|
2339
|
+
if (options.config.nonInteractive) throw new QMindError("AUTH_REQUIRED", "no credentials found; run 'qmind login' in an interactive terminal");
|
|
2340
|
+
await login();
|
|
2341
|
+
return token;
|
|
2342
|
+
}
|
|
2343
|
+
if (candidate.startsWith("pt-")) {
|
|
2344
|
+
const raw = await authRequest(options.transport, "POST", "/api/v1/jobToken/exchange", "personal token exchange", { personal_token: candidate });
|
|
2345
|
+
const parsed = personalTokenExchangeSchema.safeParse(raw);
|
|
2346
|
+
if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "personal token exchange returned an invalid response");
|
|
2347
|
+
token = parsed.data.token;
|
|
2348
|
+
return token;
|
|
2349
|
+
}
|
|
2350
|
+
if (credentials !== void 0 && candidate === credentials.deviceToken && isExpired(credentials.expiresAt, options.clock.now(), EXPIRY_SKEW_MS)) return manager.refreshToken("expired", candidate);
|
|
2351
|
+
token = candidate;
|
|
2352
|
+
return token;
|
|
2353
|
+
},
|
|
2354
|
+
login,
|
|
2355
|
+
async logout() {
|
|
2356
|
+
await options.vault.delete();
|
|
2357
|
+
credentials = void 0;
|
|
2358
|
+
explicitToken = "";
|
|
2359
|
+
token = "";
|
|
2360
|
+
refreshPromise = void 0;
|
|
2361
|
+
},
|
|
2362
|
+
async refreshToken(reason, rejectedToken) {
|
|
2363
|
+
if (rejectedToken !== void 0 && token.length > 0 && token !== rejectedToken) return token;
|
|
2364
|
+
refreshPromise ??= refreshInternal(reason).finally(() => {
|
|
2365
|
+
refreshPromise = void 0;
|
|
2366
|
+
});
|
|
2367
|
+
return await refreshPromise;
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
return Object.freeze(manager);
|
|
2190
2371
|
}
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
return
|
|
2372
|
+
//#endregion
|
|
2373
|
+
//#region ../node-host/src/authenticated-transport.ts
|
|
2374
|
+
function withBearer(request, token) {
|
|
2375
|
+
return {
|
|
2376
|
+
...request,
|
|
2377
|
+
headers: {
|
|
2378
|
+
...request.headers,
|
|
2379
|
+
authorization: `Bearer ${token}`
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2195
2382
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2383
|
+
function createAuthenticatedQMindTransport(transport, auth) {
|
|
2384
|
+
return Object.freeze({
|
|
2385
|
+
async request(request) {
|
|
2386
|
+
if (request.destination.kind === "signed") return await transport.request(request);
|
|
2387
|
+
const token = await auth.getToken();
|
|
2388
|
+
const response = await transport.request(withBearer(request, token));
|
|
2389
|
+
if (response.status !== 401) return response;
|
|
2390
|
+
const refreshed = await auth.refreshToken("unauthorized", token);
|
|
2391
|
+
return await transport.request(withBearer(request, refreshed));
|
|
2392
|
+
},
|
|
2393
|
+
async stream(request) {
|
|
2394
|
+
const token = await auth.getToken();
|
|
2395
|
+
const response = await transport.stream(withBearer(request, token));
|
|
2396
|
+
if (response.kind !== "response" || response.status !== 401) return response;
|
|
2397
|
+
const refreshed = await auth.refreshToken("unauthorized", token);
|
|
2398
|
+
return await transport.stream(withBearer(request, refreshed));
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2206
2401
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2402
|
+
//#endregion
|
|
2403
|
+
//#region ../node-host/src/config.ts
|
|
2404
|
+
const fileConfigSchema = z.object({
|
|
2405
|
+
client: z.string().optional(),
|
|
2406
|
+
dashboard_url: z.string().optional(),
|
|
2407
|
+
proxy_from_env: z.boolean().optional(),
|
|
2408
|
+
request_timeout_ms: z.number().int().positive().optional(),
|
|
2409
|
+
sash_url: z.string().optional(),
|
|
2410
|
+
stream_idle_timeout_ms: z.number().int().positive().optional(),
|
|
2411
|
+
tls_ca_file: z.string().optional(),
|
|
2412
|
+
tls_reject_unauthorized: z.boolean().optional(),
|
|
2413
|
+
update_public_key: z.string().optional()
|
|
2414
|
+
}).passthrough();
|
|
2415
|
+
const PRESETS = Object.freeze({
|
|
2416
|
+
daily: {
|
|
2417
|
+
dashboardOrigin: "https://daily.qoder.ai",
|
|
2418
|
+
sashOrigin: "https://daily-openapi.qoder.sh"
|
|
2419
|
+
},
|
|
2420
|
+
prod: {
|
|
2421
|
+
dashboardOrigin: "https://qoder.com",
|
|
2422
|
+
sashOrigin: "https://openapi.qoder.sh"
|
|
2423
|
+
},
|
|
2424
|
+
test: {
|
|
2425
|
+
dashboardOrigin: "https://test.qoder.ai",
|
|
2426
|
+
sashOrigin: "https://test-openapi.qoder.sh"
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
function invalidConfig(message, field) {
|
|
2430
|
+
return new QMindError("INVALID_ARGUMENT", message, { ...field === void 0 ? {} : { details: { field } } });
|
|
2209
2431
|
}
|
|
2210
|
-
function
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2432
|
+
function normalizeOrigin$1(rawValue, field) {
|
|
2433
|
+
let url;
|
|
2434
|
+
try {
|
|
2435
|
+
url = new URL(rawValue);
|
|
2436
|
+
} catch {
|
|
2437
|
+
throw invalidConfig(`${field} must be a valid HTTP(S) origin`, field);
|
|
2438
|
+
}
|
|
2439
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw invalidConfig(`${field} must contain only an HTTP(S) scheme and authority`, field);
|
|
2440
|
+
return url.origin;
|
|
2214
2441
|
}
|
|
2215
|
-
function
|
|
2216
|
-
|
|
2442
|
+
function optionalTrimmed(value) {
|
|
2443
|
+
const trimmed = value?.trim();
|
|
2444
|
+
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
2217
2445
|
}
|
|
2218
|
-
function
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
});
|
|
2446
|
+
function parseBoolean(value, field) {
|
|
2447
|
+
const normalized = optionalTrimmed(value)?.toLowerCase();
|
|
2448
|
+
if (normalized === void 0) return void 0;
|
|
2449
|
+
if (normalized === "1" || normalized === "true") return true;
|
|
2450
|
+
if (normalized === "0" || normalized === "false") return false;
|
|
2451
|
+
throw invalidConfig(`${field} must be 1, 0, true, or false`, field);
|
|
2225
2452
|
}
|
|
2226
|
-
|
|
2227
|
-
const
|
|
2228
|
-
if (
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
const
|
|
2235
|
-
if (
|
|
2236
|
-
const
|
|
2237
|
-
const
|
|
2238
|
-
if (
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
const
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
files_already_exist: 0,
|
|
2255
|
-
files_failed: 0,
|
|
2256
|
-
...options.delete === true ? {
|
|
2257
|
-
delete_failed: 0,
|
|
2258
|
-
dirs_deleted: 0,
|
|
2259
|
-
files_deleted: 0
|
|
2260
|
-
} : {},
|
|
2261
|
-
...options.dryRun === true ? { dry_run: true } : {}
|
|
2262
|
-
};
|
|
2263
|
-
const configuredMapping = options.directoryIdsPath === void 0 ? {} : await fileSystem.readMapping(options.directoryIdsPath);
|
|
2264
|
-
const directoryIds = {};
|
|
2265
|
-
const desiredKeys = /* @__PURE__ */ new Set();
|
|
2266
|
-
for (const directory of tree.directories) {
|
|
2267
|
-
const parentPath = posix(dirname(directory)) === "." ? "" : posix(dirname(directory));
|
|
2268
|
-
const parentId = directoryIds[parentPath] ?? "";
|
|
2269
|
-
if (parentPath.length > 0 && parentId.length === 0) {
|
|
2270
|
-
stats.dirs_failed += 1;
|
|
2271
|
-
addFailure(stats, "failed_dirs", directory);
|
|
2272
|
-
continue;
|
|
2273
|
-
}
|
|
2274
|
-
const title = basename(directory);
|
|
2275
|
-
const key = qmindFolderSyncIndexKey(parentId, title);
|
|
2276
|
-
desiredKeys.add(key);
|
|
2277
|
-
const mappedId = configuredMapping[directory];
|
|
2278
|
-
const mapped = mappedId === void 0 ? void 0 : remoteById.get(mappedId);
|
|
2279
|
-
const existing = mappedId === void 0 || mapped === void 0 ? remoteIndex.get(key) : mapped;
|
|
2280
|
-
if (existing?.isDir === true && existing.parentSourceId === parentId && existing.title === title) {
|
|
2281
|
-
directoryIds[directory] = existing.id;
|
|
2282
|
-
stats.dirs_reused += 1;
|
|
2283
|
-
continue;
|
|
2284
|
-
}
|
|
2285
|
-
if (existing !== void 0) {
|
|
2286
|
-
stats.dirs_failed += 1;
|
|
2287
|
-
addFailure(stats, "failed_dirs", directory);
|
|
2288
|
-
continue;
|
|
2289
|
-
}
|
|
2290
|
-
if (options.dryRun === true) {
|
|
2291
|
-
directoryIds[directory] = `dry-run:${directory}`;
|
|
2292
|
-
stats.dirs_created += 1;
|
|
2293
|
-
continue;
|
|
2294
|
-
}
|
|
2295
|
-
try {
|
|
2296
|
-
const created = await host.client.createDirectory(notebookId, {
|
|
2297
|
-
parentSourceId: parentId,
|
|
2298
|
-
path: title,
|
|
2299
|
-
title
|
|
2300
|
-
});
|
|
2301
|
-
directoryIds[directory] = created.id;
|
|
2302
|
-
remoteIndex.set(key, created);
|
|
2303
|
-
remoteById.set(created.id, created);
|
|
2304
|
-
stats.dirs_created += 1;
|
|
2305
|
-
} catch {
|
|
2306
|
-
stats.dirs_failed += 1;
|
|
2307
|
-
addFailure(stats, "failed_dirs", directory);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
let savedMappingPath;
|
|
2311
|
-
if (options.directoryIdsPath === void 0 && options.dryRun !== true) {
|
|
2312
|
-
savedMappingPath = options.saveDirectoryIdsPath ?? mappingPath(baseDirectory, options.cacheDirectory);
|
|
2313
|
-
await fileSystem.writeMapping(savedMappingPath, directoryIds);
|
|
2314
|
-
diagnostics.push(`directory mapping saved: ${savedMappingPath}`);
|
|
2315
|
-
}
|
|
2316
|
-
if (options.skipUpload !== true) {
|
|
2317
|
-
const limit = pLimit(concurrency);
|
|
2318
|
-
await Promise.all(tree.files.map((file) => limit(async () => {
|
|
2319
|
-
const absolute = join(baseDirectory, file);
|
|
2320
|
-
const parentPath = posix(dirname(file)) === "." ? "" : posix(dirname(file));
|
|
2321
|
-
const parentId = directoryIds[parentPath] ?? "";
|
|
2322
|
-
if (parentPath.length > 0 && parentId.length === 0) {
|
|
2323
|
-
stats.files_failed += 1;
|
|
2324
|
-
addFailure(stats, "failed_files", file);
|
|
2325
|
-
return;
|
|
2326
|
-
}
|
|
2327
|
-
const title = basename(file);
|
|
2328
|
-
const key = qmindFolderSyncIndexKey(parentId, title);
|
|
2329
|
-
desiredKeys.add(key);
|
|
2330
|
-
const existing = remoteIndex.get(key);
|
|
2331
|
-
if (existing?.isDir === true) {
|
|
2332
|
-
stats.files_failed += 1;
|
|
2333
|
-
addFailure(stats, "failed_files", file);
|
|
2334
|
-
return;
|
|
2335
|
-
}
|
|
2336
|
-
if (existing !== void 0 && !await shouldReplaceQMindFolderFile(mode, existing, absolute, fileSystem)) {
|
|
2337
|
-
stats.files_already_exist += 1;
|
|
2338
|
-
return;
|
|
2339
|
-
}
|
|
2340
|
-
if (options.dryRun === true) {
|
|
2341
|
-
stats.files_uploaded += 1;
|
|
2342
|
-
return;
|
|
2343
|
-
}
|
|
2344
|
-
try {
|
|
2345
|
-
const hash = await fileSystem.sha256(absolute);
|
|
2346
|
-
const uploaded = await withUploadRetry(async () => {
|
|
2347
|
-
const binary = await host.createFileSource(absolute);
|
|
2348
|
-
return await host.client.uploadSource(notebookId, binary, {
|
|
2349
|
-
parentSourceId: parentId,
|
|
2350
|
-
path: file,
|
|
2351
|
-
sha256: hash,
|
|
2352
|
-
title
|
|
2353
|
-
});
|
|
2354
|
-
}, options.sleep ?? defaultSleep);
|
|
2355
|
-
if (existing !== void 0) await host.client.deleteSource(notebookId, existing.id);
|
|
2356
|
-
remoteIndex.set(key, uploaded);
|
|
2357
|
-
remoteById.set(uploaded.id, uploaded);
|
|
2358
|
-
stats.files_uploaded += 1;
|
|
2359
|
-
} catch {
|
|
2360
|
-
stats.files_failed += 1;
|
|
2361
|
-
addFailure(stats, "failed_files", file);
|
|
2362
|
-
}
|
|
2363
|
-
})));
|
|
2453
|
+
function parsePositiveInteger(value, field) {
|
|
2454
|
+
const normalized = optionalTrimmed(value);
|
|
2455
|
+
if (normalized === void 0) return void 0;
|
|
2456
|
+
const parsed = Number(normalized);
|
|
2457
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw invalidConfig(`${field} must be a positive safe integer`, field);
|
|
2458
|
+
return parsed;
|
|
2459
|
+
}
|
|
2460
|
+
function qmindEnvironmentAssignment(line) {
|
|
2461
|
+
const trimmed = line.trim();
|
|
2462
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) return void 0;
|
|
2463
|
+
const assignment = trimmed.startsWith("export ") ? trimmed.slice(7) : trimmed;
|
|
2464
|
+
const separator = assignment.indexOf("=");
|
|
2465
|
+
if (separator < 1) return void 0;
|
|
2466
|
+
const key = assignment.slice(0, separator).trim();
|
|
2467
|
+
if (!/^QMIND_[A-Z0-9_]+$/.test(key)) return void 0;
|
|
2468
|
+
let value = assignment.slice(separator + 1).trim();
|
|
2469
|
+
if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
|
|
2470
|
+
return [key, value];
|
|
2471
|
+
}
|
|
2472
|
+
async function mergedEnvironment(environment, legacyEnvironmentPath) {
|
|
2473
|
+
const merged = { ...environment };
|
|
2474
|
+
if (legacyEnvironmentPath === false) return merged;
|
|
2475
|
+
let content;
|
|
2476
|
+
try {
|
|
2477
|
+
content = await readFile(legacyEnvironmentPath, "utf8");
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
if (error.code === "ENOENT") return merged;
|
|
2480
|
+
throw invalidConfig("unable to read the legacy QMind environment file");
|
|
2364
2481
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
title: basename(directory)
|
|
2371
|
-
});
|
|
2372
|
-
} catch {
|
|
2373
|
-
stats.dirs_refresh_failed += 1;
|
|
2374
|
-
}
|
|
2482
|
+
for (const line of content.split(/\r?\n/)) {
|
|
2483
|
+
const parsed = qmindEnvironmentAssignment(line);
|
|
2484
|
+
if (parsed === void 0) continue;
|
|
2485
|
+
const [key, value] = parsed;
|
|
2486
|
+
if (merged[key] === void 0) merged[key] = value;
|
|
2375
2487
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
if (source.isDir) stats.dirs_deleted = (stats.dirs_deleted ?? 0) + 1;
|
|
2385
|
-
else stats.files_deleted = (stats.files_deleted ?? 0) + 1;
|
|
2386
|
-
} catch {
|
|
2387
|
-
stats.delete_failed = (stats.delete_failed ?? 0) + 1;
|
|
2388
|
-
(stats.failed_deletes ??= []).push(source.path || source.title);
|
|
2389
|
-
}
|
|
2488
|
+
return merged;
|
|
2489
|
+
}
|
|
2490
|
+
async function fileConfiguration(homeDirectory) {
|
|
2491
|
+
try {
|
|
2492
|
+
return fileConfigSchema.parse(JSON.parse(await readFile(join(homeDirectory, "config.json"), "utf8")));
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
if (error.code === "ENOENT") return {};
|
|
2495
|
+
throw invalidConfig("stored QMind configuration is invalid");
|
|
2390
2496
|
}
|
|
2391
|
-
|
|
2497
|
+
}
|
|
2498
|
+
function defaultProcessEnvironment() {
|
|
2499
|
+
return process.env;
|
|
2500
|
+
}
|
|
2501
|
+
async function loadQMindCliConfig(options = {}) {
|
|
2502
|
+
const initialEnvironment = options.environment ?? defaultProcessEnvironment();
|
|
2503
|
+
const baseHome = options.homeDirectory ?? homedir();
|
|
2504
|
+
const environment = await mergedEnvironment(initialEnvironment, options.legacyEnvironmentPath === void 0 ? join(baseHome, ".qmind-env") : options.legacyEnvironmentPath);
|
|
2505
|
+
const configuredHome = optionalTrimmed(environment.QMIND_HOME);
|
|
2506
|
+
const homeDirectory = resolve(options.homeDirectory ?? configuredHome ?? join(baseHome, ".qmind"));
|
|
2507
|
+
const file = await fileConfiguration(homeDirectory);
|
|
2508
|
+
const flags = options.flags ?? {};
|
|
2509
|
+
const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY === true;
|
|
2510
|
+
const presetName = optionalTrimmed(environment.QMIND_ENV) ?? "prod";
|
|
2511
|
+
const preset = PRESETS[presetName] ?? PRESETS.prod;
|
|
2512
|
+
if (!(presetName in PRESETS)) options.reporter?.warn(`warning: unknown QMIND_ENV ${JSON.stringify(presetName)}, falling back to prod`);
|
|
2513
|
+
const sashOrigin = normalizeOrigin$1(optionalTrimmed(flags.sash) ?? optionalTrimmed(environment.QMIND_SASH_URL) ?? optionalTrimmed(file.sash_url) ?? optionalTrimmed(options.credentialSashOrigin) ?? preset.sashOrigin, "sash");
|
|
2514
|
+
const dashboardOrigin = normalizeOrigin$1(optionalTrimmed(flags.dashboard) ?? optionalTrimmed(environment.QMIND_DASHBOARD_URL) ?? optionalTrimmed(file.dashboard_url) ?? preset.dashboardOrigin, "dashboard");
|
|
2515
|
+
const runtime = Object.freeze({
|
|
2516
|
+
client: optionalTrimmed(flags.client) ?? optionalTrimmed(environment.QMIND_CLIENT) ?? optionalTrimmed(file.client) ?? "qmind",
|
|
2517
|
+
dashboardOrigin,
|
|
2518
|
+
homeDirectory,
|
|
2519
|
+
nonInteractive: flags.nonInteractive === true || parseBoolean(environment.QMIND_NON_INTERACTIVE, "QMIND_NON_INTERACTIVE") === true || !stdinIsTTY,
|
|
2520
|
+
sashOrigin,
|
|
2521
|
+
updatePublicKey: optionalTrimmed(environment.QMIND_UPDATE_PUBLIC_KEY) ?? optionalTrimmed(file.update_public_key) ?? ""
|
|
2522
|
+
});
|
|
2392
2523
|
return {
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2524
|
+
debug: parseBoolean(environment.QMIND_DEBUG, "QMIND_DEBUG") ?? false,
|
|
2525
|
+
environment,
|
|
2526
|
+
proxyFromEnvironment: parseBoolean(environment.QMIND_PROXY_FROM_ENV, "QMIND_PROXY_FROM_ENV") ?? file.proxy_from_env ?? true,
|
|
2527
|
+
requestTimeoutMs: parsePositiveInteger(environment.QMIND_REQUEST_TIMEOUT_MS, "QMIND_REQUEST_TIMEOUT_MS") ?? file.request_timeout_ms ?? 3e4,
|
|
2528
|
+
runtime,
|
|
2529
|
+
streamIdleTimeoutMs: parsePositiveInteger(environment.QMIND_STREAM_IDLE_TIMEOUT_MS, "QMIND_STREAM_IDLE_TIMEOUT_MS") ?? file.stream_idle_timeout_ms ?? 12e4,
|
|
2530
|
+
...(() => {
|
|
2531
|
+
const tlsCaFile = optionalTrimmed(environment.QMIND_TLS_CA_FILE) ?? optionalTrimmed(file.tls_ca_file);
|
|
2532
|
+
return tlsCaFile === void 0 ? {} : { tlsCaFile };
|
|
2533
|
+
})(),
|
|
2534
|
+
tlsRejectUnauthorized: parseBoolean(environment.QMIND_TLS_REJECT_UNAUTHORIZED, "QMIND_TLS_REJECT_UNAUTHORIZED") ?? file.tls_reject_unauthorized ?? true,
|
|
2535
|
+
token: optionalTrimmed(flags.token) ?? optionalTrimmed(environment.QMIND_TOKEN) ?? ""
|
|
2397
2536
|
};
|
|
2398
2537
|
}
|
|
2399
2538
|
//#endregion
|
|
2400
|
-
//#region src/
|
|
2401
|
-
const
|
|
2402
|
-
|
|
2403
|
-
const devicePollSchema = z.object({
|
|
2539
|
+
//#region ../node-host/src/credentials.ts
|
|
2540
|
+
const serializedCredentialsSchema = z.object({
|
|
2541
|
+
device_token: z.string().default(""),
|
|
2404
2542
|
expires_at: z.string().default(""),
|
|
2405
2543
|
refresh_token: z.string().default(""),
|
|
2406
2544
|
refresh_token_expires_at: z.string().default(""),
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
refresh_token_expires_at: z.string().optional()
|
|
2414
|
-
});
|
|
2415
|
-
const POLL_INTERVAL_MS = 2e3;
|
|
2416
|
-
const LOGIN_TIMEOUT_MS = 3e5;
|
|
2417
|
-
const EXPIRY_SKEW_MS = 3e4;
|
|
2418
|
-
function base64Url(bytes) {
|
|
2419
|
-
return Buffer.from(bytes).toString("base64url");
|
|
2420
|
-
}
|
|
2421
|
-
function createPkce() {
|
|
2422
|
-
const verifier = base64Url(randomBytes(32));
|
|
2423
|
-
return {
|
|
2424
|
-
challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
2425
|
-
nonce: base64Url(randomBytes(16)),
|
|
2426
|
-
verifier
|
|
2427
|
-
};
|
|
2428
|
-
}
|
|
2429
|
-
function errorCodeForStatus(status) {
|
|
2430
|
-
if (status === 401 || status === 403) return "AUTH_REQUIRED";
|
|
2431
|
-
if (status === 404) return "NOT_FOUND";
|
|
2432
|
-
if (status === 409) return "CONFLICT";
|
|
2433
|
-
if (status === 429) return "RATE_LIMITED";
|
|
2434
|
-
if (status >= 400 && status < 500) return "INVALID_ARGUMENT";
|
|
2435
|
-
return "REMOTE_ERROR";
|
|
2436
|
-
}
|
|
2437
|
-
function record$1(value) {
|
|
2438
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
2439
|
-
}
|
|
2440
|
-
function responseString(response, ...keys) {
|
|
2441
|
-
const body = record$1(response.body);
|
|
2442
|
-
const nestedError = record$1(body?.error);
|
|
2443
|
-
for (const source of [body, nestedError]) for (const key of keys) {
|
|
2444
|
-
const value = source?.[key];
|
|
2445
|
-
if (typeof value === "string" && value.length > 0) return value;
|
|
2446
|
-
}
|
|
2545
|
+
sash_url: z.string().default(""),
|
|
2546
|
+
saved_at: z.string().default(""),
|
|
2547
|
+
schema_version: z.number().int().positive().optional()
|
|
2548
|
+
}).passthrough();
|
|
2549
|
+
function storedCredentialsInvalid() {
|
|
2550
|
+
return new QMindError("AUTH_REQUIRED", "stored QMind credentials are invalid");
|
|
2447
2551
|
}
|
|
2448
|
-
function
|
|
2449
|
-
|
|
2450
|
-
for (const [key, value] of Object.entries(response.headers)) if (key.toLowerCase() === normalized && value.length > 0) return value;
|
|
2552
|
+
function isMissing(error) {
|
|
2553
|
+
return error.code === "ENOENT";
|
|
2451
2554
|
}
|
|
2452
|
-
function
|
|
2453
|
-
|
|
2454
|
-
const upstreamCode = responseString(response, "errorCode", "error_code", "code");
|
|
2455
|
-
return new QMindError(errorCodeForStatus(response.status), `${operation} failed`, {
|
|
2456
|
-
details: { operation },
|
|
2457
|
-
...requestId === void 0 ? {} : { requestId },
|
|
2458
|
-
status: response.status,
|
|
2459
|
-
...upstreamCode === void 0 ? {} : { upstreamCode }
|
|
2460
|
-
});
|
|
2555
|
+
function isValidOptionalTimestamp(value) {
|
|
2556
|
+
return value.length === 0 || Number.isFinite(Date.parse(value));
|
|
2461
2557
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
...body === void 0 ? {} : { body: {
|
|
2465
|
-
kind: "json",
|
|
2466
|
-
value: body
|
|
2467
|
-
} },
|
|
2468
|
-
destination: {
|
|
2469
|
-
kind: "host",
|
|
2470
|
-
path
|
|
2471
|
-
},
|
|
2472
|
-
idempotent: method === "GET",
|
|
2473
|
-
method
|
|
2474
|
-
};
|
|
2475
|
-
let response;
|
|
2558
|
+
function normalizeStoredCredentials(value) {
|
|
2559
|
+
let parsed;
|
|
2476
2560
|
try {
|
|
2477
|
-
|
|
2478
|
-
} catch
|
|
2479
|
-
|
|
2480
|
-
throw new QMindError("NETWORK_ERROR", `${operation} failed`);
|
|
2561
|
+
parsed = serializedCredentialsSchema.parse(value);
|
|
2562
|
+
} catch {
|
|
2563
|
+
throw storedCredentialsInvalid();
|
|
2481
2564
|
}
|
|
2482
|
-
if (
|
|
2483
|
-
const parsed = responseRecordSchema.safeParse(response.body);
|
|
2484
|
-
if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", `${operation} returned an invalid response`, {
|
|
2485
|
-
details: { operation },
|
|
2486
|
-
status: response.status
|
|
2487
|
-
});
|
|
2488
|
-
return parsed.data;
|
|
2489
|
-
}
|
|
2490
|
-
function credentialsFromLogin(value, sashOrigin) {
|
|
2565
|
+
if (parsed.schema_version !== void 0 && parsed.schema_version > 2 || parsed.device_token.length === 0 && parsed.refresh_token.length === 0 || !isValidOptionalTimestamp(parsed.expires_at) || !isValidOptionalTimestamp(parsed.refresh_token_expires_at)) throw storedCredentialsInvalid();
|
|
2491
2566
|
return {
|
|
2492
|
-
deviceToken:
|
|
2493
|
-
expiresAt:
|
|
2494
|
-
refreshToken:
|
|
2495
|
-
refreshTokenExpiresAt:
|
|
2496
|
-
sashOrigin,
|
|
2497
|
-
savedAt:
|
|
2498
|
-
schemaVersion:
|
|
2567
|
+
deviceToken: parsed.device_token,
|
|
2568
|
+
expiresAt: parsed.expires_at,
|
|
2569
|
+
refreshToken: parsed.refresh_token,
|
|
2570
|
+
refreshTokenExpiresAt: parsed.refresh_token_expires_at,
|
|
2571
|
+
sashOrigin: parsed.sash_url,
|
|
2572
|
+
savedAt: parsed.saved_at,
|
|
2573
|
+
schemaVersion: parsed.schema_version ?? 1
|
|
2499
2574
|
};
|
|
2500
2575
|
}
|
|
2501
|
-
function
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2576
|
+
function serializedCredentials(credentials, now) {
|
|
2577
|
+
return `${JSON.stringify({
|
|
2578
|
+
device_token: credentials.deviceToken,
|
|
2579
|
+
expires_at: credentials.expiresAt,
|
|
2580
|
+
refresh_token: credentials.refreshToken,
|
|
2581
|
+
refresh_token_expires_at: credentials.refreshTokenExpiresAt,
|
|
2582
|
+
sash_url: credentials.sashOrigin,
|
|
2583
|
+
saved_at: new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
2584
|
+
schema_version: 2
|
|
2585
|
+
}, null, 2)}\n`;
|
|
2505
2586
|
}
|
|
2506
|
-
function
|
|
2507
|
-
return
|
|
2587
|
+
function sameCredentials(left, right) {
|
|
2588
|
+
return left !== void 0 && right !== void 0 && left.deviceToken === right.deviceToken && left.expiresAt === right.expiresAt && left.refreshToken === right.refreshToken && left.refreshTokenExpiresAt === right.refreshTokenExpiresAt && left.sashOrigin === right.sashOrigin && left.schemaVersion === right.schemaVersion;
|
|
2508
2589
|
}
|
|
2509
|
-
function
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
return new Promise((resolve, reject) => {
|
|
2514
|
-
const alreadyAborted = aborted(signal);
|
|
2515
|
-
if (alreadyAborted !== void 0) {
|
|
2516
|
-
reject(alreadyAborted);
|
|
2517
|
-
return;
|
|
2518
|
-
}
|
|
2519
|
-
const onAbort = () => {
|
|
2520
|
-
clearTimeout(timer);
|
|
2521
|
-
reject(new QMindError("ABORTED", "QMind authorization was aborted"));
|
|
2522
|
-
};
|
|
2523
|
-
const timer = setTimeout(() => {
|
|
2524
|
-
signal?.removeEventListener("abort", onAbort);
|
|
2525
|
-
resolve();
|
|
2526
|
-
}, milliseconds);
|
|
2527
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2528
|
-
});
|
|
2529
|
-
}
|
|
2590
|
+
async function secureDirectory(path) {
|
|
2591
|
+
await mkdir(path, {
|
|
2592
|
+
mode: 448,
|
|
2593
|
+
recursive: true
|
|
2530
2594
|
});
|
|
2595
|
+
const info = await lstat(path);
|
|
2596
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw storedCredentialsInvalid();
|
|
2597
|
+
if (process.platform !== "win32") await chmod(path, 448);
|
|
2531
2598
|
}
|
|
2532
|
-
function
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
await
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
options.reporter.info("Waiting for authorization (timeout: 5m)...");
|
|
2553
|
-
const deadline = options.clock.now() + LOGIN_TIMEOUT_MS;
|
|
2554
|
-
while (options.clock.now() < deadline) {
|
|
2555
|
-
const abortError = aborted(loginOptions.signal);
|
|
2556
|
-
if (abortError !== void 0) throw abortError;
|
|
2557
|
-
await options.clock.sleep(POLL_INTERVAL_MS, loginOptions.signal);
|
|
2558
|
-
const pollPath = `/api/v1/deviceToken/poll?${new URLSearchParams({
|
|
2559
|
-
challenge_method: "S256",
|
|
2560
|
-
nonce: pkce.nonce,
|
|
2561
|
-
verifier: pkce.verifier
|
|
2562
|
-
}).toString()}`;
|
|
2563
|
-
try {
|
|
2564
|
-
const raw = await authRequest(options.transport, "GET", pollPath, "device authorization polling");
|
|
2565
|
-
const parsed = devicePollSchema.safeParse(raw);
|
|
2566
|
-
if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "device authorization returned an invalid response");
|
|
2567
|
-
credentials = credentialsFromLogin(parsed.data, options.config.sashOrigin);
|
|
2568
|
-
await options.vault.save(credentials);
|
|
2569
|
-
explicitToken = "";
|
|
2570
|
-
token = credentials.deviceToken;
|
|
2571
|
-
return {
|
|
2572
|
-
expiresAt: credentials.expiresAt,
|
|
2573
|
-
sashOrigin: credentials.sashOrigin
|
|
2574
|
-
};
|
|
2575
|
-
} catch (error) {
|
|
2576
|
-
if (error instanceof QMindError && error.status === 404) continue;
|
|
2577
|
-
throw error;
|
|
2578
|
-
}
|
|
2599
|
+
async function secureRegularFile(path) {
|
|
2600
|
+
const info = await lstat(path);
|
|
2601
|
+
if (!info.isFile() || info.isSymbolicLink()) throw storedCredentialsInvalid();
|
|
2602
|
+
if (process.platform !== "win32" && (info.mode & 63) !== 0) await chmod(path, 384);
|
|
2603
|
+
}
|
|
2604
|
+
async function replaceFileAtomically(tempPath, targetPath) {
|
|
2605
|
+
try {
|
|
2606
|
+
await rename(tempPath, targetPath);
|
|
2607
|
+
return;
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
if (process.platform !== "win32") throw error;
|
|
2610
|
+
}
|
|
2611
|
+
const backupPath = `${targetPath}.${randomBytes(6).toString("hex")}.old`;
|
|
2612
|
+
let backedUp = false;
|
|
2613
|
+
try {
|
|
2614
|
+
try {
|
|
2615
|
+
await rename(targetPath, backupPath);
|
|
2616
|
+
backedUp = true;
|
|
2617
|
+
} catch (error) {
|
|
2618
|
+
if (!isMissing(error)) throw error;
|
|
2579
2619
|
}
|
|
2580
|
-
|
|
2620
|
+
await rename(tempPath, targetPath);
|
|
2621
|
+
if (backedUp) await rm(backupPath, { force: true });
|
|
2622
|
+
} catch (error) {
|
|
2623
|
+
if (backedUp) await rename(backupPath, targetPath).catch(() => void 0);
|
|
2624
|
+
throw error;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
async function optionalSecret(operation, fallback) {
|
|
2628
|
+
try {
|
|
2629
|
+
return await operation();
|
|
2630
|
+
} catch {
|
|
2631
|
+
return fallback;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function createOptionalKeyringSecretStore() {
|
|
2635
|
+
const entry = async () => {
|
|
2636
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
2637
|
+
return new Entry("qmind-cli", "device-token");
|
|
2581
2638
|
};
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2639
|
+
return Object.freeze({
|
|
2640
|
+
async delete() {
|
|
2641
|
+
await optionalSecret(async () => void (await entry()).deletePassword(), void 0);
|
|
2642
|
+
},
|
|
2643
|
+
async get() {
|
|
2644
|
+
return await optionalSecret(async () => (await entry()).getPassword() ?? void 0, void 0);
|
|
2645
|
+
},
|
|
2646
|
+
async set(token) {
|
|
2647
|
+
await optionalSecret(async () => void (await entry()).setPassword(token), void 0);
|
|
2586
2648
|
}
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
function createFileCredentialVault(options) {
|
|
2652
|
+
const credentialsPath = join(options.homeDirectory, "credentials.json");
|
|
2653
|
+
const legacyBackupPath = `${credentialsPath}.v1.bak`;
|
|
2654
|
+
const secretStore = options.secretStore ?? createOptionalKeyringSecretStore();
|
|
2655
|
+
const now = options.now ?? Date.now;
|
|
2656
|
+
const loadUnlocked = async () => {
|
|
2657
|
+
let serialized;
|
|
2658
|
+
try {
|
|
2659
|
+
await secureRegularFile(credentialsPath);
|
|
2660
|
+
serialized = JSON.parse(await readFile(credentialsPath, "utf8"));
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
if (isMissing(error)) return void 0;
|
|
2663
|
+
if (error instanceof QMindError) throw error;
|
|
2664
|
+
throw storedCredentialsInvalid();
|
|
2592
2665
|
}
|
|
2593
|
-
const
|
|
2594
|
-
const
|
|
2595
|
-
|
|
2596
|
-
const legacy = credentials.schemaVersion !== 2;
|
|
2597
|
-
const refreshed = {
|
|
2666
|
+
const credentials = normalizeStoredCredentials(serialized);
|
|
2667
|
+
const keychainToken = await optionalSecret(() => secretStore.get(), void 0);
|
|
2668
|
+
return keychainToken === void 0 || keychainToken.length === 0 ? credentials : {
|
|
2598
2669
|
...credentials,
|
|
2599
|
-
deviceToken:
|
|
2600
|
-
expiresAt: parsed.data.expires_at,
|
|
2601
|
-
refreshToken: parsed.data.refresh_token ?? credentials.refreshToken,
|
|
2602
|
-
refreshTokenExpiresAt: parsed.data.refresh_token_expires_at ?? credentials.refreshTokenExpiresAt,
|
|
2603
|
-
sashOrigin: options.config.sashOrigin,
|
|
2604
|
-
schemaVersion: 2
|
|
2670
|
+
deviceToken: keychainToken
|
|
2605
2671
|
};
|
|
2606
|
-
await options.vault.save(refreshed, { migrateLegacy: legacy });
|
|
2607
|
-
credentials = refreshed;
|
|
2608
|
-
token = refreshed.deviceToken;
|
|
2609
|
-
return token;
|
|
2610
2672
|
};
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2673
|
+
const withLock = async (operation) => {
|
|
2674
|
+
await secureDirectory(options.homeDirectory);
|
|
2675
|
+
const release = await lockfile.lock(options.homeDirectory, {
|
|
2676
|
+
lockfilePath: join(options.homeDirectory, ".credentials.lock"),
|
|
2677
|
+
realpath: false,
|
|
2678
|
+
retries: {
|
|
2679
|
+
factor: 1.5,
|
|
2680
|
+
maxTimeout: 100,
|
|
2681
|
+
minTimeout: 20,
|
|
2682
|
+
retries: 8
|
|
2683
|
+
},
|
|
2684
|
+
stale: 1e4
|
|
2685
|
+
});
|
|
2686
|
+
try {
|
|
2687
|
+
return await operation();
|
|
2688
|
+
} finally {
|
|
2689
|
+
await release();
|
|
2690
|
+
}
|
|
2691
|
+
};
|
|
2692
|
+
const persist = async (credentials, saveOptions = {}) => {
|
|
2693
|
+
const tempPath = `${credentialsPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
2694
|
+
try {
|
|
2695
|
+
if (saveOptions.migrateLegacy === true) try {
|
|
2696
|
+
await secureRegularFile(credentialsPath);
|
|
2697
|
+
await copyFile(credentialsPath, legacyBackupPath);
|
|
2698
|
+
if (process.platform !== "win32") await chmod(legacyBackupPath, 384);
|
|
2699
|
+
} catch (error) {
|
|
2700
|
+
if (!isMissing(error)) throw error;
|
|
2626
2701
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
},
|
|
2631
|
-
login,
|
|
2632
|
-
async logout() {
|
|
2633
|
-
await options.vault.delete();
|
|
2634
|
-
credentials = void 0;
|
|
2635
|
-
explicitToken = "";
|
|
2636
|
-
token = "";
|
|
2637
|
-
refreshPromise = void 0;
|
|
2638
|
-
},
|
|
2639
|
-
async refreshToken(reason, rejectedToken) {
|
|
2640
|
-
if (rejectedToken !== void 0 && token.length > 0 && token !== rejectedToken) return token;
|
|
2641
|
-
refreshPromise ??= refreshInternal(reason).finally(() => {
|
|
2642
|
-
refreshPromise = void 0;
|
|
2702
|
+
await writeFile(tempPath, serializedCredentials(credentials, now()), {
|
|
2703
|
+
flag: "wx",
|
|
2704
|
+
mode: 384
|
|
2643
2705
|
});
|
|
2644
|
-
|
|
2706
|
+
if (process.platform !== "win32") await chmod(tempPath, 384);
|
|
2707
|
+
await replaceFileAtomically(tempPath, credentialsPath);
|
|
2708
|
+
if (process.platform !== "win32") await chmod(credentialsPath, 384);
|
|
2709
|
+
await optionalSecret(() => secretStore.set(credentials.deviceToken), void 0);
|
|
2710
|
+
} finally {
|
|
2711
|
+
await rm(tempPath, { force: true });
|
|
2645
2712
|
}
|
|
2646
2713
|
};
|
|
2647
|
-
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
//#region src/authenticated-transport.ts
|
|
2651
|
-
function withBearer(request, token) {
|
|
2652
|
-
return {
|
|
2653
|
-
...request,
|
|
2654
|
-
headers: {
|
|
2655
|
-
...request.headers,
|
|
2656
|
-
authorization: `Bearer ${token}`
|
|
2657
|
-
}
|
|
2714
|
+
const deleteUnlocked = async () => {
|
|
2715
|
+
await Promise.all([rm(credentialsPath, { force: true }), rm(legacyBackupPath, { force: true })]);
|
|
2716
|
+
await optionalSecret(() => secretStore.delete(), void 0);
|
|
2658
2717
|
};
|
|
2659
|
-
}
|
|
2660
|
-
function createAuthenticatedQMindTransport(transport, auth) {
|
|
2661
2718
|
return Object.freeze({
|
|
2662
|
-
async
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
return await transport.request(withBearer(request, refreshed));
|
|
2719
|
+
async compareAndSwap(expected, credentials, saveOptions = {}) {
|
|
2720
|
+
return await withLock(async () => {
|
|
2721
|
+
if (!sameCredentials(await loadUnlocked(), expected)) return false;
|
|
2722
|
+
await persist(credentials, saveOptions);
|
|
2723
|
+
return true;
|
|
2724
|
+
});
|
|
2669
2725
|
},
|
|
2670
|
-
async
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2726
|
+
async delete() {
|
|
2727
|
+
await withLock(deleteUnlocked);
|
|
2728
|
+
},
|
|
2729
|
+
async deleteIf(expected) {
|
|
2730
|
+
return await withLock(async () => {
|
|
2731
|
+
if (!sameCredentials(await loadUnlocked(), expected)) return false;
|
|
2732
|
+
await deleteUnlocked();
|
|
2733
|
+
return true;
|
|
2734
|
+
});
|
|
2735
|
+
},
|
|
2736
|
+
async load() {
|
|
2737
|
+
return await loadUnlocked();
|
|
2738
|
+
},
|
|
2739
|
+
async save(credentials, saveOptions = {}) {
|
|
2740
|
+
await withLock(async () => persist(credentials, saveOptions));
|
|
2676
2741
|
}
|
|
2677
2742
|
});
|
|
2678
2743
|
}
|
|
2679
2744
|
//#endregion
|
|
2680
|
-
//#region src/
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2745
|
+
//#region ../node-host/src/files.ts
|
|
2746
|
+
function safeQMindDownloadName(title, fallback) {
|
|
2747
|
+
const normalized = [...title.normalize("NFKC")].map((character) => {
|
|
2748
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
2749
|
+
return codePoint <= 31 || codePoint === 127 ? "_" : character;
|
|
2750
|
+
}).join("").replace(/[\\/]/g, "_").replace(/^\.+$/, "").trim().slice(0, 240);
|
|
2751
|
+
const safe = basename(normalized);
|
|
2752
|
+
if (safe.length > 0 && safe !== "." && safe !== "..") return safe;
|
|
2753
|
+
return basename(fallback.replace(/[\\/]/g, "_")) || "download";
|
|
2754
|
+
}
|
|
2755
|
+
async function commitQMindTemporaryFile(temporary, path, overwrite) {
|
|
2756
|
+
if (!overwrite) {
|
|
2757
|
+
await link(temporary, path).catch((error) => {
|
|
2758
|
+
if (error.code === "EEXIST") throw new QMindError("CONFLICT", `target already exists: ${path}`);
|
|
2759
|
+
throw error;
|
|
2760
|
+
});
|
|
2761
|
+
await rm(temporary, { force: true });
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
if (process.platform !== "win32") {
|
|
2765
|
+
await rename(temporary, path);
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
const backup = `${path}.${randomBytes(8).toString("hex")}.old`;
|
|
2769
|
+
let backedUp = false;
|
|
2770
|
+
try {
|
|
2771
|
+
try {
|
|
2772
|
+
await rename(path, backup);
|
|
2773
|
+
backedUp = true;
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
if (error.code !== "ENOENT") throw error;
|
|
2776
|
+
}
|
|
2777
|
+
await rename(temporary, path);
|
|
2778
|
+
if (backedUp) await rm(backup, { force: true });
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
if (backedUp) await rename(backup, path).catch(() => void 0);
|
|
2781
|
+
throw error;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
/** Atomic write with a no-clobber default. Temporary files are always cleaned. */
|
|
2785
|
+
async function atomicQMindWrite(path, content, overwrite = false) {
|
|
2786
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2787
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomBytes(8).toString("hex")}.tmp`);
|
|
2788
|
+
try {
|
|
2789
|
+
await writeFile(temporary, content, {
|
|
2790
|
+
flag: "wx",
|
|
2791
|
+
mode: 420
|
|
2792
|
+
});
|
|
2793
|
+
await commitQMindTemporaryFile(temporary, path, overwrite);
|
|
2794
|
+
} catch (error) {
|
|
2795
|
+
await rm(temporary, { force: true });
|
|
2796
|
+
throw error;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
//#endregion
|
|
2800
|
+
//#region ../node-host/src/node-transport.ts
|
|
2801
|
+
const RETRYABLE_STATUS = /* @__PURE__ */ new Set([
|
|
2802
|
+
408,
|
|
2803
|
+
429,
|
|
2804
|
+
500,
|
|
2805
|
+
502,
|
|
2806
|
+
503,
|
|
2807
|
+
504
|
|
2808
|
+
]);
|
|
2809
|
+
const CREDENTIAL_HEADERS = /* @__PURE__ */ new Set([
|
|
2810
|
+
"authorization",
|
|
2811
|
+
"cookie",
|
|
2812
|
+
"proxy-authorization",
|
|
2813
|
+
"x-csrf-token",
|
|
2814
|
+
"x-xsrf-token"
|
|
2815
|
+
]);
|
|
2816
|
+
const SSE_MAX_BUFFER_SIZE = 8388608;
|
|
2817
|
+
function invalidArgument(message, field) {
|
|
2818
|
+
return new QMindError("INVALID_ARGUMENT", message, { details: { field } });
|
|
2819
|
+
}
|
|
2820
|
+
function positiveInteger(value, fallback, field) {
|
|
2821
|
+
if (value === void 0) return fallback;
|
|
2822
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw invalidArgument(`${field} must be a positive safe integer`, field);
|
|
2823
|
+
return value;
|
|
2824
|
+
}
|
|
2825
|
+
function nonNegativeInteger(value, fallback, field) {
|
|
2826
|
+
if (value === void 0) return fallback;
|
|
2827
|
+
if (!Number.isSafeInteger(value) || value < 0) throw invalidArgument(`${field} must be a non-negative safe integer`, field);
|
|
2828
|
+
return value;
|
|
2829
|
+
}
|
|
2830
|
+
function normalizeOrigin(value) {
|
|
2831
|
+
let url;
|
|
2832
|
+
try {
|
|
2833
|
+
url = new URL(value);
|
|
2834
|
+
} catch {
|
|
2835
|
+
throw invalidArgument("origin must be a valid HTTP(S) origin", "origin");
|
|
2836
|
+
}
|
|
2837
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw invalidArgument("origin must contain only an HTTP(S) scheme and authority", "origin");
|
|
2838
|
+
return url;
|
|
2839
|
+
}
|
|
2840
|
+
function environmentValue(environment, ...keys) {
|
|
2841
|
+
for (const key of keys) {
|
|
2842
|
+
const value = environment[key]?.trim();
|
|
2843
|
+
if (value !== void 0 && value.length > 0) return value;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
function tlsConnectOptions(options, withTls) {
|
|
2847
|
+
const timeout = positiveInteger(options.connectTimeoutMs, 1e4, "connectTimeoutMs");
|
|
2848
|
+
if (!withTls) return { timeout };
|
|
2849
|
+
const ca = options.tls?.ca;
|
|
2850
|
+
return {
|
|
2851
|
+
...ca === void 0 ? {} : { ca: typeof ca === "string" ? ca : [...ca] },
|
|
2852
|
+
rejectUnauthorized: options.tls?.rejectUnauthorized ?? true,
|
|
2853
|
+
timeout
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
function createDispatchers(options) {
|
|
2857
|
+
const environment = options.environment ?? process.env;
|
|
2858
|
+
if (options.proxyFromEnvironment === false) return {
|
|
2859
|
+
host: new Agent({ connect: tlsConnectOptions(options, true) }),
|
|
2860
|
+
signed: new Agent({ connect: tlsConnectOptions(options, true) })
|
|
2861
|
+
};
|
|
2862
|
+
const httpProxy = environmentValue(environment, "HTTP_PROXY", "http_proxy");
|
|
2863
|
+
const httpsProxy = environmentValue(environment, "HTTPS_PROXY", "https_proxy");
|
|
2864
|
+
const noProxy = environmentValue(environment, "NO_PROXY", "no_proxy");
|
|
2865
|
+
if (httpProxy === void 0 && httpsProxy === void 0) return {
|
|
2866
|
+
host: new Agent({ connect: tlsConnectOptions(options, true) }),
|
|
2867
|
+
signed: new Agent({ connect: tlsConnectOptions(options, true) })
|
|
2868
|
+
};
|
|
2869
|
+
const proxy = {
|
|
2870
|
+
httpProxy: httpProxy ?? "",
|
|
2871
|
+
httpsProxy: httpsProxy ?? "",
|
|
2872
|
+
noProxy: noProxy ?? ""
|
|
2873
|
+
};
|
|
2874
|
+
return {
|
|
2875
|
+
host: new EnvHttpProxyAgent({
|
|
2876
|
+
...proxy,
|
|
2877
|
+
requestTls: tlsConnectOptions(options, true)
|
|
2878
|
+
}),
|
|
2879
|
+
signed: new EnvHttpProxyAgent({
|
|
2880
|
+
...proxy,
|
|
2881
|
+
requestTls: tlsConnectOptions(options, true)
|
|
2882
|
+
})
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
function headersToRecord(headers) {
|
|
2886
|
+
const result = {};
|
|
2887
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
2888
|
+
if (value === void 0) continue;
|
|
2889
|
+
result[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
2705
2890
|
}
|
|
2706
|
-
|
|
2707
|
-
function invalidConfig(message, field) {
|
|
2708
|
-
return new QMindError("INVALID_ARGUMENT", message, { ...field === void 0 ? {} : { details: { field } } });
|
|
2891
|
+
return result;
|
|
2709
2892
|
}
|
|
2710
|
-
function
|
|
2711
|
-
|
|
2893
|
+
function parseRawBody(rawBody) {
|
|
2894
|
+
if (rawBody.trim().length === 0) return void 0;
|
|
2712
2895
|
try {
|
|
2713
|
-
|
|
2896
|
+
return JSON.parse(rawBody);
|
|
2714
2897
|
} catch {
|
|
2715
|
-
|
|
2898
|
+
return rawBody;
|
|
2716
2899
|
}
|
|
2717
|
-
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw invalidConfig(`${field} must contain only an HTTP(S) scheme and authority`, field);
|
|
2718
|
-
return url.origin;
|
|
2719
2900
|
}
|
|
2720
|
-
function
|
|
2721
|
-
const
|
|
2722
|
-
return
|
|
2901
|
+
async function wireResponse(response) {
|
|
2902
|
+
const rawBody = await response.body.text();
|
|
2903
|
+
return {
|
|
2904
|
+
body: parseRawBody(rawBody),
|
|
2905
|
+
headers: headersToRecord(response.headers),
|
|
2906
|
+
rawBody,
|
|
2907
|
+
status: response.statusCode
|
|
2908
|
+
};
|
|
2723
2909
|
}
|
|
2724
|
-
function
|
|
2725
|
-
|
|
2726
|
-
if (
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2910
|
+
function assertWireRequest(request) {
|
|
2911
|
+
if (request.timeoutMs !== void 0) positiveInteger(request.timeoutMs, request.timeoutMs, "timeoutMs");
|
|
2912
|
+
if (request.destination.kind === "host") {
|
|
2913
|
+
if (!request.destination.path.startsWith("/") || request.destination.path.startsWith("//")) throw new QMindError("HOST_POLICY_ERROR", "host destination must be root-relative");
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
let url;
|
|
2917
|
+
try {
|
|
2918
|
+
url = new URL(request.destination.url);
|
|
2919
|
+
} catch {
|
|
2920
|
+
throw new QMindError("HOST_POLICY_ERROR", "signed destination must be valid HTTPS");
|
|
2921
|
+
}
|
|
2922
|
+
if (url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0) throw new QMindError("HOST_POLICY_ERROR", "signed destination must be credential-free HTTPS");
|
|
2923
|
+
for (const name of Object.keys(request.headers ?? {})) if (CREDENTIAL_HEADERS.has(name.toLowerCase())) throw new QMindError("HOST_POLICY_ERROR", "host credentials cannot reach a signed URL", { details: { header: name.toLowerCase() } });
|
|
2730
2924
|
}
|
|
2731
|
-
function
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2925
|
+
function sourceFailure(message, source, yieldedBytes) {
|
|
2926
|
+
return new QMindError("HOST_POLICY_ERROR", message, { details: {
|
|
2927
|
+
filename: source.filename,
|
|
2928
|
+
size: source.size,
|
|
2929
|
+
...yieldedBytes === void 0 ? {} : { yieldedBytes }
|
|
2930
|
+
} });
|
|
2737
2931
|
}
|
|
2738
|
-
function
|
|
2739
|
-
|
|
2740
|
-
if (trimmed.length === 0 || trimmed.startsWith("#")) return void 0;
|
|
2741
|
-
const assignment = trimmed.startsWith("export ") ? trimmed.slice(7) : trimmed;
|
|
2742
|
-
const separator = assignment.indexOf("=");
|
|
2743
|
-
if (separator < 1) return void 0;
|
|
2744
|
-
const key = assignment.slice(0, separator).trim();
|
|
2745
|
-
if (!/^QMIND_[A-Z0-9_]+$/.test(key)) return void 0;
|
|
2746
|
-
let value = assignment.slice(separator + 1).trim();
|
|
2747
|
-
if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
|
|
2748
|
-
return [key, value];
|
|
2932
|
+
function assertSource(source) {
|
|
2933
|
+
if (!Number.isSafeInteger(source.size) || source.size <= 0 || typeof source.open !== "function") throw sourceFailure("binary source metadata is invalid", source);
|
|
2749
2934
|
}
|
|
2750
|
-
async function
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
if (
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
for (const line of content.split(/\r?\n/)) {
|
|
2761
|
-
const parsed = qmindEnvironmentAssignment(line);
|
|
2762
|
-
if (parsed === void 0) continue;
|
|
2763
|
-
const [key, value] = parsed;
|
|
2764
|
-
if (merged[key] === void 0) merged[key] = value;
|
|
2935
|
+
async function* sourceBytes(source, signal, onProgress) {
|
|
2936
|
+
assertSource(source);
|
|
2937
|
+
let bytes = 0;
|
|
2938
|
+
for await (const chunk of source.open()) {
|
|
2939
|
+
if (signal?.aborted === true) throw signal.reason;
|
|
2940
|
+
if (!(chunk instanceof Uint8Array)) throw sourceFailure("binary source yielded a non-Uint8Array chunk", source, bytes);
|
|
2941
|
+
bytes += chunk.byteLength;
|
|
2942
|
+
if (bytes > source.size) throw sourceFailure("binary source yielded more bytes than declared", source, bytes);
|
|
2943
|
+
yield chunk;
|
|
2944
|
+
onProgress?.(bytes);
|
|
2765
2945
|
}
|
|
2766
|
-
|
|
2946
|
+
if (bytes !== source.size) throw sourceFailure("binary source yielded fewer bytes than declared", source, bytes);
|
|
2767
2947
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
return fileConfigSchema.parse(JSON.parse(await readFile(join(homeDirectory, "config.json"), "utf8")));
|
|
2771
|
-
} catch (error) {
|
|
2772
|
-
if (error.code === "ENOENT") return {};
|
|
2773
|
-
throw invalidConfig("stored QMind configuration is invalid");
|
|
2774
|
-
}
|
|
2948
|
+
function quoteDisposition(value) {
|
|
2949
|
+
return value.replace(/["\r\n]/g, "_");
|
|
2775
2950
|
}
|
|
2776
|
-
function
|
|
2777
|
-
return
|
|
2951
|
+
function multipartPart(name, value, boundary) {
|
|
2952
|
+
return `--${boundary}\r\nContent-Disposition: form-data; name="${quoteDisposition(name)}"\r\n\r\n${value}\r\n`;
|
|
2778
2953
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
const
|
|
2787
|
-
const
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2954
|
+
function multipartPreamble(source, boundary) {
|
|
2955
|
+
return `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${quoteDisposition(source.filename)}"\r\nContent-Type: ${source.mimeType}\r\n\r\n`;
|
|
2956
|
+
}
|
|
2957
|
+
function byteLength(value) {
|
|
2958
|
+
return Buffer.byteLength(value, "utf8");
|
|
2959
|
+
}
|
|
2960
|
+
function encodeBody(request, boundary) {
|
|
2961
|
+
const initialHeaders = { ...request.headers };
|
|
2962
|
+
const body = request.body;
|
|
2963
|
+
if (body === void 0) return { headers: initialHeaders };
|
|
2964
|
+
if (body.kind === "json") {
|
|
2965
|
+
initialHeaders["content-type"] ??= "application/json";
|
|
2966
|
+
return {
|
|
2967
|
+
body: JSON.stringify(body.value),
|
|
2968
|
+
headers: initialHeaders
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
if (body.kind === "binary") {
|
|
2972
|
+
assertSource(body.source);
|
|
2973
|
+
initialHeaders["content-type"] ??= body.source.mimeType;
|
|
2974
|
+
initialHeaders["content-length"] = String(body.source.size);
|
|
2975
|
+
return {
|
|
2976
|
+
body: Readable.from(sourceBytes(body.source, request.signal, body.onProgress)),
|
|
2977
|
+
headers: initialHeaders
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
const multipartBody = body;
|
|
2981
|
+
assertSource(multipartBody.file);
|
|
2982
|
+
const fieldParts = Object.entries(multipartBody.fields).map(([name, value]) => multipartPart(name, value, boundary));
|
|
2983
|
+
const preamble = multipartPreamble(multipartBody.file, boundary);
|
|
2984
|
+
const closing = `\r\n--${boundary}--\r\n`;
|
|
2985
|
+
const contentLength = fieldParts.reduce((total, part) => total + byteLength(part), 0) + byteLength(preamble) + multipartBody.file.size + byteLength(closing);
|
|
2986
|
+
async function* multipart() {
|
|
2987
|
+
for (const part of fieldParts) yield Buffer.from(part);
|
|
2988
|
+
yield Buffer.from(preamble);
|
|
2989
|
+
yield* sourceBytes(multipartBody.file, request.signal, multipartBody.onProgress);
|
|
2990
|
+
yield Buffer.from(closing);
|
|
2991
|
+
}
|
|
2992
|
+
initialHeaders["content-type"] = `multipart/form-data; boundary=${boundary}`;
|
|
2993
|
+
initialHeaders["content-length"] = String(contentLength);
|
|
2802
2994
|
return {
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
proxyFromEnvironment: parseBoolean(environment.QMIND_PROXY_FROM_ENV, "QMIND_PROXY_FROM_ENV") ?? file.proxy_from_env ?? true,
|
|
2806
|
-
requestTimeoutMs: parsePositiveInteger(environment.QMIND_REQUEST_TIMEOUT_MS, "QMIND_REQUEST_TIMEOUT_MS") ?? file.request_timeout_ms ?? 3e4,
|
|
2807
|
-
runtime,
|
|
2808
|
-
streamIdleTimeoutMs: parsePositiveInteger(environment.QMIND_STREAM_IDLE_TIMEOUT_MS, "QMIND_STREAM_IDLE_TIMEOUT_MS") ?? file.stream_idle_timeout_ms ?? 12e4,
|
|
2809
|
-
...(() => {
|
|
2810
|
-
const tlsCaFile = optionalTrimmed(environment.QMIND_TLS_CA_FILE) ?? optionalTrimmed(file.tls_ca_file);
|
|
2811
|
-
return tlsCaFile === void 0 ? {} : { tlsCaFile };
|
|
2812
|
-
})(),
|
|
2813
|
-
tlsRejectUnauthorized: parseBoolean(environment.QMIND_TLS_REJECT_UNAUTHORIZED, "QMIND_TLS_REJECT_UNAUTHORIZED") ?? file.tls_reject_unauthorized ?? true,
|
|
2814
|
-
token: optionalTrimmed(flags.token) ?? optionalTrimmed(environment.QMIND_TOKEN) ?? ""
|
|
2995
|
+
body: Readable.from(multipart()),
|
|
2996
|
+
headers: initialHeaders
|
|
2815
2997
|
};
|
|
2816
2998
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2821
|
-
expires_at: z.string().default(""),
|
|
2822
|
-
refresh_token: z.string().default(""),
|
|
2823
|
-
refresh_token_expires_at: z.string().default(""),
|
|
2824
|
-
sash_url: z.string().default(""),
|
|
2825
|
-
saved_at: z.string().default(""),
|
|
2826
|
-
schema_version: z.number().int().positive().optional()
|
|
2827
|
-
}).passthrough();
|
|
2828
|
-
function storedCredentialsInvalid() {
|
|
2829
|
-
return new QMindError("AUTH_REQUIRED", "stored QMind credentials are invalid");
|
|
2999
|
+
function isTimeoutCause(cause) {
|
|
3000
|
+
const code = cause.code;
|
|
3001
|
+
const name = cause.name;
|
|
3002
|
+
return code === "QMIND_REQUEST_TIMEOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT" || name === "TimeoutError";
|
|
2830
3003
|
}
|
|
2831
|
-
function
|
|
2832
|
-
|
|
3004
|
+
function safeCause(cause) {
|
|
3005
|
+
const record = cause;
|
|
3006
|
+
return {
|
|
3007
|
+
...typeof record.name === "string" ? { name: record.name } : {},
|
|
3008
|
+
...typeof record.code === "string" ? { code: record.code } : {}
|
|
3009
|
+
};
|
|
2833
3010
|
}
|
|
2834
|
-
function
|
|
2835
|
-
|
|
3011
|
+
function transportFailure(request, cause, deadlineExpired = false) {
|
|
3012
|
+
if (cause instanceof QMindError) return cause;
|
|
3013
|
+
if (request.signal?.aborted === true) return new QMindError("ABORTED", "QMind request was aborted");
|
|
3014
|
+
if (deadlineExpired || isTimeoutCause(cause)) return new QMindError("TIMEOUT", "QMind request timed out", { cause: safeCause(cause) });
|
|
3015
|
+
return new QMindError("NETWORK_ERROR", "QMind network request failed", { cause: safeCause(cause) });
|
|
2836
3016
|
}
|
|
2837
|
-
function
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3017
|
+
function requestDeadline(signal, milliseconds) {
|
|
3018
|
+
const controller = new AbortController();
|
|
3019
|
+
let timedOut = false;
|
|
3020
|
+
const timeout = setTimeout(() => {
|
|
3021
|
+
timedOut = true;
|
|
3022
|
+
const error = Object.assign(/* @__PURE__ */ new Error("QMind request deadline expired"), {
|
|
3023
|
+
code: "QMIND_REQUEST_TIMEOUT",
|
|
3024
|
+
name: "TimeoutError"
|
|
3025
|
+
});
|
|
3026
|
+
controller.abort(error);
|
|
3027
|
+
}, milliseconds);
|
|
3028
|
+
timeout.unref();
|
|
3029
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
3030
|
+
if (signal?.aborted === true) onAbort();
|
|
3031
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3032
|
+
const clearDeadline = () => clearTimeout(timeout);
|
|
2845
3033
|
return {
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
3034
|
+
clearTimeout: clearDeadline,
|
|
3035
|
+
dispose() {
|
|
3036
|
+
clearDeadline();
|
|
3037
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3038
|
+
},
|
|
3039
|
+
signal: controller.signal,
|
|
3040
|
+
get timedOut() {
|
|
3041
|
+
return timedOut;
|
|
3042
|
+
}
|
|
2853
3043
|
};
|
|
2854
3044
|
}
|
|
2855
|
-
function
|
|
2856
|
-
return
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
3045
|
+
function abortableSleep(milliseconds, signal) {
|
|
3046
|
+
return new Promise((resolve, reject) => {
|
|
3047
|
+
if (signal?.aborted === true) {
|
|
3048
|
+
reject(signal.reason);
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
const onAbort = () => {
|
|
3052
|
+
clearTimeout(timer);
|
|
3053
|
+
reject(signal?.reason);
|
|
3054
|
+
};
|
|
3055
|
+
const timer = setTimeout(() => {
|
|
3056
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3057
|
+
resolve();
|
|
3058
|
+
}, milliseconds);
|
|
3059
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2870
3060
|
});
|
|
2871
|
-
const info = await lstat(path);
|
|
2872
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw storedCredentialsInvalid();
|
|
2873
|
-
if (process.platform !== "win32") await chmod(path, 448);
|
|
2874
|
-
}
|
|
2875
|
-
async function secureRegularFile(path) {
|
|
2876
|
-
const info = await lstat(path);
|
|
2877
|
-
if (!info.isFile() || info.isSymbolicLink()) throw storedCredentialsInvalid();
|
|
2878
|
-
if (process.platform !== "win32" && (info.mode & 63) !== 0) await chmod(path, 384);
|
|
2879
3061
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
await rename(tempPath, targetPath);
|
|
2883
|
-
return;
|
|
2884
|
-
} catch (error) {
|
|
2885
|
-
if (process.platform !== "win32") throw error;
|
|
2886
|
-
}
|
|
2887
|
-
const backupPath = `${targetPath}.${randomBytes(6).toString("hex")}.old`;
|
|
2888
|
-
let backedUp = false;
|
|
2889
|
-
try {
|
|
2890
|
-
try {
|
|
2891
|
-
await rename(targetPath, backupPath);
|
|
2892
|
-
backedUp = true;
|
|
2893
|
-
} catch (error) {
|
|
2894
|
-
if (!isMissing(error)) throw error;
|
|
2895
|
-
}
|
|
2896
|
-
await rename(tempPath, targetPath);
|
|
2897
|
-
if (backedUp) await rm(backupPath, { force: true });
|
|
2898
|
-
} catch (error) {
|
|
2899
|
-
if (backedUp) await rename(backupPath, targetPath).catch(() => void 0);
|
|
2900
|
-
throw error;
|
|
2901
|
-
}
|
|
3062
|
+
function requestUrl(origin, request) {
|
|
3063
|
+
return request.destination.kind === "host" ? new URL(request.destination.path, origin) : new URL(request.destination.url);
|
|
2902
3064
|
}
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
return await operation();
|
|
2906
|
-
} catch {
|
|
2907
|
-
return fallback;
|
|
2908
|
-
}
|
|
3065
|
+
function debugRequest(options, request, url) {
|
|
3066
|
+
options.debugLog?.(`[qmind-debug] ${request.method} ${url.origin}${url.pathname}`);
|
|
2909
3067
|
}
|
|
2910
|
-
function
|
|
2911
|
-
const
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
async get() {
|
|
2920
|
-
return await optionalSecret(async () => (await entry()).getPassword() ?? void 0, void 0);
|
|
3068
|
+
async function* decodedEvents(body, request, disposeSignal) {
|
|
3069
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
3070
|
+
const pending = [];
|
|
3071
|
+
let done = false;
|
|
3072
|
+
let parserFailure;
|
|
3073
|
+
const parser = createParser({
|
|
3074
|
+
maxBufferSize: SSE_MAX_BUFFER_SIZE,
|
|
3075
|
+
onError(error) {
|
|
3076
|
+
if (error.type === "max-buffer-size-exceeded") parserFailure = error;
|
|
2921
3077
|
},
|
|
2922
|
-
|
|
2923
|
-
|
|
3078
|
+
onEvent(event) {
|
|
3079
|
+
if (event.data.trim() === "[DONE]") done = true;
|
|
3080
|
+
else pending.push(event.data);
|
|
2924
3081
|
}
|
|
2925
3082
|
});
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
const secretStore = options.secretStore ?? createOptionalKeyringSecretStore();
|
|
2931
|
-
const now = options.now ?? Date.now;
|
|
2932
|
-
const withLock = async (operation) => {
|
|
2933
|
-
await secureDirectory(options.homeDirectory);
|
|
2934
|
-
const release = await lockfile.lock(options.homeDirectory, {
|
|
2935
|
-
lockfilePath: join(options.homeDirectory, ".credentials.lock"),
|
|
2936
|
-
realpath: false,
|
|
2937
|
-
retries: {
|
|
2938
|
-
factor: 1.5,
|
|
2939
|
-
maxTimeout: 100,
|
|
2940
|
-
minTimeout: 20,
|
|
2941
|
-
retries: 8
|
|
2942
|
-
},
|
|
2943
|
-
stale: 1e4
|
|
2944
|
-
});
|
|
2945
|
-
try {
|
|
2946
|
-
return await operation();
|
|
2947
|
-
} finally {
|
|
2948
|
-
await release();
|
|
3083
|
+
const drain = function* () {
|
|
3084
|
+
while (pending.length > 0) {
|
|
3085
|
+
const value = pending.shift();
|
|
3086
|
+
if (value !== void 0) yield value;
|
|
2949
3087
|
}
|
|
2950
3088
|
};
|
|
3089
|
+
try {
|
|
3090
|
+
for await (const chunk of body) {
|
|
3091
|
+
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
3092
|
+
if (parserFailure !== void 0) throw new QMindError("STREAM_ERROR", "SSE parser buffer limit exceeded", { details: { limitBytes: SSE_MAX_BUFFER_SIZE } });
|
|
3093
|
+
yield* drain();
|
|
3094
|
+
if (done) return;
|
|
3095
|
+
}
|
|
3096
|
+
parser.feed(decoder.decode());
|
|
3097
|
+
parser.reset({ consume: true });
|
|
3098
|
+
if (parserFailure !== void 0) throw new QMindError("STREAM_ERROR", "SSE parser buffer limit exceeded", { details: { limitBytes: SSE_MAX_BUFFER_SIZE } });
|
|
3099
|
+
yield* drain();
|
|
3100
|
+
} catch (cause) {
|
|
3101
|
+
if (cause instanceof QMindError) throw cause;
|
|
3102
|
+
if (request.signal?.aborted === true) throw new QMindError("ABORTED", "QMind stream was aborted");
|
|
3103
|
+
if (isTimeoutCause(cause)) throw new QMindError("TIMEOUT", "QMind stream timed out", { cause: safeCause(cause) });
|
|
3104
|
+
throw new QMindError("STREAM_ERROR", "QMind SSE stream failed", { cause: safeCause(cause) });
|
|
3105
|
+
} finally {
|
|
3106
|
+
disposeSignal();
|
|
3107
|
+
body.destroy();
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
function createQMindNodeTransport(options) {
|
|
3111
|
+
const origin = normalizeOrigin(options.origin);
|
|
3112
|
+
const requestTimeoutMs = positiveInteger(options.requestTimeoutMs, 3e4, "requestTimeoutMs");
|
|
3113
|
+
const streamIdleTimeoutMs = positiveInteger(options.streamIdleTimeoutMs, 12e4, "streamIdleTimeoutMs");
|
|
3114
|
+
const maxRetries = nonNegativeInteger(options.maxRetries, 2, "maxRetries");
|
|
3115
|
+
const dispatchers = createDispatchers(options);
|
|
3116
|
+
const dispatcherFor = (request) => request.destination.kind === "host" ? dispatchers.host : dispatchers.signed;
|
|
2951
3117
|
return Object.freeze({
|
|
2952
|
-
async
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
3118
|
+
async close() {
|
|
3119
|
+
const unique = /* @__PURE__ */ new Set([dispatchers.host, dispatchers.signed]);
|
|
3120
|
+
await Promise.all([...unique].map(async (dispatcher) => dispatcher.close()));
|
|
3121
|
+
},
|
|
3122
|
+
async downloadToFile(download) {
|
|
3123
|
+
const request$1 = {
|
|
3124
|
+
destination: {
|
|
3125
|
+
kind: "signed",
|
|
3126
|
+
url: download.url
|
|
3127
|
+
},
|
|
3128
|
+
idempotent: true,
|
|
3129
|
+
method: "GET",
|
|
3130
|
+
...download.signal === void 0 ? {} : { signal: download.signal }
|
|
3131
|
+
};
|
|
3132
|
+
assertWireRequest(request$1);
|
|
3133
|
+
const url = requestUrl(origin, request$1);
|
|
3134
|
+
await mkdir(dirname(download.targetPath), { recursive: true });
|
|
3135
|
+
if (download.overwrite !== true) try {
|
|
3136
|
+
await stat(download.targetPath);
|
|
3137
|
+
throw new QMindError("CONFLICT", `target already exists: ${download.targetPath}`);
|
|
3138
|
+
} catch (error) {
|
|
3139
|
+
if (error.code !== "ENOENT") throw error;
|
|
3140
|
+
}
|
|
3141
|
+
const temporary = join(dirname(download.targetPath), `.${basename(download.targetPath)}.${randomBytes(8).toString("hex")}.tmp`);
|
|
3142
|
+
let response;
|
|
3143
|
+
try {
|
|
3144
|
+
response = await request(url, {
|
|
3145
|
+
bodyTimeout: requestTimeoutMs,
|
|
3146
|
+
dispatcher: dispatchers.signed,
|
|
3147
|
+
headersTimeout: requestTimeoutMs,
|
|
3148
|
+
idempotent: true,
|
|
3149
|
+
method: "GET",
|
|
3150
|
+
...download.signal === void 0 ? {} : { signal: download.signal }
|
|
3151
|
+
});
|
|
3152
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
3153
|
+
const decoded = await wireResponse(response);
|
|
3154
|
+
throw new QMindError("REMOTE_ERROR", "download request failed", {
|
|
3155
|
+
...decoded.rawBody === void 0 ? {} : { rawBody: decoded.rawBody },
|
|
3156
|
+
status: decoded.status
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
await pipeline(response.body, createWriteStream(temporary, {
|
|
3160
|
+
flags: "wx",
|
|
3161
|
+
mode: 384
|
|
3162
|
+
}));
|
|
3163
|
+
const size = (await stat(temporary)).size;
|
|
3164
|
+
await chmod(temporary, 420);
|
|
3165
|
+
await commitQMindTemporaryFile(temporary, download.targetPath, download.overwrite === true);
|
|
3166
|
+
return size;
|
|
3167
|
+
} catch (cause) {
|
|
3168
|
+
response?.body.destroy();
|
|
3169
|
+
await rm(temporary, { force: true });
|
|
3170
|
+
if (cause instanceof QMindError) throw cause;
|
|
3171
|
+
throw transportFailure(request$1, cause);
|
|
3172
|
+
}
|
|
3173
|
+
},
|
|
3174
|
+
async request(request$2) {
|
|
3175
|
+
assertWireRequest(request$2);
|
|
3176
|
+
const url = requestUrl(origin, request$2);
|
|
3177
|
+
const attempts = request$2.idempotent ? maxRetries + 1 : 1;
|
|
3178
|
+
let lastFailure;
|
|
3179
|
+
let lastDeadlineExpired = false;
|
|
3180
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
3181
|
+
const deadline = requestDeadline(request$2.signal, request$2.timeoutMs ?? requestTimeoutMs);
|
|
3182
|
+
try {
|
|
3183
|
+
debugRequest(options, request$2, url);
|
|
3184
|
+
const encoded = encodeBody(request$2, `----qmind-${randomBytes(18).toString("hex")}`);
|
|
3185
|
+
const response = await request(url, {
|
|
3186
|
+
...encoded.body === void 0 ? {} : { body: encoded.body },
|
|
3187
|
+
bodyTimeout: request$2.timeoutMs ?? requestTimeoutMs,
|
|
3188
|
+
dispatcher: dispatcherFor(request$2),
|
|
3189
|
+
headers: encoded.headers,
|
|
3190
|
+
headersTimeout: request$2.timeoutMs ?? requestTimeoutMs,
|
|
3191
|
+
idempotent: request$2.idempotent,
|
|
3192
|
+
method: request$2.method,
|
|
3193
|
+
signal: deadline.signal
|
|
3194
|
+
});
|
|
3195
|
+
if (attempt + 1 < attempts && RETRYABLE_STATUS.has(response.statusCode)) {
|
|
3196
|
+
await response.body.dump();
|
|
3197
|
+
await abortableSleep(250 * 2 ** attempt, request$2.signal);
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
return await wireResponse(response);
|
|
3201
|
+
} catch (cause) {
|
|
3202
|
+
lastFailure = cause;
|
|
3203
|
+
lastDeadlineExpired = deadline.timedOut;
|
|
3204
|
+
if (attempt + 1 < attempts && request$2.signal?.aborted !== true) {
|
|
3205
|
+
await abortableSleep(250 * 2 ** attempt, request$2.signal);
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3208
|
+
} finally {
|
|
3209
|
+
deadline.dispose();
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
throw transportFailure(request$2, lastFailure, lastDeadlineExpired);
|
|
2957
3213
|
},
|
|
2958
|
-
async
|
|
2959
|
-
|
|
3214
|
+
async stream(request$3) {
|
|
3215
|
+
assertWireRequest(request$3);
|
|
3216
|
+
const url = requestUrl(origin, request$3);
|
|
3217
|
+
debugRequest(options, request$3, url);
|
|
3218
|
+
const encoded = encodeBody(request$3, `----qmind-${randomBytes(18).toString("hex")}`);
|
|
3219
|
+
let response;
|
|
3220
|
+
const deadline = requestDeadline(request$3.signal, request$3.timeoutMs ?? requestTimeoutMs);
|
|
2960
3221
|
try {
|
|
2961
|
-
await
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
3222
|
+
response = await request(url, {
|
|
3223
|
+
...encoded.body === void 0 ? {} : { body: encoded.body },
|
|
3224
|
+
bodyTimeout: streamIdleTimeoutMs,
|
|
3225
|
+
dispatcher: dispatchers.host,
|
|
3226
|
+
headers: {
|
|
3227
|
+
accept: "text/event-stream",
|
|
3228
|
+
...encoded.headers
|
|
3229
|
+
},
|
|
3230
|
+
headersTimeout: request$3.timeoutMs ?? requestTimeoutMs,
|
|
3231
|
+
idempotent: request$3.idempotent,
|
|
3232
|
+
method: request$3.method,
|
|
3233
|
+
signal: deadline.signal
|
|
3234
|
+
});
|
|
3235
|
+
} catch (cause) {
|
|
3236
|
+
deadline.dispose();
|
|
3237
|
+
throw transportFailure(request$3, cause, deadline.timedOut);
|
|
3238
|
+
}
|
|
3239
|
+
deadline.clearTimeout();
|
|
3240
|
+
const headers = headersToRecord(response.headers);
|
|
3241
|
+
if (response.statusCode < 200 || response.statusCode >= 300 || !headers["content-type"]?.toLowerCase().includes("text/event-stream")) try {
|
|
3242
|
+
return {
|
|
3243
|
+
...await wireResponse(response),
|
|
3244
|
+
kind: "response"
|
|
3245
|
+
};
|
|
3246
|
+
} finally {
|
|
3247
|
+
deadline.dispose();
|
|
2967
3248
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3249
|
+
return {
|
|
3250
|
+
events: decodedEvents(response.body, request$3, deadline.dispose),
|
|
3251
|
+
headers,
|
|
3252
|
+
kind: "events",
|
|
3253
|
+
status: response.statusCode
|
|
2973
3254
|
};
|
|
2974
|
-
},
|
|
2975
|
-
async save(credentials, saveOptions = {}) {
|
|
2976
|
-
await withLock(async () => {
|
|
2977
|
-
const tempPath = `${credentialsPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
2978
|
-
try {
|
|
2979
|
-
if (saveOptions.migrateLegacy === true) try {
|
|
2980
|
-
await secureRegularFile(credentialsPath);
|
|
2981
|
-
await copyFile(credentialsPath, legacyBackupPath);
|
|
2982
|
-
if (process.platform !== "win32") await chmod(legacyBackupPath, 384);
|
|
2983
|
-
} catch (error) {
|
|
2984
|
-
if (!isMissing(error)) throw error;
|
|
2985
|
-
}
|
|
2986
|
-
await writeFile(tempPath, serializedCredentials(credentials, now()), {
|
|
2987
|
-
flag: "wx",
|
|
2988
|
-
mode: 384
|
|
2989
|
-
});
|
|
2990
|
-
if (process.platform !== "win32") await chmod(tempPath, 384);
|
|
2991
|
-
await replaceFileAtomically(tempPath, credentialsPath);
|
|
2992
|
-
if (process.platform !== "win32") await chmod(credentialsPath, 384);
|
|
2993
|
-
await optionalSecret(() => secretStore.set(credentials.deviceToken), void 0);
|
|
2994
|
-
} finally {
|
|
2995
|
-
await rm(tempPath, { force: true });
|
|
2996
|
-
}
|
|
2997
|
-
});
|
|
2998
3255
|
}
|
|
2999
3256
|
});
|
|
3000
3257
|
}
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
"
|
|
3013
|
-
"
|
|
3014
|
-
"
|
|
3015
|
-
"
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
}
|
|
3027
|
-
function nonNegativeInteger(value, fallback, field) {
|
|
3028
|
-
if (value === void 0) return fallback;
|
|
3029
|
-
if (!Number.isSafeInteger(value) || value < 0) throw invalidArgument(`${field} must be a non-negative safe integer`, field);
|
|
3030
|
-
return value;
|
|
3031
|
-
}
|
|
3032
|
-
function normalizeOrigin(value) {
|
|
3033
|
-
let url;
|
|
3034
|
-
try {
|
|
3035
|
-
url = new URL(value);
|
|
3036
|
-
} catch {
|
|
3037
|
-
throw invalidArgument("origin must be a valid HTTP(S) origin", "origin");
|
|
3038
|
-
}
|
|
3039
|
-
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw invalidArgument("origin must contain only an HTTP(S) scheme and authority", "origin");
|
|
3040
|
-
return url;
|
|
3041
|
-
}
|
|
3042
|
-
function environmentValue(environment, ...keys) {
|
|
3043
|
-
for (const key of keys) {
|
|
3044
|
-
const value = environment[key]?.trim();
|
|
3045
|
-
if (value !== void 0 && value.length > 0) return value;
|
|
3046
|
-
}
|
|
3047
|
-
}
|
|
3048
|
-
function tlsConnectOptions(options, withTls) {
|
|
3049
|
-
const timeout = positiveInteger(options.connectTimeoutMs, 1e4, "connectTimeoutMs");
|
|
3050
|
-
if (!withTls) return { timeout };
|
|
3051
|
-
const ca = options.tls?.ca;
|
|
3052
|
-
return {
|
|
3053
|
-
...ca === void 0 ? {} : { ca: typeof ca === "string" ? ca : [...ca] },
|
|
3054
|
-
rejectUnauthorized: options.tls?.rejectUnauthorized ?? true,
|
|
3055
|
-
timeout
|
|
3056
|
-
};
|
|
3057
|
-
}
|
|
3058
|
-
function createDispatchers(options) {
|
|
3059
|
-
const environment = options.environment ?? process.env;
|
|
3060
|
-
if (options.proxyFromEnvironment === false) return {
|
|
3061
|
-
host: new Agent({ connect: tlsConnectOptions(options, true) }),
|
|
3062
|
-
signed: new Agent({ connect: tlsConnectOptions(options, true) })
|
|
3063
|
-
};
|
|
3064
|
-
const httpProxy = environmentValue(environment, "HTTP_PROXY", "http_proxy");
|
|
3065
|
-
const httpsProxy = environmentValue(environment, "HTTPS_PROXY", "https_proxy");
|
|
3066
|
-
const noProxy = environmentValue(environment, "NO_PROXY", "no_proxy");
|
|
3067
|
-
const proxy = {
|
|
3068
|
-
...httpProxy === void 0 ? {} : { httpProxy },
|
|
3069
|
-
...httpsProxy === void 0 ? {} : { httpsProxy },
|
|
3070
|
-
...noProxy === void 0 ? {} : { noProxy }
|
|
3071
|
-
};
|
|
3072
|
-
return {
|
|
3073
|
-
host: new EnvHttpProxyAgent({
|
|
3074
|
-
...proxy,
|
|
3075
|
-
requestTls: tlsConnectOptions(options, true)
|
|
3076
|
-
}),
|
|
3077
|
-
signed: new EnvHttpProxyAgent({
|
|
3078
|
-
...proxy,
|
|
3079
|
-
requestTls: tlsConnectOptions(options, true)
|
|
3080
|
-
})
|
|
3081
|
-
};
|
|
3082
|
-
}
|
|
3083
|
-
function headersToRecord(headers) {
|
|
3084
|
-
const result = {};
|
|
3085
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
3086
|
-
if (value === void 0) continue;
|
|
3087
|
-
result[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
3088
|
-
}
|
|
3089
|
-
return result;
|
|
3090
|
-
}
|
|
3091
|
-
function parseRawBody(rawBody) {
|
|
3092
|
-
if (rawBody.trim().length === 0) return void 0;
|
|
3093
|
-
try {
|
|
3094
|
-
return JSON.parse(rawBody);
|
|
3095
|
-
} catch {
|
|
3096
|
-
return rawBody;
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
async function wireResponse(response) {
|
|
3100
|
-
const rawBody = await response.body.text();
|
|
3101
|
-
return {
|
|
3102
|
-
body: parseRawBody(rawBody),
|
|
3103
|
-
headers: headersToRecord(response.headers),
|
|
3104
|
-
rawBody,
|
|
3105
|
-
status: response.statusCode
|
|
3106
|
-
};
|
|
3107
|
-
}
|
|
3108
|
-
function assertWireRequest(request) {
|
|
3109
|
-
if (request.timeoutMs !== void 0) positiveInteger(request.timeoutMs, request.timeoutMs, "timeoutMs");
|
|
3110
|
-
if (request.destination.kind === "host") {
|
|
3111
|
-
if (!request.destination.path.startsWith("/") || request.destination.path.startsWith("//")) throw new QMindError("HOST_POLICY_ERROR", "host destination must be root-relative");
|
|
3112
|
-
return;
|
|
3113
|
-
}
|
|
3114
|
-
let url;
|
|
3115
|
-
try {
|
|
3116
|
-
url = new URL(request.destination.url);
|
|
3117
|
-
} catch {
|
|
3118
|
-
throw new QMindError("HOST_POLICY_ERROR", "signed destination must be valid HTTPS");
|
|
3119
|
-
}
|
|
3120
|
-
if (url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0) throw new QMindError("HOST_POLICY_ERROR", "signed destination must be credential-free HTTPS");
|
|
3121
|
-
for (const name of Object.keys(request.headers ?? {})) if (CREDENTIAL_HEADERS.has(name.toLowerCase())) throw new QMindError("HOST_POLICY_ERROR", "host credentials cannot reach a signed URL", { details: { header: name.toLowerCase() } });
|
|
3122
|
-
}
|
|
3123
|
-
function sourceFailure(message, source, yieldedBytes) {
|
|
3124
|
-
return new QMindError("HOST_POLICY_ERROR", message, { details: {
|
|
3125
|
-
filename: source.filename,
|
|
3126
|
-
size: source.size,
|
|
3127
|
-
...yieldedBytes === void 0 ? {} : { yieldedBytes }
|
|
3128
|
-
} });
|
|
3129
|
-
}
|
|
3130
|
-
function assertSource(source) {
|
|
3131
|
-
if (!Number.isSafeInteger(source.size) || source.size <= 0 || typeof source.open !== "function") throw sourceFailure("binary source metadata is invalid", source);
|
|
3132
|
-
}
|
|
3133
|
-
async function* sourceBytes(source, signal, onProgress) {
|
|
3134
|
-
assertSource(source);
|
|
3135
|
-
let bytes = 0;
|
|
3136
|
-
for await (const chunk of source.open()) {
|
|
3137
|
-
if (signal?.aborted === true) throw signal.reason;
|
|
3138
|
-
if (!(chunk instanceof Uint8Array)) throw sourceFailure("binary source yielded a non-Uint8Array chunk", source, bytes);
|
|
3139
|
-
bytes += chunk.byteLength;
|
|
3140
|
-
if (bytes > source.size) throw sourceFailure("binary source yielded more bytes than declared", source, bytes);
|
|
3141
|
-
yield chunk;
|
|
3142
|
-
onProgress?.(bytes);
|
|
3143
|
-
}
|
|
3144
|
-
if (bytes !== source.size) throw sourceFailure("binary source yielded fewer bytes than declared", source, bytes);
|
|
3145
|
-
}
|
|
3146
|
-
function quoteDisposition(value) {
|
|
3147
|
-
return value.replace(/["\r\n]/g, "_");
|
|
3148
|
-
}
|
|
3149
|
-
function multipartPart(name, value, boundary) {
|
|
3150
|
-
return `--${boundary}\r\nContent-Disposition: form-data; name="${quoteDisposition(name)}"\r\n\r\n${value}\r\n`;
|
|
3151
|
-
}
|
|
3152
|
-
function multipartPreamble(source, boundary) {
|
|
3153
|
-
return `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${quoteDisposition(source.filename)}"\r\nContent-Type: ${source.mimeType}\r\n\r\n`;
|
|
3154
|
-
}
|
|
3155
|
-
function byteLength(value) {
|
|
3156
|
-
return Buffer.byteLength(value, "utf8");
|
|
3157
|
-
}
|
|
3158
|
-
function encodeBody(request, boundary) {
|
|
3159
|
-
const initialHeaders = { ...request.headers };
|
|
3160
|
-
const body = request.body;
|
|
3161
|
-
if (body === void 0) return { headers: initialHeaders };
|
|
3162
|
-
if (body.kind === "json") {
|
|
3163
|
-
initialHeaders["content-type"] ??= "application/json";
|
|
3164
|
-
return {
|
|
3165
|
-
body: JSON.stringify(body.value),
|
|
3166
|
-
headers: initialHeaders
|
|
3167
|
-
};
|
|
3168
|
-
}
|
|
3169
|
-
if (body.kind === "binary") {
|
|
3170
|
-
assertSource(body.source);
|
|
3171
|
-
initialHeaders["content-type"] ??= body.source.mimeType;
|
|
3172
|
-
initialHeaders["content-length"] = String(body.source.size);
|
|
3173
|
-
return {
|
|
3174
|
-
body: Readable.from(sourceBytes(body.source, request.signal, body.onProgress)),
|
|
3175
|
-
headers: initialHeaders
|
|
3176
|
-
};
|
|
3177
|
-
}
|
|
3178
|
-
const multipartBody = body;
|
|
3179
|
-
assertSource(multipartBody.file);
|
|
3180
|
-
const fieldParts = Object.entries(multipartBody.fields).map(([name, value]) => multipartPart(name, value, boundary));
|
|
3181
|
-
const preamble = multipartPreamble(multipartBody.file, boundary);
|
|
3182
|
-
const closing = `\r\n--${boundary}--\r\n`;
|
|
3183
|
-
const contentLength = fieldParts.reduce((total, part) => total + byteLength(part), 0) + byteLength(preamble) + multipartBody.file.size + byteLength(closing);
|
|
3184
|
-
async function* multipart() {
|
|
3185
|
-
for (const part of fieldParts) yield Buffer.from(part);
|
|
3186
|
-
yield Buffer.from(preamble);
|
|
3187
|
-
yield* sourceBytes(multipartBody.file, request.signal, multipartBody.onProgress);
|
|
3188
|
-
yield Buffer.from(closing);
|
|
3258
|
+
const MIME_TYPES = Object.freeze({
|
|
3259
|
+
".csv": "text/csv",
|
|
3260
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
3261
|
+
".gif": "image/gif",
|
|
3262
|
+
".jpeg": "image/jpeg",
|
|
3263
|
+
".jpg": "image/jpeg",
|
|
3264
|
+
".md": "text/markdown",
|
|
3265
|
+
".mdx": "text/markdown",
|
|
3266
|
+
".pdf": "application/pdf",
|
|
3267
|
+
".png": "image/png",
|
|
3268
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
3269
|
+
".svg": "image/svg+xml",
|
|
3270
|
+
".txt": "text/plain",
|
|
3271
|
+
".webp": "image/webp",
|
|
3272
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
3273
|
+
});
|
|
3274
|
+
function detectQMindMimeType(filename) {
|
|
3275
|
+
return MIME_TYPES[extname(filename).toLowerCase()] ?? "application/octet-stream";
|
|
3276
|
+
}
|
|
3277
|
+
async function createQMindFileSource(path, options = {}) {
|
|
3278
|
+
let info;
|
|
3279
|
+
try {
|
|
3280
|
+
info = await stat(path);
|
|
3281
|
+
} catch {
|
|
3282
|
+
throw invalidArgument("source path must reference a readable file", "path");
|
|
3189
3283
|
}
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3284
|
+
if (!info.isFile() || info.size <= 0) throw invalidArgument("source path must reference a non-empty regular file", "path");
|
|
3285
|
+
const filename = options.filename?.trim() || basename(path);
|
|
3286
|
+
const mimeType = options.mimeType?.trim() || detectQMindMimeType(filename);
|
|
3287
|
+
return Object.freeze({
|
|
3288
|
+
filename,
|
|
3289
|
+
mimeType,
|
|
3290
|
+
async *open() {
|
|
3291
|
+
for await (const chunk of createReadStream(path)) yield chunk;
|
|
3292
|
+
},
|
|
3293
|
+
size: info.size
|
|
3294
|
+
});
|
|
3196
3295
|
}
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
return
|
|
3296
|
+
//#endregion
|
|
3297
|
+
//#region ../node-host/src/host.ts
|
|
3298
|
+
function defaultReporter() {
|
|
3299
|
+
return Object.freeze({
|
|
3300
|
+
info(message) {
|
|
3301
|
+
console.error(message);
|
|
3302
|
+
},
|
|
3303
|
+
warn(message) {
|
|
3304
|
+
console.error(message);
|
|
3305
|
+
}
|
|
3306
|
+
});
|
|
3201
3307
|
}
|
|
3202
|
-
function
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
...typeof record.code === "string" ? { code: record.code } : {}
|
|
3207
|
-
};
|
|
3308
|
+
function defaultBrowser() {
|
|
3309
|
+
return Object.freeze({ async open(url) {
|
|
3310
|
+
(await open(url, { wait: false })).unref();
|
|
3311
|
+
} });
|
|
3208
3312
|
}
|
|
3209
|
-
function
|
|
3210
|
-
if (
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3313
|
+
async function tlsAuthority(path) {
|
|
3314
|
+
if (path === void 0) return void 0;
|
|
3315
|
+
try {
|
|
3316
|
+
return await readFile(path, "utf8");
|
|
3317
|
+
} catch {
|
|
3318
|
+
throw new QMindError("HOST_POLICY_ERROR", "unable to read the configured TLS CA file", { details: { field: "tlsCaFile" } });
|
|
3319
|
+
}
|
|
3214
3320
|
}
|
|
3215
|
-
function
|
|
3216
|
-
const controller = new AbortController();
|
|
3217
|
-
let timedOut = false;
|
|
3218
|
-
const timeout = setTimeout(() => {
|
|
3219
|
-
timedOut = true;
|
|
3220
|
-
const error = Object.assign(/* @__PURE__ */ new Error("QMind request deadline expired"), {
|
|
3221
|
-
code: "QMIND_REQUEST_TIMEOUT",
|
|
3222
|
-
name: "TimeoutError"
|
|
3223
|
-
});
|
|
3224
|
-
controller.abort(error);
|
|
3225
|
-
}, milliseconds);
|
|
3226
|
-
timeout.unref();
|
|
3227
|
-
const onAbort = () => controller.abort(signal?.reason);
|
|
3228
|
-
if (signal?.aborted === true) onAbort();
|
|
3229
|
-
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3230
|
-
const clearDeadline = () => clearTimeout(timeout);
|
|
3321
|
+
function configurationOptions(options, reporter) {
|
|
3231
3322
|
return {
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
},
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
return timedOut;
|
|
3240
|
-
}
|
|
3323
|
+
...options.credentialSashOrigin === void 0 ? {} : { credentialSashOrigin: options.credentialSashOrigin },
|
|
3324
|
+
...options.environment === void 0 ? {} : { environment: options.environment },
|
|
3325
|
+
...options.flags === void 0 ? {} : { flags: options.flags },
|
|
3326
|
+
...options.homeDirectory === void 0 ? {} : { homeDirectory: options.homeDirectory },
|
|
3327
|
+
...options.legacyEnvironmentPath === void 0 ? {} : { legacyEnvironmentPath: options.legacyEnvironmentPath },
|
|
3328
|
+
...reporter === void 0 ? {} : { reporter },
|
|
3329
|
+
...options.stdinIsTTY === void 0 ? {} : { stdinIsTTY: options.stdinIsTTY }
|
|
3241
3330
|
};
|
|
3242
3331
|
}
|
|
3243
|
-
function
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3332
|
+
async function createQMindCliHost(options = {}) {
|
|
3333
|
+
const reporter = options.adapters?.reporter ?? defaultReporter();
|
|
3334
|
+
const clock = options.adapters?.clock ?? createSystemClock();
|
|
3335
|
+
const vault = createFileCredentialVault({
|
|
3336
|
+
homeDirectory: (await loadQMindCliConfig(configurationOptions(options))).runtime.homeDirectory,
|
|
3337
|
+
now: () => clock.now(),
|
|
3338
|
+
secretStore: options.adapters?.secretStore ?? createOptionalKeyringSecretStore()
|
|
3339
|
+
});
|
|
3340
|
+
let credentials;
|
|
3341
|
+
try {
|
|
3342
|
+
credentials = await vault.load();
|
|
3343
|
+
} catch (error) {
|
|
3344
|
+
if (options.credentialReadFailure !== "unauthenticated") throw error;
|
|
3345
|
+
reporter.warn("QMind credential vault is unavailable; authentication is required.");
|
|
3346
|
+
credentials = void 0;
|
|
3347
|
+
}
|
|
3348
|
+
const resolved = await loadQMindCliConfig({
|
|
3349
|
+
...configurationOptions(options, reporter),
|
|
3350
|
+
...credentials?.sashOrigin ? { credentialSashOrigin: credentials.sashOrigin } : {},
|
|
3351
|
+
reporter
|
|
3352
|
+
});
|
|
3353
|
+
const ca = await tlsAuthority(resolved.tlsCaFile);
|
|
3354
|
+
const nodeOptions = {
|
|
3355
|
+
...options.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: options.connectTimeoutMs },
|
|
3356
|
+
...resolved.debug ? { debugLog: (message) => reporter.info(message) } : {},
|
|
3357
|
+
environment: resolved.environment,
|
|
3358
|
+
...options.maxRetries === void 0 ? {} : { maxRetries: options.maxRetries },
|
|
3359
|
+
origin: resolved.runtime.sashOrigin,
|
|
3360
|
+
proxyFromEnvironment: resolved.proxyFromEnvironment,
|
|
3361
|
+
requestTimeoutMs: resolved.requestTimeoutMs,
|
|
3362
|
+
streamIdleTimeoutMs: resolved.streamIdleTimeoutMs,
|
|
3363
|
+
tls: {
|
|
3364
|
+
...ca === void 0 ? {} : { ca },
|
|
3365
|
+
rejectUnauthorized: resolved.tlsRejectUnauthorized
|
|
3248
3366
|
}
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3367
|
+
};
|
|
3368
|
+
const ownedTransport = options.adapters?.transport === void 0 ? createQMindNodeTransport(nodeOptions) : void 0;
|
|
3369
|
+
const rawTransport = options.adapters?.transport ?? ownedTransport;
|
|
3370
|
+
if (rawTransport === void 0) throw new QMindError("HOST_POLICY_ERROR", "unable to initialize the QMind transport");
|
|
3371
|
+
const auth = createQMindCliAuthManager({
|
|
3372
|
+
browser: options.adapters?.browser ?? defaultBrowser(),
|
|
3373
|
+
clock,
|
|
3374
|
+
config: resolved.runtime,
|
|
3375
|
+
credentials,
|
|
3376
|
+
explicitToken: resolved.token,
|
|
3377
|
+
reporter,
|
|
3378
|
+
transport: rawTransport,
|
|
3379
|
+
vault
|
|
3380
|
+
});
|
|
3381
|
+
const client = createQMindClient({
|
|
3382
|
+
access: "sash",
|
|
3383
|
+
client: resolved.runtime.client,
|
|
3384
|
+
transport: createAuthenticatedQMindTransport(rawTransport, auth)
|
|
3385
|
+
});
|
|
3386
|
+
return Object.freeze({
|
|
3387
|
+
client,
|
|
3388
|
+
async close() {
|
|
3389
|
+
await ownedTransport?.close();
|
|
3390
|
+
},
|
|
3391
|
+
config: resolved.runtime,
|
|
3392
|
+
createFileSource: createQMindFileSource,
|
|
3393
|
+
async downloadToFile(downloadOptions) {
|
|
3394
|
+
const downloader = options.adapters?.downloader ?? ownedTransport;
|
|
3395
|
+
if (downloader === void 0) throw new QMindError("UNSUPPORTED_OPERATION", "the injected CLI transport does not provide file downloads");
|
|
3396
|
+
return await downloader.downloadToFile(downloadOptions);
|
|
3397
|
+
},
|
|
3398
|
+
login: auth.login,
|
|
3399
|
+
logout: auth.logout
|
|
3258
3400
|
});
|
|
3259
3401
|
}
|
|
3260
|
-
|
|
3261
|
-
|
|
3402
|
+
//#endregion
|
|
3403
|
+
//#region src/folder-sync.ts
|
|
3404
|
+
const QMIND_FOLDER_SYNC_DEFAULT_EXTENSIONS = Object.freeze([
|
|
3405
|
+
".mdx",
|
|
3406
|
+
".md",
|
|
3407
|
+
".pdf",
|
|
3408
|
+
".txt",
|
|
3409
|
+
".docx",
|
|
3410
|
+
".pptx",
|
|
3411
|
+
".png",
|
|
3412
|
+
".jpg",
|
|
3413
|
+
".jpeg",
|
|
3414
|
+
".gif",
|
|
3415
|
+
".svg",
|
|
3416
|
+
".webp"
|
|
3417
|
+
]);
|
|
3418
|
+
function posix(path) {
|
|
3419
|
+
return path.split(sep).join("/");
|
|
3262
3420
|
}
|
|
3263
|
-
function
|
|
3264
|
-
|
|
3421
|
+
function qmindFolderSyncIndexKey(parentId, title) {
|
|
3422
|
+
return `${parentId}\u0000${title}`;
|
|
3265
3423
|
}
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
const pending = [];
|
|
3269
|
-
let done = false;
|
|
3270
|
-
let parserFailure;
|
|
3271
|
-
const parser = createParser({
|
|
3272
|
-
maxBufferSize: SSE_MAX_BUFFER_SIZE,
|
|
3273
|
-
onError(error) {
|
|
3274
|
-
if (error.type === "max-buffer-size-exceeded") parserFailure = error;
|
|
3275
|
-
},
|
|
3276
|
-
onEvent(event) {
|
|
3277
|
-
if (event.data.trim() === "[DONE]") done = true;
|
|
3278
|
-
else pending.push(event.data);
|
|
3279
|
-
}
|
|
3280
|
-
});
|
|
3281
|
-
const drain = function* () {
|
|
3282
|
-
while (pending.length > 0) {
|
|
3283
|
-
const value = pending.shift();
|
|
3284
|
-
if (value !== void 0) yield value;
|
|
3285
|
-
}
|
|
3286
|
-
};
|
|
3287
|
-
try {
|
|
3288
|
-
for await (const chunk of body) {
|
|
3289
|
-
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
3290
|
-
if (parserFailure !== void 0) throw new QMindError("STREAM_ERROR", "SSE parser buffer limit exceeded", { details: { limitBytes: SSE_MAX_BUFFER_SIZE } });
|
|
3291
|
-
yield* drain();
|
|
3292
|
-
if (done) return;
|
|
3293
|
-
}
|
|
3294
|
-
parser.feed(decoder.decode());
|
|
3295
|
-
parser.reset({ consume: true });
|
|
3296
|
-
if (parserFailure !== void 0) throw new QMindError("STREAM_ERROR", "SSE parser buffer limit exceeded", { details: { limitBytes: SSE_MAX_BUFFER_SIZE } });
|
|
3297
|
-
yield* drain();
|
|
3298
|
-
} catch (cause) {
|
|
3299
|
-
if (cause instanceof QMindError) throw cause;
|
|
3300
|
-
if (request.signal?.aborted === true) throw new QMindError("ABORTED", "QMind stream was aborted");
|
|
3301
|
-
if (isTimeoutCause(cause)) throw new QMindError("TIMEOUT", "QMind stream timed out", { cause: safeCause(cause) });
|
|
3302
|
-
throw new QMindError("STREAM_ERROR", "QMind SSE stream failed", { cause: safeCause(cause) });
|
|
3303
|
-
} finally {
|
|
3304
|
-
disposeSignal();
|
|
3305
|
-
body.destroy();
|
|
3306
|
-
}
|
|
3424
|
+
function normalizeExtensions(values) {
|
|
3425
|
+
return new Set(values.map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0).map((value) => value.startsWith(".") ? value : `.${value}`));
|
|
3307
3426
|
}
|
|
3308
|
-
function
|
|
3309
|
-
|
|
3310
|
-
const
|
|
3311
|
-
const
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3427
|
+
function validateMapping(value) {
|
|
3428
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new QMindError("INVALID_ARGUMENT", "directory ID mapping must be a JSON object");
|
|
3429
|
+
const result = {};
|
|
3430
|
+
for (const [path, id] of Object.entries(value)) {
|
|
3431
|
+
if (typeof id !== "string" || id.trim().length === 0) throw new QMindError("INVALID_ARGUMENT", "directory ID mapping values must be non-empty strings");
|
|
3432
|
+
result[posix(path)] = id.trim();
|
|
3433
|
+
}
|
|
3434
|
+
return result;
|
|
3435
|
+
}
|
|
3436
|
+
async function sha256(path) {
|
|
3437
|
+
const hash = createHash("sha256");
|
|
3438
|
+
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
3439
|
+
return hash.digest("hex");
|
|
3440
|
+
}
|
|
3441
|
+
function createNodeQMindFolderSyncFileSystem() {
|
|
3315
3442
|
return Object.freeze({
|
|
3316
|
-
async
|
|
3317
|
-
|
|
3318
|
-
await Promise.all([...unique].map(async (dispatcher) => dispatcher.close()));
|
|
3443
|
+
async isDirectory(path) {
|
|
3444
|
+
return (await stat(path).catch(() => void 0))?.isDirectory() === true;
|
|
3319
3445
|
},
|
|
3320
|
-
async
|
|
3321
|
-
const request$1 = {
|
|
3322
|
-
destination: {
|
|
3323
|
-
kind: "signed",
|
|
3324
|
-
url: download.url
|
|
3325
|
-
},
|
|
3326
|
-
idempotent: true,
|
|
3327
|
-
method: "GET",
|
|
3328
|
-
...download.signal === void 0 ? {} : { signal: download.signal }
|
|
3329
|
-
};
|
|
3330
|
-
assertWireRequest(request$1);
|
|
3331
|
-
const url = requestUrl(origin, request$1);
|
|
3332
|
-
await mkdir(dirname(download.targetPath), { recursive: true });
|
|
3333
|
-
if (download.overwrite !== true) try {
|
|
3334
|
-
await stat(download.targetPath);
|
|
3335
|
-
throw new QMindError("CONFLICT", `target already exists: ${download.targetPath}`);
|
|
3336
|
-
} catch (error) {
|
|
3337
|
-
if (error.code !== "ENOENT") throw error;
|
|
3338
|
-
}
|
|
3339
|
-
const temporary = join(dirname(download.targetPath), `.${basename(download.targetPath)}.${randomBytes(8).toString("hex")}.tmp`);
|
|
3340
|
-
let response;
|
|
3446
|
+
async readMapping(path) {
|
|
3341
3447
|
try {
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
idempotent: true,
|
|
3347
|
-
method: "GET",
|
|
3348
|
-
...download.signal === void 0 ? {} : { signal: download.signal }
|
|
3349
|
-
});
|
|
3350
|
-
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
3351
|
-
const decoded = await wireResponse(response);
|
|
3352
|
-
throw new QMindError("REMOTE_ERROR", "download request failed", {
|
|
3353
|
-
...decoded.rawBody === void 0 ? {} : { rawBody: decoded.rawBody },
|
|
3354
|
-
status: decoded.status
|
|
3355
|
-
});
|
|
3356
|
-
}
|
|
3357
|
-
await pipeline(response.body, createWriteStream(temporary, {
|
|
3358
|
-
flags: "wx",
|
|
3359
|
-
mode: 384
|
|
3360
|
-
}));
|
|
3361
|
-
const size = (await stat(temporary)).size;
|
|
3362
|
-
await chmod(temporary, 420);
|
|
3363
|
-
await commitQMindTemporaryFile(temporary, download.targetPath, download.overwrite === true);
|
|
3364
|
-
return size;
|
|
3365
|
-
} catch (cause) {
|
|
3366
|
-
response?.body.destroy();
|
|
3367
|
-
await rm(temporary, { force: true });
|
|
3368
|
-
if (cause instanceof QMindError) throw cause;
|
|
3369
|
-
throw transportFailure(request$1, cause);
|
|
3448
|
+
return validateMapping(JSON.parse(await readFile(path, "utf8")));
|
|
3449
|
+
} catch (error) {
|
|
3450
|
+
if (error instanceof QMindError) throw error;
|
|
3451
|
+
throw new QMindError("INVALID_ARGUMENT", `unable to read directory ID mapping: ${path}`);
|
|
3370
3452
|
}
|
|
3371
3453
|
},
|
|
3372
|
-
async
|
|
3373
|
-
|
|
3374
|
-
const
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
const
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
bodyTimeout: request$2.timeoutMs ?? requestTimeoutMs,
|
|
3386
|
-
dispatcher: dispatcherFor(request$2),
|
|
3387
|
-
headers: encoded.headers,
|
|
3388
|
-
headersTimeout: request$2.timeoutMs ?? requestTimeoutMs,
|
|
3389
|
-
idempotent: request$2.idempotent,
|
|
3390
|
-
method: request$2.method,
|
|
3391
|
-
signal: deadline.signal
|
|
3392
|
-
});
|
|
3393
|
-
if (attempt + 1 < attempts && RETRYABLE_STATUS.has(response.statusCode)) {
|
|
3394
|
-
await response.body.dump();
|
|
3395
|
-
await abortableSleep(250 * 2 ** attempt, request$2.signal);
|
|
3454
|
+
async scan(baseDirectory, options) {
|
|
3455
|
+
const directories = [];
|
|
3456
|
+
const files = [];
|
|
3457
|
+
let skipped = 0;
|
|
3458
|
+
const isIgnored = options.ignorePatterns.length === 0 ? () => false : picomatch([...options.ignorePatterns], { dot: true });
|
|
3459
|
+
const visit = async (directory) => {
|
|
3460
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
3461
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
3462
|
+
for (const entry of entries) {
|
|
3463
|
+
const absolute = join(directory, entry.name);
|
|
3464
|
+
const path = posix(relative(baseDirectory, absolute));
|
|
3465
|
+
if (entry.name.startsWith(".") || isIgnored(path)) {
|
|
3466
|
+
skipped += entry.isFile() ? 1 : 0;
|
|
3396
3467
|
continue;
|
|
3397
3468
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
continue;
|
|
3469
|
+
if (entry.isDirectory()) {
|
|
3470
|
+
directories.push(path);
|
|
3471
|
+
await visit(absolute);
|
|
3472
|
+
} else if (entry.isFile()) {
|
|
3473
|
+
if (options.extensions.has(extname(entry.name).toLowerCase())) files.push(path);
|
|
3474
|
+
else skipped += 1;
|
|
3405
3475
|
}
|
|
3406
|
-
} finally {
|
|
3407
|
-
deadline.dispose();
|
|
3408
3476
|
}
|
|
3477
|
+
};
|
|
3478
|
+
await visit(baseDirectory);
|
|
3479
|
+
directories.sort((left, right) => left.split("/").length - right.split("/").length || left.localeCompare(right));
|
|
3480
|
+
files.sort((left, right) => left.localeCompare(right));
|
|
3481
|
+
return {
|
|
3482
|
+
directories,
|
|
3483
|
+
files,
|
|
3484
|
+
skipped
|
|
3485
|
+
};
|
|
3486
|
+
},
|
|
3487
|
+
sha256,
|
|
3488
|
+
async writeMapping(path, mapping) {
|
|
3489
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3490
|
+
await writeFile(path, `${JSON.stringify(mapping, null, 2)}\n`, { mode: 384 });
|
|
3491
|
+
}
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
async function shouldReplaceQMindFolderFile(mode, existing, localPath, fileSystem = createNodeQMindFolderSyncFileSystem()) {
|
|
3495
|
+
if (mode === "skip") return false;
|
|
3496
|
+
if (mode === "overwrite") return true;
|
|
3497
|
+
const metadata = existing.metadata ?? {};
|
|
3498
|
+
const remoteHash = metadata.fileSha256 ?? metadata.rawFileSha256 ?? metadata.sha256;
|
|
3499
|
+
if (typeof remoteHash !== "string" || !/^[a-f0-9]{64}$/i.test(remoteHash)) return true;
|
|
3500
|
+
return (await fileSystem.sha256(localPath)).toLowerCase() !== remoteHash.toLowerCase();
|
|
3501
|
+
}
|
|
3502
|
+
function retryable(error) {
|
|
3503
|
+
if (!(error instanceof QMindError)) return true;
|
|
3504
|
+
if (error.code === "FILE_TOO_LARGE" || error.code === "INVALID_ARGUMENT" || error.code === "AUTH_REQUIRED" || error.code === "FORBIDDEN") return false;
|
|
3505
|
+
return error.status === void 0 || error.status === 408 || error.status === 429 || error.status >= 500;
|
|
3506
|
+
}
|
|
3507
|
+
async function withUploadRetry(task, sleep) {
|
|
3508
|
+
let lastFailure;
|
|
3509
|
+
for (let attempt = 0; attempt < 3; attempt += 1) try {
|
|
3510
|
+
return await task();
|
|
3511
|
+
} catch (error) {
|
|
3512
|
+
lastFailure = error;
|
|
3513
|
+
if (!retryable(error) || attempt === 2) break;
|
|
3514
|
+
await sleep(250 * 2 ** attempt);
|
|
3515
|
+
}
|
|
3516
|
+
throw lastFailure;
|
|
3517
|
+
}
|
|
3518
|
+
function defaultSleep(milliseconds) {
|
|
3519
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
|
|
3520
|
+
}
|
|
3521
|
+
function mappingPath(baseDirectory, cacheDirectory) {
|
|
3522
|
+
const cache = cacheDirectory ?? process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
|
|
3523
|
+
const name = createHash("sha1").update(baseDirectory).digest("hex").slice(0, 16);
|
|
3524
|
+
return join(cache, "qmind", "dir-ids", `${name}.json`);
|
|
3525
|
+
}
|
|
3526
|
+
function addFailure(stats, field, path) {
|
|
3527
|
+
(stats[field] ??= []).push(path);
|
|
3528
|
+
}
|
|
3529
|
+
function publicStats(stats) {
|
|
3530
|
+
return Object.freeze({
|
|
3531
|
+
...stats,
|
|
3532
|
+
...stats.failed_dirs === void 0 ? {} : { failed_dirs: Object.freeze(stats.failed_dirs) },
|
|
3533
|
+
...stats.failed_files === void 0 ? {} : { failed_files: Object.freeze(stats.failed_files) },
|
|
3534
|
+
...stats.failed_deletes === void 0 ? {} : { failed_deletes: Object.freeze(stats.failed_deletes) }
|
|
3535
|
+
});
|
|
3536
|
+
}
|
|
3537
|
+
async function synchronizeQMindFolder(host, options) {
|
|
3538
|
+
const notebookId = options.notebookId.trim();
|
|
3539
|
+
if (notebookId.length === 0) throw new QMindError("INVALID_ARGUMENT", "notebookId is required");
|
|
3540
|
+
const fileSystem = options.fileSystem ?? createNodeQMindFolderSyncFileSystem();
|
|
3541
|
+
const baseDirectory = resolve(options.baseDirectory);
|
|
3542
|
+
if (!await fileSystem.isDirectory(baseDirectory)) throw new QMindError("INVALID_ARGUMENT", `${baseDirectory} is not a directory`);
|
|
3543
|
+
const mode = options.mode ?? "skip";
|
|
3544
|
+
if (options.skipUpload === true && options.delete === true) throw new QMindError("INVALID_ARGUMENT", "delete cannot be combined with skipUpload because local files would not enter the mirror plan");
|
|
3545
|
+
const requestedConcurrency = options.concurrency ?? 3;
|
|
3546
|
+
if (!Number.isSafeInteger(requestedConcurrency) || requestedConcurrency < 1) throw new QMindError("INVALID_ARGUMENT", "concurrency must be a positive integer");
|
|
3547
|
+
const concurrency = Math.min(10, requestedConcurrency);
|
|
3548
|
+
const diagnostics = [];
|
|
3549
|
+
if (requestedConcurrency > 10) diagnostics.push(`warning: concurrency ${requestedConcurrency} exceeds maximum (10); clamped to 10`);
|
|
3550
|
+
const extensions = normalizeExtensions(options.extensions ?? QMIND_FOLDER_SYNC_DEFAULT_EXTENSIONS);
|
|
3551
|
+
const tree = await fileSystem.scan(baseDirectory, {
|
|
3552
|
+
extensions,
|
|
3553
|
+
ignorePatterns: options.ignorePatterns ?? []
|
|
3554
|
+
});
|
|
3555
|
+
const remote = await host.client.listAllSources(notebookId);
|
|
3556
|
+
const remoteIndex = new Map(remote.map((source) => [qmindFolderSyncIndexKey(source.parentSourceId, source.title), source]));
|
|
3557
|
+
const remoteById = new Map(remote.map((source) => [source.id, source]));
|
|
3558
|
+
const stats = {
|
|
3559
|
+
dirs_created: 0,
|
|
3560
|
+
dirs_reused: 0,
|
|
3561
|
+
dirs_failed: 0,
|
|
3562
|
+
dirs_refresh_failed: 0,
|
|
3563
|
+
files_uploaded: 0,
|
|
3564
|
+
files_skipped: options.skipUpload === true ? 0 : tree.skipped,
|
|
3565
|
+
files_already_exist: 0,
|
|
3566
|
+
files_failed: 0,
|
|
3567
|
+
...options.delete === true ? {
|
|
3568
|
+
delete_failed: 0,
|
|
3569
|
+
dirs_deleted: 0,
|
|
3570
|
+
files_deleted: 0
|
|
3571
|
+
} : {},
|
|
3572
|
+
...options.dryRun === true ? { dry_run: true } : {}
|
|
3573
|
+
};
|
|
3574
|
+
const configuredMapping = options.directoryIdsPath === void 0 ? {} : await fileSystem.readMapping(options.directoryIdsPath);
|
|
3575
|
+
const directoryIds = {};
|
|
3576
|
+
const desiredKeys = /* @__PURE__ */ new Set();
|
|
3577
|
+
for (const directory of tree.directories) {
|
|
3578
|
+
const parentPath = posix(dirname(directory)) === "." ? "" : posix(dirname(directory));
|
|
3579
|
+
const parentId = directoryIds[parentPath] ?? "";
|
|
3580
|
+
if (parentPath.length > 0 && parentId.length === 0) {
|
|
3581
|
+
stats.dirs_failed += 1;
|
|
3582
|
+
addFailure(stats, "failed_dirs", directory);
|
|
3583
|
+
continue;
|
|
3584
|
+
}
|
|
3585
|
+
const title = basename(directory);
|
|
3586
|
+
const key = qmindFolderSyncIndexKey(parentId, title);
|
|
3587
|
+
desiredKeys.add(key);
|
|
3588
|
+
const mappedId = configuredMapping[directory];
|
|
3589
|
+
const mapped = mappedId === void 0 ? void 0 : remoteById.get(mappedId);
|
|
3590
|
+
const existing = mappedId === void 0 || mapped === void 0 ? remoteIndex.get(key) : mapped;
|
|
3591
|
+
if (existing?.isDir === true && existing.parentSourceId === parentId && existing.title === title) {
|
|
3592
|
+
directoryIds[directory] = existing.id;
|
|
3593
|
+
stats.dirs_reused += 1;
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3596
|
+
if (existing !== void 0) {
|
|
3597
|
+
stats.dirs_failed += 1;
|
|
3598
|
+
addFailure(stats, "failed_dirs", directory);
|
|
3599
|
+
continue;
|
|
3600
|
+
}
|
|
3601
|
+
if (options.dryRun === true) {
|
|
3602
|
+
directoryIds[directory] = `dry-run:${directory}`;
|
|
3603
|
+
stats.dirs_created += 1;
|
|
3604
|
+
continue;
|
|
3605
|
+
}
|
|
3606
|
+
try {
|
|
3607
|
+
const created = await host.client.createDirectory(notebookId, {
|
|
3608
|
+
parentSourceId: parentId,
|
|
3609
|
+
path: title,
|
|
3610
|
+
title
|
|
3611
|
+
});
|
|
3612
|
+
directoryIds[directory] = created.id;
|
|
3613
|
+
remoteIndex.set(key, created);
|
|
3614
|
+
remoteById.set(created.id, created);
|
|
3615
|
+
stats.dirs_created += 1;
|
|
3616
|
+
} catch {
|
|
3617
|
+
stats.dirs_failed += 1;
|
|
3618
|
+
addFailure(stats, "failed_dirs", directory);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
let savedMappingPath;
|
|
3622
|
+
if (options.directoryIdsPath === void 0 && options.dryRun !== true) {
|
|
3623
|
+
savedMappingPath = options.saveDirectoryIdsPath ?? mappingPath(baseDirectory, options.cacheDirectory);
|
|
3624
|
+
await fileSystem.writeMapping(savedMappingPath, directoryIds);
|
|
3625
|
+
diagnostics.push(`directory mapping saved: ${savedMappingPath}`);
|
|
3626
|
+
}
|
|
3627
|
+
if (options.skipUpload !== true) {
|
|
3628
|
+
const limit = pLimit(concurrency);
|
|
3629
|
+
await Promise.all(tree.files.map((file) => limit(async () => {
|
|
3630
|
+
const absolute = join(baseDirectory, file);
|
|
3631
|
+
const parentPath = posix(dirname(file)) === "." ? "" : posix(dirname(file));
|
|
3632
|
+
const parentId = directoryIds[parentPath] ?? "";
|
|
3633
|
+
if (parentPath.length > 0 && parentId.length === 0) {
|
|
3634
|
+
stats.files_failed += 1;
|
|
3635
|
+
addFailure(stats, "failed_files", file);
|
|
3636
|
+
return;
|
|
3409
3637
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
const deadline = requestDeadline(request$3.signal, request$3.timeoutMs ?? requestTimeoutMs);
|
|
3419
|
-
try {
|
|
3420
|
-
response = await request(url, {
|
|
3421
|
-
...encoded.body === void 0 ? {} : { body: encoded.body },
|
|
3422
|
-
bodyTimeout: streamIdleTimeoutMs,
|
|
3423
|
-
dispatcher: dispatchers.host,
|
|
3424
|
-
headers: {
|
|
3425
|
-
accept: "text/event-stream",
|
|
3426
|
-
...encoded.headers
|
|
3427
|
-
},
|
|
3428
|
-
headersTimeout: request$3.timeoutMs ?? requestTimeoutMs,
|
|
3429
|
-
idempotent: request$3.idempotent,
|
|
3430
|
-
method: request$3.method,
|
|
3431
|
-
signal: deadline.signal
|
|
3432
|
-
});
|
|
3433
|
-
} catch (cause) {
|
|
3434
|
-
deadline.dispose();
|
|
3435
|
-
throw transportFailure(request$3, cause, deadline.timedOut);
|
|
3638
|
+
const title = basename(file);
|
|
3639
|
+
const key = qmindFolderSyncIndexKey(parentId, title);
|
|
3640
|
+
desiredKeys.add(key);
|
|
3641
|
+
const existing = remoteIndex.get(key);
|
|
3642
|
+
if (existing?.isDir === true) {
|
|
3643
|
+
stats.files_failed += 1;
|
|
3644
|
+
addFailure(stats, "failed_files", file);
|
|
3645
|
+
return;
|
|
3436
3646
|
}
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
return {
|
|
3441
|
-
...await wireResponse(response),
|
|
3442
|
-
kind: "response"
|
|
3443
|
-
};
|
|
3444
|
-
} finally {
|
|
3445
|
-
deadline.dispose();
|
|
3647
|
+
if (existing !== void 0 && !await shouldReplaceQMindFolderFile(mode, existing, absolute, fileSystem)) {
|
|
3648
|
+
stats.files_already_exist += 1;
|
|
3649
|
+
return;
|
|
3446
3650
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3651
|
+
if (options.dryRun === true) {
|
|
3652
|
+
stats.files_uploaded += 1;
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
try {
|
|
3656
|
+
const hash = await fileSystem.sha256(absolute);
|
|
3657
|
+
const uploaded = await withUploadRetry(async () => {
|
|
3658
|
+
const binary = await host.createFileSource(absolute);
|
|
3659
|
+
return await host.client.uploadSource(notebookId, binary, {
|
|
3660
|
+
parentSourceId: parentId,
|
|
3661
|
+
path: file,
|
|
3662
|
+
sha256: hash,
|
|
3663
|
+
title
|
|
3664
|
+
});
|
|
3665
|
+
}, options.sleep ?? defaultSleep);
|
|
3666
|
+
if (existing !== void 0) await host.client.deleteSource(notebookId, existing.id);
|
|
3667
|
+
remoteIndex.set(key, uploaded);
|
|
3668
|
+
remoteById.set(uploaded.id, uploaded);
|
|
3669
|
+
stats.files_uploaded += 1;
|
|
3670
|
+
} catch {
|
|
3671
|
+
stats.files_failed += 1;
|
|
3672
|
+
addFailure(stats, "failed_files", file);
|
|
3673
|
+
}
|
|
3674
|
+
})));
|
|
3675
|
+
}
|
|
3676
|
+
if (options.dryRun !== true) for (const [directory, id] of Object.entries(directoryIds)) {
|
|
3677
|
+
if (id.startsWith("dry-run:")) continue;
|
|
3678
|
+
try {
|
|
3679
|
+
await host.client.updateSource(notebookId, id, {
|
|
3680
|
+
isDir: true,
|
|
3681
|
+
title: basename(directory)
|
|
3682
|
+
});
|
|
3683
|
+
} catch {
|
|
3684
|
+
stats.dirs_refresh_failed += 1;
|
|
3453
3685
|
}
|
|
3454
|
-
});
|
|
3455
|
-
}
|
|
3456
|
-
const MIME_TYPES = Object.freeze({
|
|
3457
|
-
".csv": "text/csv",
|
|
3458
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
3459
|
-
".gif": "image/gif",
|
|
3460
|
-
".jpeg": "image/jpeg",
|
|
3461
|
-
".jpg": "image/jpeg",
|
|
3462
|
-
".md": "text/markdown",
|
|
3463
|
-
".mdx": "text/markdown",
|
|
3464
|
-
".pdf": "application/pdf",
|
|
3465
|
-
".png": "image/png",
|
|
3466
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
3467
|
-
".svg": "image/svg+xml",
|
|
3468
|
-
".txt": "text/plain",
|
|
3469
|
-
".webp": "image/webp",
|
|
3470
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
3471
|
-
});
|
|
3472
|
-
function detectQMindMimeType(filename) {
|
|
3473
|
-
return MIME_TYPES[extname(filename).toLowerCase()] ?? "application/octet-stream";
|
|
3474
|
-
}
|
|
3475
|
-
async function createQMindFileSource(path, options = {}) {
|
|
3476
|
-
let info;
|
|
3477
|
-
try {
|
|
3478
|
-
info = await stat(path);
|
|
3479
|
-
} catch {
|
|
3480
|
-
throw invalidArgument("source path must reference a readable file", "path");
|
|
3481
3686
|
}
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
//#region src/host.ts
|
|
3496
|
-
function defaultReporter() {
|
|
3497
|
-
return Object.freeze({
|
|
3498
|
-
info(message) {
|
|
3499
|
-
console.error(message);
|
|
3500
|
-
},
|
|
3501
|
-
warn(message) {
|
|
3502
|
-
console.error(message);
|
|
3687
|
+
const preDeleteFailures = stats.files_failed + stats.dirs_failed;
|
|
3688
|
+
if (options.delete === true && preDeleteFailures === 0) {
|
|
3689
|
+
const deletions = remote.filter((source) => !desiredKeys.has(qmindFolderSyncIndexKey(source.parentSourceId, source.title))).sort((left, right) => {
|
|
3690
|
+
if (left.isDir !== right.isDir) return left.isDir ? 1 : -1;
|
|
3691
|
+
return right.path.split("/").length - left.path.split("/").length;
|
|
3692
|
+
});
|
|
3693
|
+
for (const source of deletions) try {
|
|
3694
|
+
if (options.dryRun !== true) await host.client.deleteSource(notebookId, source.id);
|
|
3695
|
+
if (source.isDir) stats.dirs_deleted = (stats.dirs_deleted ?? 0) + 1;
|
|
3696
|
+
else stats.files_deleted = (stats.files_deleted ?? 0) + 1;
|
|
3697
|
+
} catch {
|
|
3698
|
+
stats.delete_failed = (stats.delete_failed ?? 0) + 1;
|
|
3699
|
+
(stats.failed_deletes ??= []).push(source.path || source.title);
|
|
3503
3700
|
}
|
|
3504
|
-
});
|
|
3505
|
-
}
|
|
3506
|
-
function defaultBrowser() {
|
|
3507
|
-
return Object.freeze({ async open(url) {
|
|
3508
|
-
(await open(url, { wait: false })).unref();
|
|
3509
|
-
} });
|
|
3510
|
-
}
|
|
3511
|
-
async function tlsAuthority(path) {
|
|
3512
|
-
if (path === void 0) return void 0;
|
|
3513
|
-
try {
|
|
3514
|
-
return await readFile(path, "utf8");
|
|
3515
|
-
} catch {
|
|
3516
|
-
throw new QMindError("HOST_POLICY_ERROR", "unable to read the configured TLS CA file", { details: { field: "tlsCaFile" } });
|
|
3517
3701
|
}
|
|
3518
|
-
|
|
3519
|
-
function configurationOptions(options, reporter) {
|
|
3702
|
+
const exitCode = stats.files_failed > 0 || stats.dirs_failed > 0 || stats.dirs_refresh_failed > 0 || (stats.delete_failed ?? 0) > 0 ? 1 : 0;
|
|
3520
3703
|
return {
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
...
|
|
3524
|
-
|
|
3525
|
-
...options.legacyEnvironmentPath === void 0 ? {} : { legacyEnvironmentPath: options.legacyEnvironmentPath },
|
|
3526
|
-
...reporter === void 0 ? {} : { reporter },
|
|
3527
|
-
...options.stdinIsTTY === void 0 ? {} : { stdinIsTTY: options.stdinIsTTY }
|
|
3528
|
-
};
|
|
3529
|
-
}
|
|
3530
|
-
async function createQMindCliHost(options = {}) {
|
|
3531
|
-
const reporter = options.adapters?.reporter ?? defaultReporter();
|
|
3532
|
-
const clock = options.adapters?.clock ?? createSystemClock();
|
|
3533
|
-
const vault = createFileCredentialVault({
|
|
3534
|
-
homeDirectory: (await loadQMindCliConfig(configurationOptions(options))).runtime.homeDirectory,
|
|
3535
|
-
now: () => clock.now(),
|
|
3536
|
-
secretStore: options.adapters?.secretStore ?? createOptionalKeyringSecretStore()
|
|
3537
|
-
});
|
|
3538
|
-
const credentials = await vault.load();
|
|
3539
|
-
const resolved = await loadQMindCliConfig({
|
|
3540
|
-
...configurationOptions(options, reporter),
|
|
3541
|
-
...credentials?.sashOrigin ? { credentialSashOrigin: credentials.sashOrigin } : {},
|
|
3542
|
-
reporter
|
|
3543
|
-
});
|
|
3544
|
-
const ca = await tlsAuthority(resolved.tlsCaFile);
|
|
3545
|
-
const nodeOptions = {
|
|
3546
|
-
...options.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: options.connectTimeoutMs },
|
|
3547
|
-
...resolved.debug ? { debugLog: (message) => reporter.info(message) } : {},
|
|
3548
|
-
environment: resolved.environment,
|
|
3549
|
-
...options.maxRetries === void 0 ? {} : { maxRetries: options.maxRetries },
|
|
3550
|
-
origin: resolved.runtime.sashOrigin,
|
|
3551
|
-
proxyFromEnvironment: resolved.proxyFromEnvironment,
|
|
3552
|
-
requestTimeoutMs: resolved.requestTimeoutMs,
|
|
3553
|
-
streamIdleTimeoutMs: resolved.streamIdleTimeoutMs,
|
|
3554
|
-
tls: {
|
|
3555
|
-
...ca === void 0 ? {} : { ca },
|
|
3556
|
-
rejectUnauthorized: resolved.tlsRejectUnauthorized
|
|
3557
|
-
}
|
|
3704
|
+
diagnostics: Object.freeze(diagnostics),
|
|
3705
|
+
exitCode,
|
|
3706
|
+
...savedMappingPath === void 0 ? {} : { mappingPath: savedMappingPath },
|
|
3707
|
+
stats: publicStats(stats)
|
|
3558
3708
|
};
|
|
3559
|
-
const ownedTransport = options.adapters?.transport === void 0 ? createQMindNodeTransport(nodeOptions) : void 0;
|
|
3560
|
-
const rawTransport = options.adapters?.transport ?? ownedTransport;
|
|
3561
|
-
if (rawTransport === void 0) throw new QMindError("HOST_POLICY_ERROR", "unable to initialize the QMind transport");
|
|
3562
|
-
const auth = createQMindCliAuthManager({
|
|
3563
|
-
browser: options.adapters?.browser ?? defaultBrowser(),
|
|
3564
|
-
clock,
|
|
3565
|
-
config: resolved.runtime,
|
|
3566
|
-
credentials,
|
|
3567
|
-
explicitToken: resolved.token,
|
|
3568
|
-
reporter,
|
|
3569
|
-
transport: rawTransport,
|
|
3570
|
-
vault
|
|
3571
|
-
});
|
|
3572
|
-
const client = createQMindClient({
|
|
3573
|
-
access: "sash",
|
|
3574
|
-
client: resolved.runtime.client,
|
|
3575
|
-
transport: createAuthenticatedQMindTransport(rawTransport, auth),
|
|
3576
|
-
userId: resolved.runtime.userId
|
|
3577
|
-
});
|
|
3578
|
-
return Object.freeze({
|
|
3579
|
-
client,
|
|
3580
|
-
async close() {
|
|
3581
|
-
await ownedTransport?.close();
|
|
3582
|
-
},
|
|
3583
|
-
config: resolved.runtime,
|
|
3584
|
-
createFileSource: createQMindFileSource,
|
|
3585
|
-
async downloadToFile(downloadOptions) {
|
|
3586
|
-
const downloader = options.adapters?.downloader ?? ownedTransport;
|
|
3587
|
-
if (downloader === void 0) throw new QMindError("UNSUPPORTED_OPERATION", "the injected CLI transport does not provide file downloads");
|
|
3588
|
-
return await downloader.downloadToFile(downloadOptions);
|
|
3589
|
-
},
|
|
3590
|
-
login: auth.login,
|
|
3591
|
-
logout: auth.logout
|
|
3592
|
-
});
|
|
3593
3709
|
}
|
|
3594
3710
|
//#endregion
|
|
3595
3711
|
//#region src/output.ts
|
|
@@ -4435,14 +4551,12 @@ function commonFlags(options) {
|
|
|
4435
4551
|
const dashboard = optionalString(options, "dashboard");
|
|
4436
4552
|
const sash = optionalString(options, "sash");
|
|
4437
4553
|
const token = optionalString(options, "token");
|
|
4438
|
-
const user = optionalString(options, "user");
|
|
4439
4554
|
return {
|
|
4440
4555
|
...client === void 0 ? {} : { client },
|
|
4441
4556
|
...dashboard === void 0 ? {} : { dashboard },
|
|
4442
4557
|
...options.nonInteractive === true ? { nonInteractive: true } : {},
|
|
4443
4558
|
...sash === void 0 ? {} : { sash },
|
|
4444
|
-
...token === void 0 ? {} : { token }
|
|
4445
|
-
...user === void 0 ? {} : { user }
|
|
4559
|
+
...token === void 0 ? {} : { token }
|
|
4446
4560
|
};
|
|
4447
4561
|
}
|
|
4448
4562
|
async function hostFor(context, options, environmentOverrides = {}) {
|
|
@@ -4475,7 +4589,7 @@ function render(context, options, presentation) {
|
|
|
4475
4589
|
context.io.stdout(renderQMindCliPresentation(presentation, formatOf(options)));
|
|
4476
4590
|
}
|
|
4477
4591
|
function withCommon(command, output = true) {
|
|
4478
|
-
command.option("--token <token>", "Bearer access token").option("--sash <url>", "Sash server origin").option("--dashboard <url>", "Dashboard origin").option("--org <id>", "organization ID (deprecated and ignored)").option("--
|
|
4592
|
+
command.option("--token <token>", "Bearer access token").option("--sash <url>", "Sash server origin").option("--dashboard <url>", "Dashboard origin").option("--org <id>", "organization ID (deprecated and ignored)").option("--client <name>", "originating client identifier").option("--non-interactive", "never open a browser when credentials are missing");
|
|
4479
4593
|
if (output) command.option("--format <format>", "output format: table | json | agent | ndjson", "table");
|
|
4480
4594
|
return command;
|
|
4481
4595
|
}
|
|
@@ -4833,7 +4947,7 @@ function registerUpdateCommand(context, program) {
|
|
|
4833
4947
|
publicKey: String(raw.publicKey ?? "")
|
|
4834
4948
|
});
|
|
4835
4949
|
if (result.updated) context.io.stdout(`[self-update] updated ${QMIND_CLI_VERSION} -> ${result.version}\n`);
|
|
4836
|
-
else if (result.version === "
|
|
4950
|
+
else if (result.version === "2.0.0".replace(/^v/, "")) context.io.stdout(`[self-update] already current: ${result.version}\n`);
|
|
4837
4951
|
else context.io.stdout(`[self-update] verified available version: ${result.version}\n`);
|
|
4838
4952
|
});
|
|
4839
4953
|
}
|