wiki-graph-sdk 0.7.0 → 0.8.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.
@@ -3,7 +3,10 @@ import { resolve as __WIKIGRAPH_RESOLVE__ } from "path";
3
3
  globalThis.__WIKIGRAPH_DATA_DIR__ ??= __WIKIGRAPH_RESOLVE__(__WIKIGRAPH_FILE_URL_TO_PATH__(new URL("./data", import.meta.url)));
4
4
 
5
5
  // src/conversions.ts
6
+ import { createHash } from "crypto";
7
+ import { createReadStream } from "fs";
6
8
  import { rm as rm2 } from "fs/promises";
9
+ import { basename as basename3 } from "path";
7
10
  import {
8
11
  WikiGraph
9
12
  } from "wiki-graph-core";
@@ -313,6 +316,14 @@ function decodeNodeResourceIdentity(identity, kind) {
313
316
  return void 0;
314
317
  }
315
318
  }
319
+ function getNodeResourcePath(resource) {
320
+ const brandedPath = getBrandedNodeResourcePath(resource, resource.kind);
321
+ if (brandedPath !== void 0) return brandedPath;
322
+ if (typeof resource === "object" && resource !== null && "path" in resource && typeof resource.path === "string") {
323
+ return resource.path;
324
+ }
325
+ throw new TypeError("Expected a Node File or Directory resource");
326
+ }
316
327
  function assertChildName(name) {
317
328
  if (name.length === 0 || name === "." || name === ".." || pathModule.isAbsolute(name) || name.includes("/") || name.includes("\\")) {
318
329
  throw new TypeError(`Directory child must be a relative name: ${name}`);
@@ -324,42 +335,42 @@ var NodeDatabaseConnection = class {
324
335
  }
325
336
  database;
326
337
  async close() {
327
- await new Promise((resolve3, reject) => {
338
+ await new Promise((resolve4, reject) => {
328
339
  this.database.close((error) => {
329
340
  if (error) reject(error);
330
- else resolve3();
341
+ else resolve4();
331
342
  });
332
343
  });
333
344
  }
334
345
  async execute(sql) {
335
- await new Promise((resolve3, reject) => {
346
+ await new Promise((resolve4, reject) => {
336
347
  this.database.exec(sql, (error) => {
337
348
  if (error) reject(error);
338
- else resolve3();
349
+ else resolve4();
339
350
  });
340
351
  });
341
352
  }
342
353
  async queryAll(sql, params = []) {
343
- return await new Promise((resolve3, reject) => {
354
+ return await new Promise((resolve4, reject) => {
344
355
  this.database.all(sql, [...params], (error, rows) => {
345
356
  if (error) reject(error);
346
- else resolve3(rows);
357
+ else resolve4(rows);
347
358
  });
348
359
  });
349
360
  }
350
361
  async queryOne(sql, params = []) {
351
- return await new Promise((resolve3, reject) => {
362
+ return await new Promise((resolve4, reject) => {
352
363
  this.database.get(sql, [...params], (error, row) => {
353
364
  if (error) reject(error);
354
- else resolve3(row);
365
+ else resolve4(row);
355
366
  });
356
367
  });
357
368
  }
358
369
  async run(sql, params = []) {
359
- await new Promise((resolve3, reject) => {
370
+ await new Promise((resolve4, reject) => {
360
371
  this.database.run(sql, [...params], (error) => {
361
372
  if (error) reject(error);
362
- else resolve3();
373
+ else resolve4();
363
374
  });
364
375
  });
365
376
  }
@@ -373,10 +384,10 @@ async function openNativeNodeDatabase(file, flags) {
373
384
  if (path === void 0) {
374
385
  throw new TypeError("The Node database adapter requires a NodeFile");
375
386
  }
376
- return await new Promise((resolve3, reject) => {
387
+ return await new Promise((resolve4, reject) => {
377
388
  const database = new nodeSqlite3.Database(path, flags, (error) => {
378
389
  if (error) reject(error);
379
- else resolve3(database);
390
+ else resolve4(database);
380
391
  });
381
392
  });
382
393
  }
@@ -385,19 +396,19 @@ async function openNodeZip(file) {
385
396
  if (path === void 0) {
386
397
  throw new TypeError("The Node ZIP adapter requires a NodeFile");
387
398
  }
388
- const zipFile = await new Promise((resolve3, reject) => {
399
+ const zipFile = await new Promise((resolve4, reject) => {
389
400
  yauzl.open(
390
401
  path,
391
402
  { autoClose: false, lazyEntries: true },
392
403
  (error, opened) => {
393
404
  if (error || opened === void 0)
394
405
  reject(error ?? new Error("Cannot open ZIP"));
395
- else resolve3(opened);
406
+ else resolve4(opened);
396
407
  }
397
408
  );
398
409
  });
399
410
  const entries = await new Promise(
400
- (resolve3, reject) => {
411
+ (resolve4, reject) => {
401
412
  const discovered = /* @__PURE__ */ new Map();
402
413
  const rejectAndClose = (error) => {
403
414
  zipFile.close();
@@ -407,7 +418,7 @@ async function openNodeZip(file) {
407
418
  };
408
419
  const resolveEntries = () => {
409
420
  zipFile.off("error", rejectAndClose);
410
- resolve3(discovered);
421
+ resolve4(discovered);
411
422
  };
412
423
  zipFile.on("entry", (entry) => {
413
424
  if (!entry.fileName.endsWith("/")) {
@@ -517,12 +528,12 @@ async function collectDecodedNodeStreamRange(input, offset, length) {
517
528
  return Buffer.concat(chunks, collected);
518
529
  }
519
530
  async function openNodeZipEntryStream(zipFile, entry, options) {
520
- return await new Promise((resolve3, reject) => {
531
+ return await new Promise((resolve4, reject) => {
521
532
  const callback = (error, opened) => {
522
533
  if (error || opened === void 0) {
523
534
  reject(error ?? new Error(`Cannot read ZIP entry: ${entry.fileName}`));
524
535
  } else {
525
- resolve3(opened);
536
+ resolve4(opened);
526
537
  }
527
538
  };
528
539
  if (options === void 0) zipFile.openReadStream(entry, callback);
@@ -653,11 +664,11 @@ async function writeNodeStream(input, writer) {
653
664
  );
654
665
  }
655
666
  async function collectNodeStream(input) {
656
- return await new Promise((resolve3, reject) => {
667
+ return await new Promise((resolve4, reject) => {
657
668
  const chunks = [];
658
669
  input.on("data", (chunk) => chunks.push(chunk));
659
670
  input.once("error", reject);
660
- input.once("end", () => resolve3(Buffer.concat(chunks)));
671
+ input.once("end", () => resolve4(Buffer.concat(chunks)));
661
672
  });
662
673
  }
663
674
  var nodeWikiGraphPlatform = {
@@ -1210,6 +1221,116 @@ function resolveWikiGraphRuntimePath(path) {
1210
1221
  return isAbsolute2(path) ? path : resolve2(context.cwd, path);
1211
1222
  }
1212
1223
 
1224
+ // src/archive/target.ts
1225
+ import { realpath, stat as stat2 } from "fs/promises";
1226
+ import { basename as basename2, dirname as dirname2, isAbsolute as isAbsolute3, join as join3, relative, resolve as resolve3 } from "path";
1227
+ import {
1228
+ formatLocatedWikiGraphUri,
1229
+ formatWikiGraphLibraryUri,
1230
+ listWikiGraphLibraries,
1231
+ listWikiGraphLibraryArchives,
1232
+ parseLocatedWikiGraphUri,
1233
+ parseWikiGraphLibraryUri,
1234
+ resolveWikiGraphLibrary,
1235
+ resolveWikiGraphLibraryArchiveFile
1236
+ } from "wiki-graph-core";
1237
+ var WikiGraphArchiveOwnershipError = class extends Error {
1238
+ code = "WIKI_GRAPH_ARCHIVE_OWNERSHIP_MISMATCH";
1239
+ constructor(message) {
1240
+ super(message);
1241
+ this.name = "WikiGraphArchiveOwnershipError";
1242
+ }
1243
+ };
1244
+ async function assertStandaloneWikiGraphArchivePath(inputPath) {
1245
+ const path = resolveWikiGraphRuntimePath(inputPath);
1246
+ const candidate = await canonicalPath(path);
1247
+ const candidateIdentity = await readFileIdentity(path);
1248
+ const libraries = await listWikiGraphLibraries();
1249
+ const matches = await findMatchingMembers(
1250
+ libraries,
1251
+ candidate,
1252
+ candidateIdentity
1253
+ );
1254
+ if (matches.length > 0) {
1255
+ const match = requireUnambiguousManagedMatch(path, matches);
1256
+ throw new WikiGraphArchiveOwnershipError(
1257
+ `Standalone archive path belongs to registered library ${formatWikiGraphLibraryUri(
1258
+ match.library.isDefault ? void 0 : match.library.publicId
1259
+ )}: ${path}. Use the library UUID URI ${match.archive.uri} instead.`
1260
+ );
1261
+ }
1262
+ for (const library of libraries) {
1263
+ const rootPath = getNodeResourcePath(library.folder);
1264
+ const root = await canonicalPath(rootPath);
1265
+ if (!isWithin(root, candidate)) continue;
1266
+ throw new WikiGraphArchiveOwnershipError(
1267
+ `Standalone archive path belongs to registered library ${formatWikiGraphLibraryUri(
1268
+ library.isDefault ? void 0 : library.publicId
1269
+ )}: ${path}. Scan the library, then address the archive by its library UUID URI.`
1270
+ );
1271
+ }
1272
+ return path;
1273
+ }
1274
+ async function canonicalPath(path) {
1275
+ try {
1276
+ return await realpath(path);
1277
+ } catch {
1278
+ const absolute = resolve3(path);
1279
+ const parent = dirname2(absolute);
1280
+ return parent === absolute ? absolute : join3(await canonicalPath(parent), basename2(absolute));
1281
+ }
1282
+ }
1283
+ function isWithin(root, candidate) {
1284
+ const child = relative(root, candidate);
1285
+ return child === "" || !child.startsWith("..") && !isAbsolute3(child);
1286
+ }
1287
+ async function findMatchingMembers(libraries, candidate, candidateIdentity) {
1288
+ const members = [];
1289
+ for (const library of libraries) {
1290
+ const archives = await listWikiGraphLibraryArchives({
1291
+ isDefault: library.isDefault,
1292
+ kind: "scope",
1293
+ ...library.isDefault ? {} : { publicId: library.publicId }
1294
+ });
1295
+ for (const archive of archives) {
1296
+ if (archive.file === void 0) continue;
1297
+ members.push({
1298
+ archive,
1299
+ library,
1300
+ memberPath: getNodeResourcePath(archive.file)
1301
+ });
1302
+ }
1303
+ }
1304
+ const exact = [];
1305
+ for (const member of members) {
1306
+ if (await canonicalPath(member.memberPath) === candidate) {
1307
+ exact.push(member);
1308
+ }
1309
+ }
1310
+ if (exact.length > 0 || candidateIdentity === void 0) return exact;
1311
+ const identityMatches = [];
1312
+ for (const member of members) {
1313
+ if (await readFileIdentity(member.memberPath) === candidateIdentity) {
1314
+ identityMatches.push(member);
1315
+ }
1316
+ }
1317
+ return identityMatches;
1318
+ }
1319
+ function requireUnambiguousManagedMatch(path, matches) {
1320
+ if (matches.length === 1) return matches[0];
1321
+ throw new WikiGraphArchiveOwnershipError(
1322
+ `Archive file identity is ambiguous across managed archives: ${path}. Use an explicit library UUID URI (${matches.map((match) => match.archive.uri).join(", ")}).`
1323
+ );
1324
+ }
1325
+ async function readFileIdentity(path) {
1326
+ try {
1327
+ const value = await stat2(path);
1328
+ return `${value.dev}:${value.ino}`;
1329
+ } catch {
1330
+ return void 0;
1331
+ }
1332
+ }
1333
+
1213
1334
  // src/conversions.ts
1214
1335
  var WikiGraphConversionManager = class {
1215
1336
  #runtime;
@@ -1224,6 +1345,9 @@ var WikiGraphConversionManager = class {
1224
1345
  if (options.targetStage !== void 0 && options.output.format !== "wikg") {
1225
1346
  throw new Error("targetStage is only supported for wikg output.");
1226
1347
  }
1348
+ if (options.output.format === "pcex") {
1349
+ throw new Error("pcex is supported only as a conversion input.");
1350
+ }
1227
1351
  if (options.targetStage !== void 0 && options.input.format === "wikg") {
1228
1352
  throw new Error(
1229
1353
  "targetStage is only supported when creating a wikg archive."
@@ -1242,7 +1366,17 @@ var WikiGraphConversionManager = class {
1242
1366
  options.digestDirectory,
1243
1367
  requiresDigest
1244
1368
  );
1369
+ let chapterCount;
1370
+ let chapterPaths;
1245
1371
  const write = async (archive) => {
1372
+ if (requiresDigest) await options.onSourceImported?.();
1373
+ if (typeof archive.readToc === "function") {
1374
+ const toc = await archive.readToc();
1375
+ chapterCount = toc === void 0 ? void 0 : countTocChapters(toc.items);
1376
+ if (options.output.format === "wikg") {
1377
+ chapterPaths = toc === void 0 ? [] : collectTocChapterPaths(toc.items);
1378
+ }
1379
+ }
1246
1380
  await writeArchive(archive, options.output.path, options.output.format);
1247
1381
  };
1248
1382
  if (options.input.format === "wikg") {
@@ -1250,7 +1384,9 @@ var WikiGraphConversionManager = class {
1250
1384
  throw new Error("wikg input requires a file path.");
1251
1385
  }
1252
1386
  await app.openSession(
1253
- new NodeFile(resolveWikiGraphRuntimePath(options.input.path)),
1387
+ new NodeFile(
1388
+ await assertStandaloneWikiGraphArchivePath(options.input.path)
1389
+ ),
1254
1390
  write
1255
1391
  );
1256
1392
  } else if ("stream" in options.input) {
@@ -1275,16 +1411,29 @@ var WikiGraphConversionManager = class {
1275
1411
  };
1276
1412
  if (options.input.format === "epub") {
1277
1413
  await app.digestEpubSession(digestOptions, write);
1414
+ } else if (options.input.format === "pcex") {
1415
+ await app.digestPcexSession(
1416
+ {
1417
+ ...digestOptions,
1418
+ pdfDigest: options.input.sourcePdf.digest,
1419
+ ...options.input.sourcePdf.name === void 0 ? {} : { pdfName: options.input.sourcePdf.name }
1420
+ },
1421
+ write
1422
+ );
1278
1423
  } else if (options.input.format === "markdown") {
1279
1424
  await app.digestMarkdownSession(digestOptions, write);
1280
1425
  } else {
1281
1426
  await app.digestTxtSession(digestOptions, write);
1282
1427
  }
1283
1428
  }
1429
+ const sourceArtifact = await resolveSourceArtifact(options.input);
1284
1430
  return {
1431
+ ...chapterCount === void 0 ? {} : { chapterCount },
1432
+ ...chapterPaths === void 0 ? {} : { chapterPaths },
1285
1433
  inputFormat: options.input.format,
1286
1434
  outputFormat: options.output.format,
1287
- outputPath: options.output.path
1435
+ outputPath: options.output.path,
1436
+ ...sourceArtifact === void 0 ? {} : { sourceArtifact }
1288
1437
  };
1289
1438
  }
1290
1439
  };
@@ -1297,11 +1446,67 @@ async function prepareDigestDirectory(path, required) {
1297
1446
  return new NodeDirectory(resolved);
1298
1447
  }
1299
1448
  async function writeArchive(archive, path, format) {
1300
- const file = new NodeFile(resolveWikiGraphRuntimePath(path));
1449
+ const outputPath = format === "wikg" ? await assertStandaloneWikiGraphArchivePath(path) : resolveWikiGraphRuntimePath(path);
1450
+ const file = new NodeFile(outputPath);
1301
1451
  if (format === "epub") await archive.exportEpub(file);
1302
1452
  else if (format === "wikg") await archive.saveAs(file);
1453
+ else if (format === "pcex") throw new Error("pcex output is not supported.");
1303
1454
  else await archive.exportText(file);
1304
1455
  }
1456
+ async function resolveSourceArtifact(input) {
1457
+ if (!("path" in input) || input.format === "wikg") return void 0;
1458
+ if (input.format === "pcex") {
1459
+ return {
1460
+ digest: input.sourcePdf.digest.trim().toLowerCase(),
1461
+ mediaType: "application/pdf",
1462
+ ...input.sourcePdf.name === void 0 ? {} : { name: input.sourcePdf.name }
1463
+ };
1464
+ }
1465
+ if (input.format !== "epub") return void 0;
1466
+ const path = resolveWikiGraphRuntimePath(input.path);
1467
+ const hash = createHash("sha256");
1468
+ try {
1469
+ for await (const chunk of createReadStream(path)) {
1470
+ hash.update(chunk);
1471
+ }
1472
+ } catch (error) {
1473
+ if (isNodeErrorCode(error, "ENOENT")) return void 0;
1474
+ throw error;
1475
+ }
1476
+ return {
1477
+ digest: hash.digest("hex"),
1478
+ mediaType: "application/epub+zip",
1479
+ name: basename3(path)
1480
+ };
1481
+ }
1482
+ function isNodeErrorCode(error, code) {
1483
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
1484
+ }
1485
+ function countTocChapters(items) {
1486
+ let count = 0;
1487
+ for (const item of items) {
1488
+ if (item.serialId !== void 0) count += 1;
1489
+ count += countTocChapters(
1490
+ item.children
1491
+ );
1492
+ }
1493
+ return count;
1494
+ }
1495
+ function collectTocChapterPaths(items, ancestorKeys = []) {
1496
+ const paths = [];
1497
+ for (const item of items) {
1498
+ const key = item.key ?? `chapter-${item.serialId ?? "group"}`;
1499
+ const chapterPath = [...ancestorKeys, key];
1500
+ if (item.serialId !== void 0) paths.push(chapterPath.join("/"));
1501
+ paths.push(
1502
+ ...collectTocChapterPaths(
1503
+ item.children,
1504
+ chapterPath
1505
+ )
1506
+ );
1507
+ }
1508
+ return paths;
1509
+ }
1305
1510
  export {
1306
1511
  WikiGraphConversionManager
1307
1512
  };