@adeu/core 1.23.0 → 1.25.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.
@@ -435,12 +435,17 @@ export function scrub_doc_properties(doc: DocumentObject): string[] {
435
435
  // Classification-style properties are textbook leak vectors ("Project
436
436
  // Falcon", "confidential,merger,..."). Unlike title they carry no
437
437
  // legitimate outbound formatting value, so strip them and say so.
438
+ // dc:identifier, dc:language and cp:version are scrubbed with them:
439
+ // identifiers are DMS/matter-ID carriers.
438
440
  const leakFields: Array<[string, string]> = [
439
441
  ['category', 'Category'],
440
442
  ['keywords', 'Keywords'],
441
443
  ['subject', 'Subject'],
442
444
  ['contentStatus', 'Content status'],
443
445
  ['description', 'Description/comments'],
446
+ ['identifier', 'Identifier'],
447
+ ['language', 'Language'],
448
+ ['version', 'Version'],
444
449
  ];
445
450
  for (const [local, label] of leakFields) {
446
451
  findDescendantsByLocalName(corePart._element, local).forEach(c => {
@@ -498,33 +503,106 @@ export function scrub_timestamps(doc: DocumentObject): string[] {
498
503
  return modified ? ["Timestamps normalized to epoch"] : [];
499
504
  }
500
505
 
501
- export function strip_custom_xml(doc: DocumentObject): string[] {
502
- const customParts = doc.pkg.parts.filter(p => p.partname.includes('/customXml'));
503
- if (customParts.length === 0) return [];
504
-
505
- const partnames = new Set(customParts.map(p => p.partname));
506
- doc.pkg.parts = doc.pkg.parts.filter(p => !partnames.has(p.partname));
506
+ /**
507
+ * Fully ejects package members whose path matches `matcher`: the parsed part
508
+ * list, the raw `pkg.unzipped` bytes (save() re-zips EVERY unzipped
509
+ * member, so skipping this ships the original bytes in the output), the
510
+ * [Content_Types].xml overrides, and every .rels Relationship whose
511
+ * target matches `relTargetMatcher`.
512
+ */
513
+ function ejectPackageMembers(
514
+ doc: DocumentObject,
515
+ matcher: (path: string) => boolean,
516
+ relTargetMatcher: (target: string) => boolean,
517
+ ) {
518
+ const pkg = doc.pkg;
519
+ const normalized = (p: string) => (p.startsWith('/') ? p.substring(1) : p);
507
520
 
508
- const removeRelationsTo = (relsPart: Part) => {
521
+ // 1. Sever relationships from every .rels part (and the in-memory rels maps)
522
+ for (const part of pkg.parts) {
523
+ if (!part.partname.endsWith('.rels')) continue;
509
524
  const toRemove: Element[] = [];
510
- for (const rel of findAllDescendants(relsPart._element, 'Relationship')) {
511
- const target = rel.getAttribute('Target');
512
- if (target && target.includes('customXml')) toRemove.push(rel);
525
+ for (const rel of findAllDescendants(part._element, 'Relationship')) {
526
+ const target = rel.getAttribute('Target') || '';
527
+ if (relTargetMatcher(target)) {
528
+ toRemove.push(rel);
529
+ const sourcePath = part.partname.replace('/_rels/', '/').replace('.rels', '');
530
+ const sourcePart = pkg.getPartByPath(sourcePath);
531
+ if (sourcePart) {
532
+ const relId = rel.getAttribute('Id');
533
+ if (relId) sourcePart.rels.delete(relId);
534
+ }
535
+ }
513
536
  }
514
537
  toRemove.forEach(r => r.parentNode?.removeChild(r));
515
- };
538
+ }
516
539
 
517
- const rootRels = doc.pkg.getPartByPath('_rels/.rels');
518
- if (rootRels) removeRelationsTo(rootRels);
540
+ // 2. Remove [Content_Types].xml overrides
541
+ const ctPart = pkg.getPartByPath('[Content_Types].xml');
542
+ if (ctPart) {
543
+ const toRemove: Element[] = [];
544
+ for (const override of findAllDescendants(ctPart._element, 'Override')) {
545
+ const partName = override.getAttribute('PartName') || '';
546
+ if (matcher(normalized(partName))) toRemove.push(override);
547
+ }
548
+ toRemove.forEach(o => o.parentNode?.removeChild(o));
549
+ }
519
550
 
520
- const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
521
- if (docRels) removeRelationsTo(docRels);
551
+ // 3. Drop the parts from the serialization list AND the raw zip map
552
+ pkg.parts = pkg.parts.filter(p => !matcher(normalized(p.partname)));
553
+ for (const key of Object.keys(pkg.unzipped)) {
554
+ if (matcher(normalized(key))) delete pkg.unzipped[key];
555
+ }
556
+ }
557
+
558
+ export function strip_custom_xml(doc: DocumentObject): string[] {
559
+ const isCustomXml = (path: string) => path.startsWith('customXml/');
560
+ const customParts = doc.pkg.parts.filter(p =>
561
+ isCustomXml(p.partname.startsWith('/') ? p.partname.substring(1) : p.partname),
562
+ );
563
+ const leakedMembers = Object.keys(doc.pkg.unzipped).filter(isCustomXml);
564
+ if (customParts.length === 0 && leakedMembers.length === 0) return [];
565
+
566
+ ejectPackageMembers(doc, isCustomXml, target => target.includes('customXml'));
522
567
 
523
568
  for (const sdtPr of findAllDescendants(doc.element, 'w:sdtPr')) {
524
569
  findChildren(sdtPr, 'w:dataBinding').forEach(b => sdtPr.removeChild(b));
525
570
  }
526
571
 
527
- return [`Custom XML parts: ${customParts.length} removed`];
572
+ return [`Custom XML parts: ${Math.max(customParts.length, leakedMembers.length)} removed`];
573
+ }
574
+
575
+ const CUSTOM_PROPS_PATH = 'docProps/custom.xml';
576
+
577
+ /**
578
+ * Remove the custom document properties part (docProps/custom.xml) and its
579
+ * package relationship. Custom properties are a standard home for matter
580
+ * numbers, client names, DMS identifiers and workflow secrets; a package
581
+ * that keeps them can never be reported CLEAN.
582
+ */
583
+ export function strip_custom_properties(doc: DocumentObject): string[] {
584
+ const part = doc.pkg.getPartByPath(CUSTOM_PROPS_PATH);
585
+ const leaked = Object.keys(doc.pkg.unzipped).includes(CUSTOM_PROPS_PATH);
586
+ if (!part && !leaked) return [];
587
+
588
+ // Enumerate what is being removed so the report discloses it.
589
+ const lines: string[] = [];
590
+ if (part) {
591
+ for (const prop of findDescendantsByLocalName(part._element, 'property')) {
592
+ const name = prop.getAttribute('name') || '(unnamed)';
593
+ const value = (prop.textContent || '').trim();
594
+ lines.push(` Custom property removed: ${name} = "${_truncate(value, 60)}"`);
595
+ }
596
+ }
597
+
598
+ ejectPackageMembers(
599
+ doc,
600
+ path => path === CUSTOM_PROPS_PATH,
601
+ target => target.includes('docProps/custom.xml'),
602
+ );
603
+
604
+ const count = Math.max(lines.length, 1);
605
+ return [`Custom document properties: ${count} removed (docProps/custom.xml)`].concat(lines);
528
606
  }
529
607
 
530
608
  export function strip_image_alt_text(doc: DocumentObject): string[] {