wiki-graph-sdk 0.7.1 → 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.
@@ -6,7 +6,7 @@ globalThis.__WIKIGRAPH_DATA_DIR__ ??= __WIKIGRAPH_RESOLVE__(__WIKIGRAPH_FILE_URL
6
6
  import { createHash } from "crypto";
7
7
  import { createReadStream } from "fs";
8
8
  import { rm as rm2 } from "fs/promises";
9
- import { basename as basename2 } from "path";
9
+ import { basename as basename3 } from "path";
10
10
  import {
11
11
  WikiGraph
12
12
  } from "wiki-graph-core";
@@ -316,6 +316,14 @@ function decodeNodeResourceIdentity(identity, kind) {
316
316
  return void 0;
317
317
  }
318
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
+ }
319
327
  function assertChildName(name) {
320
328
  if (name.length === 0 || name === "." || name === ".." || pathModule.isAbsolute(name) || name.includes("/") || name.includes("\\")) {
321
329
  throw new TypeError(`Directory child must be a relative name: ${name}`);
@@ -327,42 +335,42 @@ var NodeDatabaseConnection = class {
327
335
  }
328
336
  database;
329
337
  async close() {
330
- await new Promise((resolve3, reject) => {
338
+ await new Promise((resolve4, reject) => {
331
339
  this.database.close((error) => {
332
340
  if (error) reject(error);
333
- else resolve3();
341
+ else resolve4();
334
342
  });
335
343
  });
336
344
  }
337
345
  async execute(sql) {
338
- await new Promise((resolve3, reject) => {
346
+ await new Promise((resolve4, reject) => {
339
347
  this.database.exec(sql, (error) => {
340
348
  if (error) reject(error);
341
- else resolve3();
349
+ else resolve4();
342
350
  });
343
351
  });
344
352
  }
345
353
  async queryAll(sql, params = []) {
346
- return await new Promise((resolve3, reject) => {
354
+ return await new Promise((resolve4, reject) => {
347
355
  this.database.all(sql, [...params], (error, rows) => {
348
356
  if (error) reject(error);
349
- else resolve3(rows);
357
+ else resolve4(rows);
350
358
  });
351
359
  });
352
360
  }
353
361
  async queryOne(sql, params = []) {
354
- return await new Promise((resolve3, reject) => {
362
+ return await new Promise((resolve4, reject) => {
355
363
  this.database.get(sql, [...params], (error, row) => {
356
364
  if (error) reject(error);
357
- else resolve3(row);
365
+ else resolve4(row);
358
366
  });
359
367
  });
360
368
  }
361
369
  async run(sql, params = []) {
362
- await new Promise((resolve3, reject) => {
370
+ await new Promise((resolve4, reject) => {
363
371
  this.database.run(sql, [...params], (error) => {
364
372
  if (error) reject(error);
365
- else resolve3();
373
+ else resolve4();
366
374
  });
367
375
  });
368
376
  }
@@ -376,10 +384,10 @@ async function openNativeNodeDatabase(file, flags) {
376
384
  if (path === void 0) {
377
385
  throw new TypeError("The Node database adapter requires a NodeFile");
378
386
  }
379
- return await new Promise((resolve3, reject) => {
387
+ return await new Promise((resolve4, reject) => {
380
388
  const database = new nodeSqlite3.Database(path, flags, (error) => {
381
389
  if (error) reject(error);
382
- else resolve3(database);
390
+ else resolve4(database);
383
391
  });
384
392
  });
385
393
  }
@@ -388,19 +396,19 @@ async function openNodeZip(file) {
388
396
  if (path === void 0) {
389
397
  throw new TypeError("The Node ZIP adapter requires a NodeFile");
390
398
  }
391
- const zipFile = await new Promise((resolve3, reject) => {
399
+ const zipFile = await new Promise((resolve4, reject) => {
392
400
  yauzl.open(
393
401
  path,
394
402
  { autoClose: false, lazyEntries: true },
395
403
  (error, opened) => {
396
404
  if (error || opened === void 0)
397
405
  reject(error ?? new Error("Cannot open ZIP"));
398
- else resolve3(opened);
406
+ else resolve4(opened);
399
407
  }
400
408
  );
401
409
  });
402
410
  const entries = await new Promise(
403
- (resolve3, reject) => {
411
+ (resolve4, reject) => {
404
412
  const discovered = /* @__PURE__ */ new Map();
405
413
  const rejectAndClose = (error) => {
406
414
  zipFile.close();
@@ -410,7 +418,7 @@ async function openNodeZip(file) {
410
418
  };
411
419
  const resolveEntries = () => {
412
420
  zipFile.off("error", rejectAndClose);
413
- resolve3(discovered);
421
+ resolve4(discovered);
414
422
  };
415
423
  zipFile.on("entry", (entry) => {
416
424
  if (!entry.fileName.endsWith("/")) {
@@ -520,12 +528,12 @@ async function collectDecodedNodeStreamRange(input, offset, length) {
520
528
  return Buffer.concat(chunks, collected);
521
529
  }
522
530
  async function openNodeZipEntryStream(zipFile, entry, options) {
523
- return await new Promise((resolve3, reject) => {
531
+ return await new Promise((resolve4, reject) => {
524
532
  const callback = (error, opened) => {
525
533
  if (error || opened === void 0) {
526
534
  reject(error ?? new Error(`Cannot read ZIP entry: ${entry.fileName}`));
527
535
  } else {
528
- resolve3(opened);
536
+ resolve4(opened);
529
537
  }
530
538
  };
531
539
  if (options === void 0) zipFile.openReadStream(entry, callback);
@@ -656,11 +664,11 @@ async function writeNodeStream(input, writer) {
656
664
  );
657
665
  }
658
666
  async function collectNodeStream(input) {
659
- return await new Promise((resolve3, reject) => {
667
+ return await new Promise((resolve4, reject) => {
660
668
  const chunks = [];
661
669
  input.on("data", (chunk) => chunks.push(chunk));
662
670
  input.once("error", reject);
663
- input.once("end", () => resolve3(Buffer.concat(chunks)));
671
+ input.once("end", () => resolve4(Buffer.concat(chunks)));
664
672
  });
665
673
  }
666
674
  var nodeWikiGraphPlatform = {
@@ -1213,6 +1221,116 @@ function resolveWikiGraphRuntimePath(path) {
1213
1221
  return isAbsolute2(path) ? path : resolve2(context.cwd, path);
1214
1222
  }
1215
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
+
1216
1334
  // src/conversions.ts
1217
1335
  var WikiGraphConversionManager = class {
1218
1336
  #runtime;
@@ -1249,11 +1367,15 @@ var WikiGraphConversionManager = class {
1249
1367
  requiresDigest
1250
1368
  );
1251
1369
  let chapterCount;
1370
+ let chapterPaths;
1252
1371
  const write = async (archive) => {
1253
1372
  if (requiresDigest) await options.onSourceImported?.();
1254
1373
  if (typeof archive.readToc === "function") {
1255
1374
  const toc = await archive.readToc();
1256
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
+ }
1257
1379
  }
1258
1380
  await writeArchive(archive, options.output.path, options.output.format);
1259
1381
  };
@@ -1262,7 +1384,9 @@ var WikiGraphConversionManager = class {
1262
1384
  throw new Error("wikg input requires a file path.");
1263
1385
  }
1264
1386
  await app.openSession(
1265
- new NodeFile(resolveWikiGraphRuntimePath(options.input.path)),
1387
+ new NodeFile(
1388
+ await assertStandaloneWikiGraphArchivePath(options.input.path)
1389
+ ),
1266
1390
  write
1267
1391
  );
1268
1392
  } else if ("stream" in options.input) {
@@ -1305,6 +1429,7 @@ var WikiGraphConversionManager = class {
1305
1429
  const sourceArtifact = await resolveSourceArtifact(options.input);
1306
1430
  return {
1307
1431
  ...chapterCount === void 0 ? {} : { chapterCount },
1432
+ ...chapterPaths === void 0 ? {} : { chapterPaths },
1308
1433
  inputFormat: options.input.format,
1309
1434
  outputFormat: options.output.format,
1310
1435
  outputPath: options.output.path,
@@ -1321,7 +1446,8 @@ async function prepareDigestDirectory(path, required) {
1321
1446
  return new NodeDirectory(resolved);
1322
1447
  }
1323
1448
  async function writeArchive(archive, path, format) {
1324
- const file = new NodeFile(resolveWikiGraphRuntimePath(path));
1449
+ const outputPath = format === "wikg" ? await assertStandaloneWikiGraphArchivePath(path) : resolveWikiGraphRuntimePath(path);
1450
+ const file = new NodeFile(outputPath);
1325
1451
  if (format === "epub") await archive.exportEpub(file);
1326
1452
  else if (format === "wikg") await archive.saveAs(file);
1327
1453
  else if (format === "pcex") throw new Error("pcex output is not supported.");
@@ -1350,7 +1476,7 @@ async function resolveSourceArtifact(input) {
1350
1476
  return {
1351
1477
  digest: hash.digest("hex"),
1352
1478
  mediaType: "application/epub+zip",
1353
- name: basename2(path)
1479
+ name: basename3(path)
1354
1480
  };
1355
1481
  }
1356
1482
  function isNodeErrorCode(error, code) {
@@ -1366,6 +1492,21 @@ function countTocChapters(items) {
1366
1492
  }
1367
1493
  return count;
1368
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
+ }
1369
1510
  export {
1370
1511
  WikiGraphConversionManager
1371
1512
  };