@utilix-tech/sdk 0.76.0 → 0.77.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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
4
4
 
5
+ ## 0.77.0
6
+ - Add `readPdfOutline` (pdf-outline-extractor): walk a PDF's /Outlines bookmark tree (the Root -> Outlines -> First/Next/First chain) and list every bookmark's title, nesting level, and link target, in document order.
7
+ - Add `scoreSecurityHeaders` (security-headers-scorecard): score a pasted block of raw HTTP response headers against commonly recommended security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-*), with an overall 0-100 score and A-F letter grade.
8
+
5
9
  ## 0.76.0
6
10
  - Add `detectWordyPhrases` (wordy-phrase-detector): scan text against a curated table of wordy/redundant phrases ("due to the fact that", "in order to") and flag each occurrence with a suggested concise replacement, an occurrence count, and character offsets.
7
11
  - Add `checkOxfordCommaConsistency` (oxford-comma-checker): scan a document for comma-before-and/or usage in three-or-more-item lists and flag documents that mix Oxford comma styles.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **703 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **705 developer utility functions for Node.js: runs locally, no API key required.**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@utilix-tech/sdk?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@utilix-tech/sdk)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@utilix-tech/sdk?style=flat-square)](https://www.npmjs.com/package/@utilix-tech/sdk)
@@ -710,6 +710,11 @@ buildRobotsMetaTag({ flags: ["noindex", "nofollow"], name: "googlebot" });
710
710
  // Cookie header byte-size budget: per-cookie and total sizes against RFC 6265's 4096-byte limit
711
711
  calculateCookieSizeBudget("session=abc123; theme=dark; cart=42");
712
712
  // { cookieCount: 3, totalBytes: 35, budgetBytes: 4096, exceedsBudget: false, cookies: [...] }
713
+
714
+ // Score pasted response headers against commonly recommended security headers
715
+ import { scoreSecurityHeaders } from "@utilix-tech/sdk/network";
716
+ scoreSecurityHeaders("Content-Security-Policy: default-src 'self'\nStrict-Transport-Security: max-age=63072000");
717
+ // { score: 22, grade: "F", passCount: 2, warningCount: 0, missingCount: 7, totalChecks: 9, checks: [...] }
713
718
  ```
714
719
 
715
720
  ---
@@ -910,10 +915,10 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
910
915
 
911
916
  ---
912
917
 
913
- ### `/media`: Image Header Parsing, EXIF, PDF Metadata & Page Dimensions, WAV, ID3, ICO, FLAC, Video, GIF & MIDI Metadata
918
+ ### `/media`: Image Header Parsing, EXIF, PDF Metadata, Page Dimensions & Outlines, WAV, ID3, ICO, FLAC, Video, GIF & MIDI Metadata
914
919
 
915
920
  ```ts
916
- import { readImageInfo, readExifData, readPdfMetadata, readPdfPageDimensions, readWavInfo, readId3Tags, readIcoInfo, readFlacInfo, readVideoInfo, inspectGifFrames, readMidiInfo } from "@utilix-tech/sdk/media";
921
+ import { readImageInfo, readExifData, readPdfMetadata, readPdfPageDimensions, readPdfOutline, readWavInfo, readId3Tags, readIcoInfo, readFlacInfo, readVideoInfo, inspectGifFrames, readMidiInfo } from "@utilix-tech/sdk/media";
917
922
 
918
923
  // Reads format, dimensions, bit depth, and alpha channel straight from
919
924
  // file bytes: no decoding, no canvas, no image library.
@@ -939,6 +944,12 @@ readPdfMetadata(new Uint8Array(pdfBytes));
939
944
  readPdfPageDimensions(new Uint8Array(pdfBytes));
940
945
  // { version: "1.7", pageCount: 12, pages: [{ pageNumber: 1, widthPt: 612, heightPt: 792, rotation: 0, orientation: "portrait", paperSize: "Letter" }, ...] }
941
946
 
947
+ // Walks a PDF's /Outlines bookmark tree (Root -> Outlines -> First/Next/
948
+ // First chain) to list every bookmark's title, nesting level, and link
949
+ // target, in document order: no PDF rendering library required.
950
+ readPdfOutline(new Uint8Array(pdfBytes));
951
+ // { version: "1.7", bookmarkCount: 3, bookmarks: [{ title: "Chapter 1", level: 0, destType: "page", target: "3 0 R" }, ...] }
952
+
942
953
  // Reads sample rate, channels, bit depth, and duration from a WAV file's
943
954
  // RIFF/fmt/data chunk headers: no audio library required.
944
955
  const wavBytes = await fs.promises.readFile("recording.wav");
@@ -72,6 +72,7 @@ __export(network_exports, {
72
72
  parseVaryHeader: () => parseVaryHeader,
73
73
  parseXContentTypeOptionsHeader: () => parseXContentTypeOptionsHeader,
74
74
  parseXRobotsTag: () => parseXRobotsTag,
75
+ scoreSecurityHeaders: () => scoreSecurityHeaders,
75
76
  searchHeaders: () => searchHeaders,
76
77
  searchStatusCodes: () => searchStatusCodes,
77
78
  splitSitemap: () => splitSitemap,
@@ -3511,5 +3512,122 @@ function calculateCookieSizeBudget(cookieHeader, budgetBytes = DEFAULT_COOKIE_BU
3511
3512
  exceedsBudget: totalBytes > budgetBytes
3512
3513
  };
3513
3514
  }
3515
+ function parseRawHeaderBlock(raw) {
3516
+ const map = /* @__PURE__ */ new Map();
3517
+ for (const line of raw.split(/\r?\n/)) {
3518
+ const trimmed = line.trim();
3519
+ if (!trimmed) continue;
3520
+ const idx = trimmed.indexOf(":");
3521
+ if (idx === -1) continue;
3522
+ const name = trimmed.slice(0, idx).trim().toLowerCase();
3523
+ const value = trimmed.slice(idx + 1).trim();
3524
+ if (!map.has(name)) map.set(name, value);
3525
+ }
3526
+ return map;
3527
+ }
3528
+ var SECURITY_HEADER_CHECKS = [
3529
+ {
3530
+ header: "Content-Security-Policy",
3531
+ evaluate: (v) => {
3532
+ if (!v) return { status: "missing", note: "Without a CSP, an injected script runs with full page privileges." };
3533
+ if (/unsafe-inline|unsafe-eval/i.test(v)) return { status: "warning", note: "Contains unsafe-inline or unsafe-eval, which weakens XSS protection." };
3534
+ return { status: "pass", note: "Present. Use the CSP Header Builder tool to validate its directives in depth." };
3535
+ }
3536
+ },
3537
+ {
3538
+ header: "Strict-Transport-Security",
3539
+ evaluate: (v) => {
3540
+ if (!v) return { status: "missing", note: "Without HSTS, a user's first visit (and any later downgrade) can be sent over plain HTTP." };
3541
+ const maxAgeMatch = v.match(/max-age\s*=\s*(\d+)/i);
3542
+ if (!maxAgeMatch) return { status: "warning", note: "Present but missing a max-age directive." };
3543
+ const maxAge = Number(maxAgeMatch[1]);
3544
+ if (maxAge < 31536e3) return { status: "warning", note: "max-age is below the recommended 1 year (31536000 seconds)." };
3545
+ return { status: "pass", note: "Present with a max-age of at least 1 year." };
3546
+ }
3547
+ },
3548
+ {
3549
+ header: "X-Content-Type-Options",
3550
+ evaluate: (v) => {
3551
+ if (!v) return { status: "missing", note: "Without this header, browsers may MIME-sniff a response into an unintended content type." };
3552
+ if (v.trim().toLowerCase() !== "nosniff") return { status: "warning", note: `"nosniff" is the only valid value; this value won't take effect.` };
3553
+ return { status: "pass", note: "Set to the only valid value, nosniff." };
3554
+ }
3555
+ },
3556
+ {
3557
+ header: "X-Frame-Options",
3558
+ evaluate: (v) => {
3559
+ if (!v) return { status: "missing", note: "Without this (or a CSP frame-ancestors directive), the page can be framed by another site for clickjacking." };
3560
+ const upper = v.trim().toUpperCase();
3561
+ if (upper === "DENY" || upper === "SAMEORIGIN") return { status: "pass", note: `Framing restricted (${upper}).` };
3562
+ if (upper.startsWith("ALLOW-FROM")) return { status: "warning", note: "ALLOW-FROM is deprecated and ignored by most modern browsers." };
3563
+ return { status: "warning", note: "Unrecognized value." };
3564
+ }
3565
+ },
3566
+ {
3567
+ header: "Referrer-Policy",
3568
+ evaluate: (v) => {
3569
+ if (!v) return { status: "missing", note: "Without this header, the browser's default referrer behavior applies, which can leak full URLs to third parties." };
3570
+ if (v.trim().toLowerCase() === "unsafe-url") return { status: "warning", note: "unsafe-url leaks the full URL, including path and query string, to every destination." };
3571
+ return { status: "pass", note: "Present." };
3572
+ }
3573
+ },
3574
+ {
3575
+ header: "Permissions-Policy",
3576
+ evaluate: (v) => {
3577
+ if (!v) return { status: "missing", note: "Without this header, embedded content can use powerful browser features (camera, geolocation, etc.) unless restricted another way." };
3578
+ return { status: "pass", note: "Present." };
3579
+ }
3580
+ },
3581
+ {
3582
+ header: "Cross-Origin-Opener-Policy",
3583
+ evaluate: (v) => {
3584
+ if (!v) return { status: "missing", note: "Without COOP, this page can be put in the same browsing context group as a malicious opener window." };
3585
+ const lower = v.trim().toLowerCase();
3586
+ if (lower === "same-origin") return { status: "pass", note: "Set to same-origin, the strongest isolation value." };
3587
+ return { status: "warning", note: `Set to "${v.trim()}", which is weaker than same-origin.` };
3588
+ }
3589
+ },
3590
+ {
3591
+ header: "Cross-Origin-Embedder-Policy",
3592
+ evaluate: (v) => {
3593
+ if (!v) return { status: "missing", note: "Required alongside COOP for cross-origin isolation (e.g. to use SharedArrayBuffer)." };
3594
+ const lower = v.trim().toLowerCase();
3595
+ if (lower === "require-corp" || lower === "credentialless") return { status: "pass", note: `Set to ${lower}.` };
3596
+ return { status: "warning", note: `Unrecognized value "${v.trim()}".` };
3597
+ }
3598
+ },
3599
+ {
3600
+ header: "Cross-Origin-Resource-Policy",
3601
+ evaluate: (v) => {
3602
+ if (!v) return { status: "missing", note: "Without CORP, this resource can be loaded cross-origin by any site that knows its URL." };
3603
+ const lower = v.trim().toLowerCase();
3604
+ if (lower === "same-origin" || lower === "same-site") return { status: "pass", note: `Set to ${lower}.` };
3605
+ if (lower === "cross-origin") return { status: "warning", note: "Set to cross-origin, which allows any site to load this resource." };
3606
+ return { status: "warning", note: `Unrecognized value "${v.trim()}".` };
3607
+ }
3608
+ }
3609
+ ];
3610
+ function scoreSecurityHeaders(rawHeaders) {
3611
+ if (typeof rawHeaders !== "string" || !rawHeaders.trim()) {
3612
+ return { error: 'Paste one or more response headers, one per line in "Name: value" form' };
3613
+ }
3614
+ const headerMap = parseRawHeaderBlock(rawHeaders);
3615
+ if (headerMap.size === 0) {
3616
+ return { error: 'Could not parse any "Name: value" header lines from the input' };
3617
+ }
3618
+ const checks = SECURITY_HEADER_CHECKS.map((def) => {
3619
+ const value = headerMap.get(def.header.toLowerCase()) ?? null;
3620
+ const { status, note } = def.evaluate(value);
3621
+ return { header: def.header, status, value, note };
3622
+ });
3623
+ const passCount = checks.filter((c) => c.status === "pass").length;
3624
+ const warningCount = checks.filter((c) => c.status === "warning").length;
3625
+ const missingCount = checks.filter((c) => c.status === "missing").length;
3626
+ const totalChecks = checks.length;
3627
+ const points = passCount + warningCount * 0.5;
3628
+ const score = Math.round(points / totalChecks * 100);
3629
+ const grade = score >= 90 ? "A" : score >= 75 ? "B" : score >= 60 ? "C" : score >= 40 ? "D" : "F";
3630
+ return { checks, score, grade, passCount, warningCount, missingCount, totalChecks };
3631
+ }
3514
3632
 
3515
- export { CLIENT_HINTS, DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, PERMISSIONS_POLICY_FEATURES, REFERRER_POLICY_VALUES, STATUS_CODES, X_CONTENT_TYPE_OPTIONS_HEADER, analyzeEmailHeaders, assessContentTypeRisk, buildAcceptLanguage, buildCacheControl, buildClientHints, buildContentDisposition, buildDnsQueryUrl, buildGeoUrl, buildHsts, buildLinkHeader, buildMapUrl, buildPermissionsPolicy, buildPreferHeader, buildPriorityHeader, buildRangeHeader, buildReferrerPolicy, buildRetryAfterHeader, buildRobotsMetaTag, buildVaryHeader, buildXContentTypeOptionsHeader, buildXRobotsTag, calculateCookieSizeBudget, checkSetCookieHeader, countryCodeToFlag, extractMultipartBoundary, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, listCloseCodes, listOpcodes, lookupCloseCode, lookupOpcode, network_exports, parseAcceptLanguage, parseClientHintHeaders, parseContentDisposition, parseContentRangeHeader, parseDnsResponse, parseGeoResponse, parseHar, parseHsts, parseIpv6Address, parseMultipartFormData, parsePermissionsPolicy, parsePreferHeader, parsePriorityHeader, parseRetryAfterHeader, parseRobotsMetaTag, parseUserAgent, parseVaryHeader, parseXContentTypeOptionsHeader, parseXRobotsTag, searchHeaders, searchStatusCodes, splitSitemap, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateManifest, validateRobotsTxt, validateSitemap };
3633
+ export { CLIENT_HINTS, DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, PERMISSIONS_POLICY_FEATURES, REFERRER_POLICY_VALUES, STATUS_CODES, X_CONTENT_TYPE_OPTIONS_HEADER, analyzeEmailHeaders, assessContentTypeRisk, buildAcceptLanguage, buildCacheControl, buildClientHints, buildContentDisposition, buildDnsQueryUrl, buildGeoUrl, buildHsts, buildLinkHeader, buildMapUrl, buildPermissionsPolicy, buildPreferHeader, buildPriorityHeader, buildRangeHeader, buildReferrerPolicy, buildRetryAfterHeader, buildRobotsMetaTag, buildVaryHeader, buildXContentTypeOptionsHeader, buildXRobotsTag, calculateCookieSizeBudget, checkSetCookieHeader, countryCodeToFlag, extractMultipartBoundary, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, listCloseCodes, listOpcodes, lookupCloseCode, lookupOpcode, network_exports, parseAcceptLanguage, parseClientHintHeaders, parseContentDisposition, parseContentRangeHeader, parseDnsResponse, parseGeoResponse, parseHar, parseHsts, parseIpv6Address, parseMultipartFormData, parsePermissionsPolicy, parsePreferHeader, parsePriorityHeader, parseRetryAfterHeader, parseRobotsMetaTag, parseUserAgent, parseVaryHeader, parseXContentTypeOptionsHeader, parseXRobotsTag, scoreSecurityHeaders, searchHeaders, searchStatusCodes, splitSitemap, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateManifest, validateRobotsTxt, validateSitemap };
@@ -11,6 +11,7 @@ __export(media_exports, {
11
11
  readImageInfo: () => readImageInfo,
12
12
  readMidiInfo: () => readMidiInfo,
13
13
  readPdfMetadata: () => readPdfMetadata,
14
+ readPdfOutline: () => readPdfOutline,
14
15
  readPdfPageDimensions: () => readPdfPageDimensions,
15
16
  readVideoInfo: () => readVideoInfo,
16
17
  readWavInfo: () => readWavInfo
@@ -581,6 +582,74 @@ function readPdfPageDimensions(bytes) {
581
582
  if (out.length === 0) return { error: "Could not locate any page objects in this PDF" };
582
583
  return { version, pageCount: out.length, pages: out };
583
584
  }
585
+ function parsePdfOutlineDest(dict) {
586
+ const directRef = dict.match(/\/Dest\s*\[\s*(\d+)\s+(\d+)\s+R/);
587
+ if (directRef) return { destType: "page", target: `${directRef[1]} ${directRef[2]} R` };
588
+ const directName = dict.match(/\/Dest\s*\/([^\s/()<>[\]]+)/);
589
+ if (directName) return { destType: "named", target: directName[1] };
590
+ const directString = dict.match(/\/Dest\s*\(((?:\\.|[^\\()])*)\)/);
591
+ if (directString) return { destType: "named", target: decodePdfString(directString[1], false) };
592
+ const actionUri = dict.match(/\/S\s*\/URI[\s\S]*?\/URI\s*\(((?:\\.|[^\\()])*)\)/);
593
+ if (actionUri) return { destType: "uri", target: decodePdfString(actionUri[1], false) };
594
+ const actionGotoRef = dict.match(/\/S\s*\/GoTo[\s\S]*?\/D\s*\[\s*(\d+)\s+(\d+)\s+R/);
595
+ if (actionGotoRef) return { destType: "page", target: `${actionGotoRef[1]} ${actionGotoRef[2]} R` };
596
+ const actionGotoName = dict.match(/\/S\s*\/GoTo[\s\S]*?\/D\s*\/([^\s/()<>[\]]+)/);
597
+ if (actionGotoName) return { destType: "named", target: actionGotoName[1] };
598
+ return { destType: "none", target: null };
599
+ }
600
+ function walkPdfOutlineChain(objMap, startRef, level, visited, out) {
601
+ let ref = startRef;
602
+ while (ref) {
603
+ if (visited.has(ref)) break;
604
+ visited.add(ref);
605
+ const body = objMap.get(ref);
606
+ if (!body) break;
607
+ const titleRaw = extractDictField(body, "Title");
608
+ const { destType, target } = parsePdfOutlineDest(body);
609
+ out.push({ title: titleRaw ?? "(untitled)", level, destType, target });
610
+ const firstMatch = body.match(/\/First\s+(\d+)\s+(\d+)\s+R/);
611
+ if (firstMatch) walkPdfOutlineChain(objMap, `${firstMatch[1]} ${firstMatch[2]}`, level + 1, visited, out);
612
+ const nextMatch = body.match(/\/Next\s+(\d+)\s+(\d+)\s+R/);
613
+ ref = nextMatch ? `${nextMatch[1]} ${nextMatch[2]}` : null;
614
+ }
615
+ }
616
+ function readPdfOutline(bytes) {
617
+ if (bytes.length < 8) return { error: "File too small to be a PDF" };
618
+ const header = bytesToLatin1(bytes.subarray(0, 16));
619
+ const versionMatch = header.match(/%PDF-(\d\.\d)/);
620
+ if (!versionMatch) return { error: "Not a valid PDF file (missing %PDF header)" };
621
+ const version = versionMatch[1];
622
+ const text = bytesToLatin1(bytes);
623
+ const objBlocks = [...text.matchAll(/(\d+)\s+(\d+)\s+obj([\s\S]*?)endobj/g)];
624
+ const objMap = /* @__PURE__ */ new Map();
625
+ for (const b of objBlocks) objMap.set(`${b[1]} ${b[2]}`, b[3]);
626
+ let outlinesBody = null;
627
+ const trailerMatches = [...text.matchAll(/trailer\s*<<([\s\S]*?)>>/g)];
628
+ if (trailerMatches.length > 0) {
629
+ const trailer = trailerMatches[trailerMatches.length - 1][1];
630
+ const rootRef = trailer.match(/\/Root\s+(\d+)\s+(\d+)\s+R/);
631
+ if (rootRef) {
632
+ const catalogBody = objMap.get(`${rootRef[1]} ${rootRef[2]}`);
633
+ const outlinesRef = catalogBody?.match(/\/Outlines\s+(\d+)\s+(\d+)\s+R/);
634
+ if (outlinesRef) outlinesBody = objMap.get(`${outlinesRef[1]} ${outlinesRef[2]}`) ?? null;
635
+ }
636
+ }
637
+ if (!outlinesBody) {
638
+ for (const b of objBlocks) {
639
+ if (/\/Type\s*\/Outlines\b/.test(b[3])) {
640
+ outlinesBody = b[3];
641
+ break;
642
+ }
643
+ }
644
+ }
645
+ if (!outlinesBody) return { error: "No /Outlines bookmark tree found in this PDF" };
646
+ const out = [];
647
+ const firstMatch = outlinesBody.match(/\/First\s+(\d+)\s+(\d+)\s+R/);
648
+ if (firstMatch) {
649
+ walkPdfOutlineChain(objMap, `${firstMatch[1]} ${firstMatch[2]}`, 0, /* @__PURE__ */ new Set(), out);
650
+ }
651
+ return { version, bookmarkCount: out.length, bookmarks: out };
652
+ }
584
653
  var AUDIO_FORMAT_LABELS = {
585
654
  1: "PCM",
586
655
  3: "IEEE float",
@@ -1438,4 +1507,4 @@ function readMidiInfo(bytes) {
1438
1507
  };
1439
1508
  }
1440
1509
 
1441
- export { inspectGifFrames, media_exports, readExifData, readFlacInfo, readIcoInfo, readId3Tags, readImageInfo, readMidiInfo, readPdfMetadata, readPdfPageDimensions, readVideoInfo, readWavInfo };
1510
+ export { inspectGifFrames, media_exports, readExifData, readFlacInfo, readIcoInfo, readId3Tags, readImageInfo, readMidiInfo, readPdfMetadata, readPdfOutline, readPdfPageDimensions, readVideoInfo, readWavInfo };
package/dist/index.cjs CHANGED
@@ -11465,6 +11465,7 @@ __export(network_exports, {
11465
11465
  parseVaryHeader: () => parseVaryHeader,
11466
11466
  parseXContentTypeOptionsHeader: () => parseXContentTypeOptionsHeader,
11467
11467
  parseXRobotsTag: () => parseXRobotsTag,
11468
+ scoreSecurityHeaders: () => scoreSecurityHeaders,
11468
11469
  searchHeaders: () => searchHeaders,
11469
11470
  searchStatusCodes: () => searchStatusCodes,
11470
11471
  splitSitemap: () => splitSitemap,
@@ -14904,6 +14905,123 @@ function calculateCookieSizeBudget(cookieHeader, budgetBytes = DEFAULT_COOKIE_BU
14904
14905
  exceedsBudget: totalBytes > budgetBytes
14905
14906
  };
14906
14907
  }
14908
+ function parseRawHeaderBlock(raw) {
14909
+ const map = /* @__PURE__ */ new Map();
14910
+ for (const line of raw.split(/\r?\n/)) {
14911
+ const trimmed = line.trim();
14912
+ if (!trimmed) continue;
14913
+ const idx = trimmed.indexOf(":");
14914
+ if (idx === -1) continue;
14915
+ const name = trimmed.slice(0, idx).trim().toLowerCase();
14916
+ const value = trimmed.slice(idx + 1).trim();
14917
+ if (!map.has(name)) map.set(name, value);
14918
+ }
14919
+ return map;
14920
+ }
14921
+ var SECURITY_HEADER_CHECKS = [
14922
+ {
14923
+ header: "Content-Security-Policy",
14924
+ evaluate: (v) => {
14925
+ if (!v) return { status: "missing", note: "Without a CSP, an injected script runs with full page privileges." };
14926
+ if (/unsafe-inline|unsafe-eval/i.test(v)) return { status: "warning", note: "Contains unsafe-inline or unsafe-eval, which weakens XSS protection." };
14927
+ return { status: "pass", note: "Present. Use the CSP Header Builder tool to validate its directives in depth." };
14928
+ }
14929
+ },
14930
+ {
14931
+ header: "Strict-Transport-Security",
14932
+ evaluate: (v) => {
14933
+ if (!v) return { status: "missing", note: "Without HSTS, a user's first visit (and any later downgrade) can be sent over plain HTTP." };
14934
+ const maxAgeMatch = v.match(/max-age\s*=\s*(\d+)/i);
14935
+ if (!maxAgeMatch) return { status: "warning", note: "Present but missing a max-age directive." };
14936
+ const maxAge = Number(maxAgeMatch[1]);
14937
+ if (maxAge < 31536e3) return { status: "warning", note: "max-age is below the recommended 1 year (31536000 seconds)." };
14938
+ return { status: "pass", note: "Present with a max-age of at least 1 year." };
14939
+ }
14940
+ },
14941
+ {
14942
+ header: "X-Content-Type-Options",
14943
+ evaluate: (v) => {
14944
+ if (!v) return { status: "missing", note: "Without this header, browsers may MIME-sniff a response into an unintended content type." };
14945
+ if (v.trim().toLowerCase() !== "nosniff") return { status: "warning", note: `"nosniff" is the only valid value; this value won't take effect.` };
14946
+ return { status: "pass", note: "Set to the only valid value, nosniff." };
14947
+ }
14948
+ },
14949
+ {
14950
+ header: "X-Frame-Options",
14951
+ evaluate: (v) => {
14952
+ if (!v) return { status: "missing", note: "Without this (or a CSP frame-ancestors directive), the page can be framed by another site for clickjacking." };
14953
+ const upper = v.trim().toUpperCase();
14954
+ if (upper === "DENY" || upper === "SAMEORIGIN") return { status: "pass", note: `Framing restricted (${upper}).` };
14955
+ if (upper.startsWith("ALLOW-FROM")) return { status: "warning", note: "ALLOW-FROM is deprecated and ignored by most modern browsers." };
14956
+ return { status: "warning", note: "Unrecognized value." };
14957
+ }
14958
+ },
14959
+ {
14960
+ header: "Referrer-Policy",
14961
+ evaluate: (v) => {
14962
+ if (!v) return { status: "missing", note: "Without this header, the browser's default referrer behavior applies, which can leak full URLs to third parties." };
14963
+ if (v.trim().toLowerCase() === "unsafe-url") return { status: "warning", note: "unsafe-url leaks the full URL, including path and query string, to every destination." };
14964
+ return { status: "pass", note: "Present." };
14965
+ }
14966
+ },
14967
+ {
14968
+ header: "Permissions-Policy",
14969
+ evaluate: (v) => {
14970
+ if (!v) return { status: "missing", note: "Without this header, embedded content can use powerful browser features (camera, geolocation, etc.) unless restricted another way." };
14971
+ return { status: "pass", note: "Present." };
14972
+ }
14973
+ },
14974
+ {
14975
+ header: "Cross-Origin-Opener-Policy",
14976
+ evaluate: (v) => {
14977
+ if (!v) return { status: "missing", note: "Without COOP, this page can be put in the same browsing context group as a malicious opener window." };
14978
+ const lower = v.trim().toLowerCase();
14979
+ if (lower === "same-origin") return { status: "pass", note: "Set to same-origin, the strongest isolation value." };
14980
+ return { status: "warning", note: `Set to "${v.trim()}", which is weaker than same-origin.` };
14981
+ }
14982
+ },
14983
+ {
14984
+ header: "Cross-Origin-Embedder-Policy",
14985
+ evaluate: (v) => {
14986
+ if (!v) return { status: "missing", note: "Required alongside COOP for cross-origin isolation (e.g. to use SharedArrayBuffer)." };
14987
+ const lower = v.trim().toLowerCase();
14988
+ if (lower === "require-corp" || lower === "credentialless") return { status: "pass", note: `Set to ${lower}.` };
14989
+ return { status: "warning", note: `Unrecognized value "${v.trim()}".` };
14990
+ }
14991
+ },
14992
+ {
14993
+ header: "Cross-Origin-Resource-Policy",
14994
+ evaluate: (v) => {
14995
+ if (!v) return { status: "missing", note: "Without CORP, this resource can be loaded cross-origin by any site that knows its URL." };
14996
+ const lower = v.trim().toLowerCase();
14997
+ if (lower === "same-origin" || lower === "same-site") return { status: "pass", note: `Set to ${lower}.` };
14998
+ if (lower === "cross-origin") return { status: "warning", note: "Set to cross-origin, which allows any site to load this resource." };
14999
+ return { status: "warning", note: `Unrecognized value "${v.trim()}".` };
15000
+ }
15001
+ }
15002
+ ];
15003
+ function scoreSecurityHeaders(rawHeaders) {
15004
+ if (typeof rawHeaders !== "string" || !rawHeaders.trim()) {
15005
+ return { error: 'Paste one or more response headers, one per line in "Name: value" form' };
15006
+ }
15007
+ const headerMap = parseRawHeaderBlock(rawHeaders);
15008
+ if (headerMap.size === 0) {
15009
+ return { error: 'Could not parse any "Name: value" header lines from the input' };
15010
+ }
15011
+ const checks = SECURITY_HEADER_CHECKS.map((def) => {
15012
+ const value = headerMap.get(def.header.toLowerCase()) ?? null;
15013
+ const { status, note } = def.evaluate(value);
15014
+ return { header: def.header, status, value, note };
15015
+ });
15016
+ const passCount = checks.filter((c) => c.status === "pass").length;
15017
+ const warningCount = checks.filter((c) => c.status === "warning").length;
15018
+ const missingCount = checks.filter((c) => c.status === "missing").length;
15019
+ const totalChecks = checks.length;
15020
+ const points = passCount + warningCount * 0.5;
15021
+ const score = Math.round(points / totalChecks * 100);
15022
+ const grade = score >= 90 ? "A" : score >= 75 ? "B" : score >= 60 ? "C" : score >= 40 ? "D" : "F";
15023
+ return { checks, score, grade, passCount, warningCount, missingCount, totalChecks };
15024
+ }
14907
15025
 
14908
15026
  // src/tools/api.ts
14909
15027
  var api_exports = {};
@@ -24902,6 +25020,7 @@ __export(media_exports, {
24902
25020
  readImageInfo: () => readImageInfo,
24903
25021
  readMidiInfo: () => readMidiInfo,
24904
25022
  readPdfMetadata: () => readPdfMetadata,
25023
+ readPdfOutline: () => readPdfOutline,
24905
25024
  readPdfPageDimensions: () => readPdfPageDimensions,
24906
25025
  readVideoInfo: () => readVideoInfo,
24907
25026
  readWavInfo: () => readWavInfo
@@ -25472,6 +25591,74 @@ function readPdfPageDimensions(bytes) {
25472
25591
  if (out.length === 0) return { error: "Could not locate any page objects in this PDF" };
25473
25592
  return { version: version2, pageCount: out.length, pages: out };
25474
25593
  }
25594
+ function parsePdfOutlineDest(dict) {
25595
+ const directRef = dict.match(/\/Dest\s*\[\s*(\d+)\s+(\d+)\s+R/);
25596
+ if (directRef) return { destType: "page", target: `${directRef[1]} ${directRef[2]} R` };
25597
+ const directName = dict.match(/\/Dest\s*\/([^\s/()<>[\]]+)/);
25598
+ if (directName) return { destType: "named", target: directName[1] };
25599
+ const directString = dict.match(/\/Dest\s*\(((?:\\.|[^\\()])*)\)/);
25600
+ if (directString) return { destType: "named", target: decodePdfString(directString[1], false) };
25601
+ const actionUri = dict.match(/\/S\s*\/URI[\s\S]*?\/URI\s*\(((?:\\.|[^\\()])*)\)/);
25602
+ if (actionUri) return { destType: "uri", target: decodePdfString(actionUri[1], false) };
25603
+ const actionGotoRef = dict.match(/\/S\s*\/GoTo[\s\S]*?\/D\s*\[\s*(\d+)\s+(\d+)\s+R/);
25604
+ if (actionGotoRef) return { destType: "page", target: `${actionGotoRef[1]} ${actionGotoRef[2]} R` };
25605
+ const actionGotoName = dict.match(/\/S\s*\/GoTo[\s\S]*?\/D\s*\/([^\s/()<>[\]]+)/);
25606
+ if (actionGotoName) return { destType: "named", target: actionGotoName[1] };
25607
+ return { destType: "none", target: null };
25608
+ }
25609
+ function walkPdfOutlineChain(objMap, startRef, level, visited, out) {
25610
+ let ref = startRef;
25611
+ while (ref) {
25612
+ if (visited.has(ref)) break;
25613
+ visited.add(ref);
25614
+ const body = objMap.get(ref);
25615
+ if (!body) break;
25616
+ const titleRaw = extractDictField(body, "Title");
25617
+ const { destType, target } = parsePdfOutlineDest(body);
25618
+ out.push({ title: titleRaw ?? "(untitled)", level, destType, target });
25619
+ const firstMatch = body.match(/\/First\s+(\d+)\s+(\d+)\s+R/);
25620
+ if (firstMatch) walkPdfOutlineChain(objMap, `${firstMatch[1]} ${firstMatch[2]}`, level + 1, visited, out);
25621
+ const nextMatch = body.match(/\/Next\s+(\d+)\s+(\d+)\s+R/);
25622
+ ref = nextMatch ? `${nextMatch[1]} ${nextMatch[2]}` : null;
25623
+ }
25624
+ }
25625
+ function readPdfOutline(bytes) {
25626
+ if (bytes.length < 8) return { error: "File too small to be a PDF" };
25627
+ const header = bytesToLatin1(bytes.subarray(0, 16));
25628
+ const versionMatch = header.match(/%PDF-(\d\.\d)/);
25629
+ if (!versionMatch) return { error: "Not a valid PDF file (missing %PDF header)" };
25630
+ const version2 = versionMatch[1];
25631
+ const text = bytesToLatin1(bytes);
25632
+ const objBlocks = [...text.matchAll(/(\d+)\s+(\d+)\s+obj([\s\S]*?)endobj/g)];
25633
+ const objMap = /* @__PURE__ */ new Map();
25634
+ for (const b of objBlocks) objMap.set(`${b[1]} ${b[2]}`, b[3]);
25635
+ let outlinesBody = null;
25636
+ const trailerMatches = [...text.matchAll(/trailer\s*<<([\s\S]*?)>>/g)];
25637
+ if (trailerMatches.length > 0) {
25638
+ const trailer = trailerMatches[trailerMatches.length - 1][1];
25639
+ const rootRef = trailer.match(/\/Root\s+(\d+)\s+(\d+)\s+R/);
25640
+ if (rootRef) {
25641
+ const catalogBody = objMap.get(`${rootRef[1]} ${rootRef[2]}`);
25642
+ const outlinesRef = catalogBody?.match(/\/Outlines\s+(\d+)\s+(\d+)\s+R/);
25643
+ if (outlinesRef) outlinesBody = objMap.get(`${outlinesRef[1]} ${outlinesRef[2]}`) ?? null;
25644
+ }
25645
+ }
25646
+ if (!outlinesBody) {
25647
+ for (const b of objBlocks) {
25648
+ if (/\/Type\s*\/Outlines\b/.test(b[3])) {
25649
+ outlinesBody = b[3];
25650
+ break;
25651
+ }
25652
+ }
25653
+ }
25654
+ if (!outlinesBody) return { error: "No /Outlines bookmark tree found in this PDF" };
25655
+ const out = [];
25656
+ const firstMatch = outlinesBody.match(/\/First\s+(\d+)\s+(\d+)\s+R/);
25657
+ if (firstMatch) {
25658
+ walkPdfOutlineChain(objMap, `${firstMatch[1]} ${firstMatch[2]}`, 0, /* @__PURE__ */ new Set(), out);
25659
+ }
25660
+ return { version: version2, bookmarkCount: out.length, bookmarks: out };
25661
+ }
25475
25662
  var AUDIO_FORMAT_LABELS = {
25476
25663
  1: "PCM",
25477
25664
  3: "IEEE float",
package/dist/index.d.cts CHANGED
@@ -6,14 +6,14 @@ export { d as data } from './data-26TALa9U.cjs';
6
6
  export { g as generators } from './generators-DAXXrMvG.cjs';
7
7
  export { t as time } from './time-KqJjdqII.cjs';
8
8
  export { u as units } from './units-BqegyALA.cjs';
9
- export { n as network } from './network-Da3meCWM.cjs';
9
+ export { n as network } from './network-BskZe1n2.cjs';
10
10
  export { a as api } from './api-CQnIUrVB.cjs';
11
11
  export { c as code } from './code-46xO9xFf.cjs';
12
12
  export { c as color } from './color-tPwZCr9H.cjs';
13
13
  export { c as css } from './css-Cf7AMGM-.cjs';
14
14
  export { m as misc } from './misc-CA3N198T.cjs';
15
15
  export { a as ai_agent } from './ai_agent-CmrVjvYH.cjs';
16
- export { m as media } from './media-56zrIBw4.cjs';
16
+ export { m as media } from './media-DF8BS-iA.cjs';
17
17
 
18
18
  declare const version = "0.1.0";
19
19
 
package/dist/index.d.ts CHANGED
@@ -6,14 +6,14 @@ export { d as data } from './data-26TALa9U.js';
6
6
  export { g as generators } from './generators-DAXXrMvG.js';
7
7
  export { t as time } from './time-KqJjdqII.js';
8
8
  export { u as units } from './units-BqegyALA.js';
9
- export { n as network } from './network-Da3meCWM.js';
9
+ export { n as network } from './network-BskZe1n2.js';
10
10
  export { a as api } from './api-CQnIUrVB.js';
11
11
  export { c as code } from './code-46xO9xFf.js';
12
12
  export { c as color } from './color-tPwZCr9H.js';
13
13
  export { c as css } from './css-Cf7AMGM-.js';
14
14
  export { m as misc } from './misc-CA3N198T.js';
15
15
  export { a as ai_agent } from './ai_agent-CmrVjvYH.js';
16
- export { m as media } from './media-56zrIBw4.js';
16
+ export { m as media } from './media-DF8BS-iA.js';
17
17
 
18
18
  declare const version = "0.1.0";
19
19
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- export { media_exports as media } from './chunk-PT4N4UBL.js';
1
+ export { media_exports as media } from './chunk-ZWFZRDQ4.js';
2
2
  export { units_exports as units } from './chunk-RP574L5Z.js';
3
- export { network_exports as network } from './chunk-CXUOV672.js';
3
+ export { network_exports as network } from './chunk-KWS4ZCYG.js';
4
4
  export { api_exports as api } from './chunk-U2HREMZU.js';
5
5
  export { code_exports as code } from './chunk-XT2BQWXT.js';
6
6
  export { color_exports as color } from './chunk-BPVAB4P2.js';
@@ -87,6 +87,32 @@ interface PdfPageDimensionsResult {
87
87
  * PDF spec requires. No PDF rendering library required.
88
88
  */
89
89
  declare function readPdfPageDimensions(bytes: Uint8Array): PdfPageDimensionsResult | PdfMetadataError;
90
+ type PdfOutlineDestType = 'page' | 'uri' | 'named' | 'none';
91
+ interface PdfOutlineItem {
92
+ title: string;
93
+ level: number;
94
+ destType: PdfOutlineDestType;
95
+ target: string | null;
96
+ }
97
+ interface PdfOutlineResult {
98
+ version: string | null;
99
+ bookmarkCount: number;
100
+ bookmarks: PdfOutlineItem[];
101
+ }
102
+ interface PdfOutlineError {
103
+ error: string;
104
+ }
105
+ /**
106
+ * Walk a PDF's /Outlines bookmark tree (the Root -> Outlines -> First/Next/
107
+ * First chain of entries a PDF viewer's sidebar builds its table of
108
+ * contents from) and list every bookmark's title, nesting level, and link
109
+ * target, in document order. Only the destination's raw target is reported
110
+ * (an object reference for a page link, a URI for a web link, or a name for
111
+ * a named destination) — this does not resolve a page-ref target to a page
112
+ * number, since that requires a second pass over the page tree; pair it
113
+ * with readPdfPageDimensions's output if you need that mapping.
114
+ */
115
+ declare function readPdfOutline(bytes: Uint8Array): PdfOutlineResult | PdfOutlineError;
90
116
  interface WavInfo {
91
117
  audioFormat: number;
92
118
  audioFormatLabel: string;
@@ -285,6 +311,10 @@ type media_MidiTimeSignature = MidiTimeSignature;
285
311
  type media_MidiTrackInfo = MidiTrackInfo;
286
312
  type media_PdfMetadata = PdfMetadata;
287
313
  type media_PdfMetadataError = PdfMetadataError;
314
+ type media_PdfOutlineDestType = PdfOutlineDestType;
315
+ type media_PdfOutlineError = PdfOutlineError;
316
+ type media_PdfOutlineItem = PdfOutlineItem;
317
+ type media_PdfOutlineResult = PdfOutlineResult;
288
318
  type media_PdfPageDimension = PdfPageDimension;
289
319
  type media_PdfPageDimensionsResult = PdfPageDimensionsResult;
290
320
  type media_PdfPageOrientation = PdfPageOrientation;
@@ -300,11 +330,12 @@ declare const media_readId3Tags: typeof readId3Tags;
300
330
  declare const media_readImageInfo: typeof readImageInfo;
301
331
  declare const media_readMidiInfo: typeof readMidiInfo;
302
332
  declare const media_readPdfMetadata: typeof readPdfMetadata;
333
+ declare const media_readPdfOutline: typeof readPdfOutline;
303
334
  declare const media_readPdfPageDimensions: typeof readPdfPageDimensions;
304
335
  declare const media_readVideoInfo: typeof readVideoInfo;
305
336
  declare const media_readWavInfo: typeof readWavInfo;
306
337
  declare namespace media {
307
- export { type media_ExifData as ExifData, type media_ExifError as ExifError, type media_FlacInfo as FlacInfo, type media_FlacInfoError as FlacInfoError, type media_GifInfo as GifInfo, type media_GifInfoError as GifInfoError, type media_IcoImageEntry as IcoImageEntry, type media_IcoInfo as IcoInfo, type media_IcoInfoError as IcoInfoError, type media_Id3Tags as Id3Tags, type media_Id3TagsError as Id3TagsError, type media_ImageFormat as ImageFormat, type media_ImageInfo as ImageInfo, type media_ImageInfoError as ImageInfoError, type media_MidiDivision as MidiDivision, type media_MidiInfo as MidiInfo, type media_MidiInfoError as MidiInfoError, type media_MidiTimeSignature as MidiTimeSignature, type media_MidiTrackInfo as MidiTrackInfo, type media_PdfMetadata as PdfMetadata, type media_PdfMetadataError as PdfMetadataError, type media_PdfPageDimension as PdfPageDimension, type media_PdfPageDimensionsResult as PdfPageDimensionsResult, type media_PdfPageOrientation as PdfPageOrientation, type media_VideoInfo as VideoInfo, type media_VideoInfoError as VideoInfoError, type media_WavInfo as WavInfo, type media_WavInfoError as WavInfoError, media_inspectGifFrames as inspectGifFrames, media_readExifData as readExifData, media_readFlacInfo as readFlacInfo, media_readIcoInfo as readIcoInfo, media_readId3Tags as readId3Tags, media_readImageInfo as readImageInfo, media_readMidiInfo as readMidiInfo, media_readPdfMetadata as readPdfMetadata, media_readPdfPageDimensions as readPdfPageDimensions, media_readVideoInfo as readVideoInfo, media_readWavInfo as readWavInfo };
338
+ export { type media_ExifData as ExifData, type media_ExifError as ExifError, type media_FlacInfo as FlacInfo, type media_FlacInfoError as FlacInfoError, type media_GifInfo as GifInfo, type media_GifInfoError as GifInfoError, type media_IcoImageEntry as IcoImageEntry, type media_IcoInfo as IcoInfo, type media_IcoInfoError as IcoInfoError, type media_Id3Tags as Id3Tags, type media_Id3TagsError as Id3TagsError, type media_ImageFormat as ImageFormat, type media_ImageInfo as ImageInfo, type media_ImageInfoError as ImageInfoError, type media_MidiDivision as MidiDivision, type media_MidiInfo as MidiInfo, type media_MidiInfoError as MidiInfoError, type media_MidiTimeSignature as MidiTimeSignature, type media_MidiTrackInfo as MidiTrackInfo, type media_PdfMetadata as PdfMetadata, type media_PdfMetadataError as PdfMetadataError, type media_PdfOutlineDestType as PdfOutlineDestType, type media_PdfOutlineError as PdfOutlineError, type media_PdfOutlineItem as PdfOutlineItem, type media_PdfOutlineResult as PdfOutlineResult, type media_PdfPageDimension as PdfPageDimension, type media_PdfPageDimensionsResult as PdfPageDimensionsResult, type media_PdfPageOrientation as PdfPageOrientation, type media_VideoInfo as VideoInfo, type media_VideoInfoError as VideoInfoError, type media_WavInfo as WavInfo, type media_WavInfoError as WavInfoError, media_inspectGifFrames as inspectGifFrames, media_readExifData as readExifData, media_readFlacInfo as readFlacInfo, media_readIcoInfo as readIcoInfo, media_readId3Tags as readId3Tags, media_readImageInfo as readImageInfo, media_readMidiInfo as readMidiInfo, media_readPdfMetadata as readPdfMetadata, media_readPdfOutline as readPdfOutline, media_readPdfPageDimensions as readPdfPageDimensions, media_readVideoInfo as readVideoInfo, media_readWavInfo as readWavInfo };
308
339
  }
309
340
 
310
- export { readImageInfo as A, readMidiInfo as B, readPdfMetadata as C, readPdfPageDimensions as D, type ExifData as E, type FlacInfo as F, type GifInfo as G, readVideoInfo as H, type IcoImageEntry as I, readWavInfo as J, type MidiDivision as M, type PdfMetadata as P, type VideoInfo as V, type WavInfo as W, type ExifError as a, type FlacInfoError as b, type GifInfoError as c, type IcoInfo as d, type IcoInfoError as e, type Id3Tags as f, type Id3TagsError as g, type ImageFormat as h, type ImageInfo as i, type ImageInfoError as j, type MidiInfo as k, type MidiInfoError as l, media as m, type MidiTimeSignature as n, type MidiTrackInfo as o, type PdfMetadataError as p, type PdfPageDimension as q, type PdfPageDimensionsResult as r, type PdfPageOrientation as s, type VideoInfoError as t, type WavInfoError as u, inspectGifFrames as v, readExifData as w, readFlacInfo as x, readIcoInfo as y, readId3Tags as z };
341
+ export { readExifData as A, readFlacInfo as B, readIcoInfo as C, readId3Tags as D, type ExifData as E, type FlacInfo as F, type GifInfo as G, readImageInfo as H, type IcoImageEntry as I, readMidiInfo as J, readPdfMetadata as K, readPdfOutline as L, type MidiDivision as M, readPdfPageDimensions as N, readVideoInfo as O, type PdfMetadata as P, readWavInfo as Q, type VideoInfo as V, type WavInfo as W, type ExifError as a, type FlacInfoError as b, type GifInfoError as c, type IcoInfo as d, type IcoInfoError as e, type Id3Tags as f, type Id3TagsError as g, type ImageFormat as h, type ImageInfo as i, type ImageInfoError as j, type MidiInfo as k, type MidiInfoError as l, media as m, type MidiTimeSignature as n, type MidiTrackInfo as o, type PdfMetadataError as p, type PdfOutlineDestType as q, type PdfOutlineError as r, type PdfOutlineItem as s, type PdfOutlineResult as t, type PdfPageDimension as u, type PdfPageDimensionsResult as v, type PdfPageOrientation as w, type VideoInfoError as x, type WavInfoError as y, inspectGifFrames as z };