@hraness/kb 0.17.1

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.
Files changed (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env bun
2
+ /** Generic, bounded, authenticated-when-explicit web capture CLI. */
3
+ import { parseArguments, usage, type CliArguments } from "./args.js";
4
+ import { runCapture, type CaptureOutcome } from "./capture.js";
5
+ import {
6
+ adapterCapabilities,
7
+ inspectClipEnvironment,
8
+ renderAdapterCapabilities,
9
+ renderDoctorReport,
10
+ } from "./doctor.js";
11
+ import { redactSensitiveText } from "./persist.js";
12
+ import { sanitizeTerminalLine, sanitizeTerminalText } from "./terminal.js";
13
+
14
+ type Output = {
15
+ readonly stdout: (value: string) => void;
16
+ readonly stderr: (value: string) => void;
17
+ };
18
+
19
+ const defaultOutput: Output = {
20
+ stdout: (value) => process.stdout.write(value),
21
+ stderr: (value) => process.stderr.write(value),
22
+ };
23
+
24
+ function line(value: string): string {
25
+ return value.endsWith("\n") ? value : `${value}\n`;
26
+ }
27
+
28
+ function safe(value: string): string {
29
+ return sanitizeTerminalLine(redactSensitiveText(value));
30
+ }
31
+
32
+ function redacted(value: string): string {
33
+ return redactSensitiveText(value);
34
+ }
35
+
36
+ function terminalSafeJson(value: unknown): string {
37
+ return `${JSON.stringify(
38
+ value,
39
+ (_key, candidate: unknown) => typeof candidate === "string" ? sanitizeTerminalText(candidate) : candidate,
40
+ 2,
41
+ )}\n`;
42
+ }
43
+
44
+ type CliDependencies = {
45
+ readonly runCapture?: typeof runCapture;
46
+ readonly inspectClipEnvironment?: typeof inspectClipEnvironment;
47
+ };
48
+
49
+ export type ClipRuntimeOptions = {
50
+ /** Trusted embedding hint; never parsed from public CLI arguments. */
51
+ readonly ownedBrowserProfile?: {
52
+ readonly path: string;
53
+ readonly profileDirectory?: "Default";
54
+ };
55
+ /** Trusted embedding hint; never parsed from public CLI arguments. */
56
+ readonly browserExecutable?: string;
57
+ };
58
+
59
+ export function captureSummary(outcome: CaptureOutcome): Record<string, unknown> {
60
+ return {
61
+ ok: captureSucceeded(outcome),
62
+ status: outcome.status,
63
+ sourceUrl: redacted(outcome.sourceUrl),
64
+ canonicalUrl: redacted(outcome.canonicalUrl),
65
+ platform: outcome.platform,
66
+ scope: outcome.scope,
67
+ slug: outcome.slug,
68
+ acquisitionMethod: outcome.acquisitionMethod,
69
+ extractor: outcome.extractor,
70
+ wordCount: outcome.wordCount,
71
+ capturedItems: outcome.capturedItems,
72
+ expectedItems: outcome.expectedItems,
73
+ outputDirectory: outcome.outputDirectory,
74
+ markdownPath: outcome.markdownPath,
75
+ assetCount: outcome.assetCount,
76
+ warnings: outcome.warnings.map((warning) => redacted(warning)),
77
+ attempts: outcome.attempts.map((attempt) => ({ ...attempt, message: redacted(attempt.message) })),
78
+ manifest: outcome.manifest,
79
+ };
80
+ }
81
+
82
+ export function captureSucceeded(outcome: CaptureOutcome): boolean {
83
+ return outcome.status === "complete" || outcome.status === "partial";
84
+ }
85
+
86
+ export function captureExitCode(outcome: CaptureOutcome): number {
87
+ return captureSucceeded(outcome) ? 0 : 3;
88
+ }
89
+
90
+ async function diagnosticCommand(
91
+ arguments_: Extract<CliArguments, { readonly command: "doctor" }>,
92
+ output: Output,
93
+ inspectEnvironment: typeof inspectClipEnvironment,
94
+ ): Promise<number> {
95
+ const report = await inspectEnvironment();
96
+ output.stdout(arguments_.json
97
+ ? terminalSafeJson(report)
98
+ : sanitizeTerminalText(renderDoctorReport(report)));
99
+ const requiredReady = report.bun.status === "ready"
100
+ && report.dependencies.every(({ status }) => status === "ready");
101
+ return requiredReady ? 0 : 4;
102
+ }
103
+
104
+ /** CLI entry point, split out so argument and output behavior can be forward-tested. */
105
+ export async function main(
106
+ rawArguments: readonly string[] = process.argv.slice(2),
107
+ environment: Readonly<Record<string, string | undefined>> = process.env,
108
+ output: Output = defaultOutput,
109
+ dependencies: CliDependencies = {},
110
+ runtimeOptions: ClipRuntimeOptions = {},
111
+ ): Promise<number> {
112
+ const parsed = parseArguments(rawArguments, environment);
113
+ if (!parsed.ok) {
114
+ output.stderr(`error: ${safe(parsed.message)}\n\n${sanitizeTerminalText(usage)}`);
115
+ return 2;
116
+ }
117
+ const arguments_ = parsed.value;
118
+ if (arguments_.command === "help") {
119
+ output.stdout(sanitizeTerminalText(usage));
120
+ return 0;
121
+ }
122
+ if (arguments_.command === "doctor") {
123
+ return diagnosticCommand(arguments_, output, dependencies.inspectClipEnvironment ?? inspectClipEnvironment);
124
+ }
125
+ if (arguments_.command === "adapters") {
126
+ output.stdout(arguments_.json
127
+ ? terminalSafeJson({ schemaVersion: 1, adapters: adapterCapabilities })
128
+ : sanitizeTerminalText(renderAdapterCapabilities()));
129
+ return 0;
130
+ }
131
+
132
+ if (!arguments_.quiet && !arguments_.json) {
133
+ const target = arguments_.currentTab ? "the current browser tab" : safe(arguments_.url?.href ?? "current");
134
+ output.stderr(`Capturing ${target} (${arguments_.mode}, ${arguments_.scope}) ...\n`);
135
+ }
136
+ try {
137
+ if (
138
+ runtimeOptions.ownedBrowserProfile !== undefined
139
+ && arguments_.browserProfile !== runtimeOptions.ownedBrowserProfile.path
140
+ ) {
141
+ throw new Error("owned browser-profile execution does not match the selected private profile path");
142
+ }
143
+ const captureArguments = runtimeOptions.ownedBrowserProfile === undefined
144
+ ? {
145
+ ...arguments_,
146
+ ...(runtimeOptions.browserExecutable === undefined
147
+ ? {}
148
+ : { browserExecutable: runtimeOptions.browserExecutable }),
149
+ }
150
+ : {
151
+ ...arguments_,
152
+ browserProfileOwnership: "owned" as const,
153
+ ...(runtimeOptions.browserExecutable === undefined
154
+ ? {}
155
+ : { browserExecutable: runtimeOptions.browserExecutable }),
156
+ ...(runtimeOptions.ownedBrowserProfile.profileDirectory === undefined
157
+ ? {}
158
+ : { browserProfileDirectory: runtimeOptions.ownedBrowserProfile.profileDirectory }),
159
+ };
160
+ const outcome = await (dependencies.runCapture ?? runCapture)(captureArguments);
161
+ if (arguments_.json) {
162
+ output.stdout(terminalSafeJson(captureSummary(outcome)));
163
+ } else if (arguments_.stdout) {
164
+ output.stdout(sanitizeTerminalText(outcome.markdown));
165
+ } else {
166
+ output.stdout(line(safe(`Done: ${outcome.markdownPath ?? outcome.outputDirectory ?? outcome.slug}`)));
167
+ output.stdout(line(safe(`Status: ${outcome.status}; ${outcome.wordCount} words; ${outcome.capturedItems}${outcome.expectedItems === null ? "" : `/${outcome.expectedItems}`} items; ${outcome.assetCount} assets.`)));
168
+ }
169
+ if (!arguments_.quiet && outcome.warnings.length > 0) {
170
+ for (const warning of outcome.warnings) output.stderr(`warning: ${safe(warning)}\n`);
171
+ }
172
+ return captureExitCode(outcome);
173
+ } catch (error) {
174
+ const message = safe(error instanceof Error ? error.message : String(error));
175
+ if (arguments_.json) output.stdout(terminalSafeJson({ ok: false, error: message }));
176
+ else output.stderr(`error: ${message}\n`);
177
+ return 1;
178
+ }
179
+ }
180
+
181
+ if (import.meta.main) process.exitCode = await main();
@@ -0,0 +1,512 @@
1
+ import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ export const MAX_COOKIE_RECORDS = 4_096;
5
+ export const MAX_COOKIE_BYTES = 2 * 1024 * 1024;
6
+
7
+ export type StrictCookie = {
8
+ readonly name: string;
9
+ readonly value: string;
10
+ readonly domain: string;
11
+ readonly hostOnly: boolean;
12
+ readonly path: string;
13
+ readonly secure: boolean;
14
+ readonly httpOnly: boolean;
15
+ readonly sameSite: "Strict" | "Lax" | "None" | null;
16
+ readonly expires: number;
17
+ };
18
+
19
+ export type FilteredCookies = {
20
+ readonly cookies: readonly StrictCookie[];
21
+ readonly rejected: number;
22
+ };
23
+
24
+ export type CookieFileResult =
25
+ | ({
26
+ readonly ok: true;
27
+ readonly format: "json" | "base64-json" | "netscape" | "cookie-header" | "curl";
28
+ /** Whether every accepted source record carried its own domain or URL scope. */
29
+ readonly scopeProvenance: "explicit" | "target-inferred";
30
+ } & FilteredCookies)
31
+ | { readonly ok: false; readonly reason: "unavailable" | "unsafe-permissions" | "too-large" | "invalid" | "empty" };
32
+
33
+ export type CookieProviderResult = FilteredCookies & {
34
+ readonly validShape: boolean;
35
+ readonly providerWarningCount: number;
36
+ };
37
+
38
+ type CandidateCookie = Readonly<Record<string, unknown>>;
39
+
40
+ const cookieNamePattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
41
+ const cookieValuePattern = /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*$/;
42
+ const quotedCookieValuePattern = /^"[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*"$/;
43
+ const cookieDomainPattern = /^[a-z0-9.-]+$/i;
44
+
45
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
46
+ typeof value === "object" && value !== null && !Array.isArray(value);
47
+ const isUnknownArray = (value: unknown): value is unknown[] => Array.isArray(value);
48
+
49
+ function hasControlCharacter(value: string): boolean {
50
+ for (let index = 0; index < value.length; index += 1) {
51
+ const code = value.charCodeAt(index);
52
+ if (code <= 0x1f || code === 0x7f) return true;
53
+ }
54
+ return false;
55
+ }
56
+
57
+ function canonicalHostname(value: string): string | null {
58
+ const trimmed = value.trim().toLowerCase().replace(/^\.+/, "").replace(/\.$/, "");
59
+ if (trimmed === "" || trimmed.length > 253 || trimmed.includes("..") || !cookieDomainPattern.test(trimmed)) {
60
+ return null;
61
+ }
62
+ try {
63
+ const hostname = new URL(`http://${trimmed}/`).hostname.toLowerCase().replace(/\.$/, "");
64
+ return hostname === "" || hostname.length > 253 ? null : hostname;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ function domainMatches(hostname: string, domain: string, hostOnly: boolean): boolean {
71
+ return hostname === domain || (!hostOnly && hostname.endsWith(`.${domain}`));
72
+ }
73
+
74
+ function pathMatches(requestPath: string, cookiePath: string): boolean {
75
+ if (requestPath === cookiePath) return true;
76
+ if (!requestPath.startsWith(cookiePath)) return false;
77
+ return cookiePath.endsWith("/") || requestPath[cookiePath.length] === "/";
78
+ }
79
+
80
+ function safeCookiePath(value: unknown): string | null {
81
+ const path = value === undefined ? "/" : value;
82
+ if (
83
+ typeof path !== "string"
84
+ || !path.startsWith("/")
85
+ || path.length > 4_096
86
+ || hasControlCharacter(path)
87
+ ) return null;
88
+ return path;
89
+ }
90
+
91
+ function cookieExpiry(value: CandidateCookie, nowSeconds: number): number | null {
92
+ const raw = value.expires ?? value.expirationDate;
93
+ if (raw === undefined || raw === null) return 0;
94
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
95
+ if (raw === 0) return 0;
96
+ if (raw <= nowSeconds || raw > 253_402_300_799) return null;
97
+ return Math.trunc(raw);
98
+ }
99
+
100
+ function cookieSameSite(value: CandidateCookie): StrictCookie["sameSite"] | undefined {
101
+ if (value.sameSite === undefined || value.sameSite === null) return null;
102
+ if (typeof value.sameSite !== "string") return undefined;
103
+ switch (value.sameSite.toLowerCase()) {
104
+ case "strict": return "Strict";
105
+ case "lax": return "Lax";
106
+ case "none":
107
+ case "no_restriction": return "None";
108
+ case "unspecified": return null;
109
+ default: return undefined;
110
+ }
111
+ }
112
+
113
+ function candidateDomain(
114
+ value: CandidateCookie,
115
+ target: URL,
116
+ ): { readonly domain: string; readonly hostOnly: boolean } | null {
117
+ const targetHostname = target.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
118
+ let rawDomain: string | undefined;
119
+ if (typeof value.domain === "string") rawDomain = value.domain;
120
+ else if (value.domain !== undefined) return null;
121
+
122
+ let urlHostname: string | undefined;
123
+ if (value.url !== undefined) {
124
+ if (typeof value.url !== "string" || value.url.length > 8_192) return null;
125
+ try {
126
+ const url = new URL(value.url);
127
+ if (
128
+ (url.protocol !== "http:" && url.protocol !== "https:")
129
+ || url.username !== ""
130
+ || url.password !== ""
131
+ ) return null;
132
+ urlHostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ if (rawDomain === undefined && urlHostname === undefined) {
139
+ return { domain: targetHostname, hostOnly: true };
140
+ }
141
+ const hadLeadingDot = rawDomain?.trim().startsWith(".") === true;
142
+ const domain = canonicalHostname(rawDomain ?? urlHostname ?? "");
143
+ if (domain === null) return null;
144
+ const explicitHostOnly = value.hostOnly;
145
+ if (explicitHostOnly !== undefined && typeof explicitHostOnly !== "boolean") return null;
146
+ const hostOnly = typeof explicitHostOnly === "boolean"
147
+ ? explicitHostOnly
148
+ : !hadLeadingDot;
149
+ if (!domainMatches(targetHostname, domain, hostOnly)) return null;
150
+ if (urlHostname !== undefined && !domainMatches(urlHostname, domain, hostOnly)) return null;
151
+ return { domain, hostOnly };
152
+ }
153
+
154
+ function hasSafeUnpartitionedProvenance(value: CandidateCookie): boolean {
155
+ for (const field of ["partitionKey", "topFrameSiteKey", "top_frame_site_key", "originAttributes"] as const) {
156
+ const provenance = value[field];
157
+ if (provenance === undefined || provenance === null) continue;
158
+ if (typeof provenance !== "string" || provenance.trim() !== "") return false;
159
+ }
160
+ for (const field of ["partitioned"] as const) {
161
+ const flag = value[field];
162
+ if (flag === undefined || flag === null) continue;
163
+ if (typeof flag !== "boolean" || flag) return false;
164
+ }
165
+ for (const field of ["isPartitionedAttributeSet", "hasCrossSiteAncestor", "has_cross_site_ancestor"] as const) {
166
+ const flag = value[field];
167
+ if (flag === undefined || flag === null) continue;
168
+ if (flag !== false && flag !== 0 && flag !== "0") return false;
169
+ }
170
+ return true;
171
+ }
172
+
173
+ function validatedCookie(
174
+ value: unknown,
175
+ target: URL,
176
+ nowSeconds: number,
177
+ ): StrictCookie | null {
178
+ if (!isRecord(value)) return null;
179
+ if (
180
+ typeof value.name !== "string"
181
+ || value.name.length > 1_024
182
+ || !cookieNamePattern.test(value.name)
183
+ ) return null;
184
+ if (
185
+ typeof value.value !== "string"
186
+ || value.value.length > 64 * 1024
187
+ || (!cookieValuePattern.test(value.value) && !quotedCookieValuePattern.test(value.value))
188
+ ) return null;
189
+ if (!hasSafeUnpartitionedProvenance(value)) return null;
190
+ const domain = candidateDomain(value, target);
191
+ const path = safeCookiePath(value.path);
192
+ const expires = cookieExpiry(value, nowSeconds);
193
+ const sameSite = cookieSameSite(value);
194
+ if (domain === null || path === null || expires === null || sameSite === undefined) return null;
195
+ if (!pathMatches(target.pathname || "/", path)) return null;
196
+ if (value.secure !== undefined && typeof value.secure !== "boolean") return null;
197
+ if (value.httpOnly !== undefined && typeof value.httpOnly !== "boolean") return null;
198
+ const secure = value.secure === true;
199
+ if (secure && target.protocol !== "https:") return null;
200
+ if (sameSite === "None" && !secure) return null;
201
+ return {
202
+ name: value.name,
203
+ value: value.value,
204
+ domain: domain.domain,
205
+ hostOnly: domain.hostOnly,
206
+ path,
207
+ secure,
208
+ httpOnly: value.httpOnly === true,
209
+ sameSite,
210
+ expires,
211
+ };
212
+ }
213
+
214
+ function cookieBytes(cookie: StrictCookie): number {
215
+ return Buffer.byteLength(
216
+ `${cookie.domain}\t${cookie.path}\t${cookie.name}\t${cookie.value}\n`,
217
+ "utf8",
218
+ );
219
+ }
220
+
221
+ /** Apply browser-like domain, path, Secure, expiry, syntax, count, and byte bounds. */
222
+ export function filterCookies(
223
+ values: readonly unknown[],
224
+ target: URL,
225
+ nowSeconds = Math.floor(Date.now() / 1_000),
226
+ ): FilteredCookies {
227
+ const bounded = values.slice(0, MAX_COOKIE_RECORDS);
228
+ let rejected = Math.max(0, values.length - bounded.length);
229
+ let totalBytes = 0;
230
+ const cookies = new Map<string, StrictCookie>();
231
+ for (const value of bounded) {
232
+ const cookie = validatedCookie(value, target, nowSeconds);
233
+ if (cookie === null) {
234
+ rejected += 1;
235
+ continue;
236
+ }
237
+ const key = `${cookie.domain}\0${cookie.hostOnly ? "host" : "domain"}\0${cookie.path}\0${cookie.name}`;
238
+ const previous = cookies.get(key);
239
+ const nextBytes = totalBytes - (previous === undefined ? 0 : cookieBytes(previous)) + cookieBytes(cookie);
240
+ if (nextBytes > MAX_COOKIE_BYTES) {
241
+ rejected += 1;
242
+ continue;
243
+ }
244
+ cookies.set(key, cookie);
245
+ totalBytes = nextBytes;
246
+ }
247
+ return {
248
+ cookies: [...cookies.values()].sort((left, right) =>
249
+ right.path.length - left.path.length
250
+ || left.name.localeCompare(right.name)
251
+ || left.domain.localeCompare(right.domain)),
252
+ rejected,
253
+ };
254
+ }
255
+
256
+ function jsonCookieArray(value: unknown): readonly unknown[] | null {
257
+ if (isUnknownArray(value)) return value;
258
+ return isRecord(value) && isUnknownArray(value.cookies) ? value.cookies : null;
259
+ }
260
+
261
+ function parseJson(input: string): readonly unknown[] | null {
262
+ try {
263
+ return jsonCookieArray(JSON.parse(input) as unknown);
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
268
+
269
+ function parseBase64Json(input: string): readonly unknown[] | null {
270
+ const compact = input.replace(/\s+/g, "");
271
+ if (compact === "" || compact.length > MAX_COOKIE_BYTES * 2 || !/^[a-z0-9+/]+=*$/i.test(compact)) return null;
272
+ try {
273
+ const decoded = Buffer.from(compact, "base64");
274
+ return decoded.byteLength > MAX_COOKIE_BYTES ? null : parseJson(decoded.toString("utf8"));
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+
280
+ function hasExplicitCookieScope(value: unknown): boolean {
281
+ if (!isRecord(value)) return false;
282
+ return (typeof value.domain === "string" && value.domain.trim() !== "")
283
+ || (typeof value.url === "string" && value.url.trim() !== "");
284
+ }
285
+
286
+ function parseNetscape(input: string): readonly unknown[] | null {
287
+ const cookies: unknown[] = [];
288
+ let looksLikeNetscape = /^# Netscape HTTP Cookie File/im.test(input);
289
+ let cursor = 0;
290
+ while (cursor <= input.length && cookies.length <= MAX_COOKIE_RECORDS) {
291
+ const newline = input.indexOf("\n", cursor);
292
+ const lineEnd = newline === -1 ? input.length : newline;
293
+ const rawLine = input.slice(cursor, lineEnd).replace(/\r$/, "");
294
+ cursor = newline === -1 ? input.length + 1 : newline + 1;
295
+ const line = rawLine.trim();
296
+ if (line === "" || (line.startsWith("#") && !line.startsWith("#HttpOnly_"))) continue;
297
+ if (line.length > 80 * 1024) continue;
298
+ const columns = line.split("\t", 8);
299
+ if (columns.length < 7) continue;
300
+ looksLikeNetscape = true;
301
+ const rawDomain = columns[0];
302
+ const includeSubdomains = columns[1];
303
+ const path = columns[2];
304
+ const secure = columns[3];
305
+ const rawExpires = columns[4];
306
+ const name = columns[5];
307
+ const value = columns.slice(6).join("\t");
308
+ if (
309
+ rawDomain === undefined
310
+ || includeSubdomains === undefined
311
+ || path === undefined
312
+ || secure === undefined
313
+ || rawExpires === undefined
314
+ || name === undefined
315
+ ) continue;
316
+ const httpOnly = rawDomain.startsWith("#HttpOnly_");
317
+ const domain = httpOnly ? rawDomain.slice("#HttpOnly_".length) : rawDomain;
318
+ const expires = Number(rawExpires);
319
+ cookies.push({
320
+ name,
321
+ value,
322
+ domain,
323
+ hostOnly: includeSubdomains.toUpperCase() !== "TRUE",
324
+ path,
325
+ secure: secure.toUpperCase() === "TRUE",
326
+ httpOnly,
327
+ ...(Number.isFinite(expires) && expires > 0 ? { expires } : {}),
328
+ });
329
+ }
330
+ return looksLikeNetscape ? cookies : null;
331
+ }
332
+
333
+ function unquote(value: string): string {
334
+ const trimmed = value.trim();
335
+ const first = trimmed[0];
336
+ return (first === "'" || first === '"') && trimmed.at(-1) === first
337
+ ? trimmed.slice(1, -1)
338
+ : trimmed;
339
+ }
340
+
341
+ function parseCookieHeaderValue(value: string, target: URL): readonly unknown[] {
342
+ const cookies: unknown[] = [];
343
+ const restrictivePath = target.pathname === "" ? "/" : target.pathname;
344
+ let cursor = 0;
345
+ while (cursor <= value.length && cookies.length <= MAX_COOKIE_RECORDS) {
346
+ const delimiter = value.indexOf(";", cursor);
347
+ const pairEnd = delimiter === -1 ? value.length : delimiter;
348
+ const pair = value.slice(cursor, pairEnd);
349
+ cursor = delimiter === -1 ? value.length + 1 : delimiter + 1;
350
+ const separator = pair.indexOf("=");
351
+ if (separator < 1) continue;
352
+ cookies.push({
353
+ name: pair.slice(0, separator).trim(),
354
+ value: pair.slice(separator + 1).trim(),
355
+ domain: target.hostname,
356
+ hostOnly: true,
357
+ path: restrictivePath,
358
+ secure: target.protocol === "https:",
359
+ httpOnly: true,
360
+ sameSite: "Strict",
361
+ });
362
+ }
363
+ return cookies;
364
+ }
365
+
366
+ function curlCookieValue(input: string): { readonly value: string; readonly curl: boolean } | null {
367
+ const patterns = [
368
+ /(?:^|\s)(?:-b|--cookie)(?:=|\s+)(('[^']*')|("[^"]*")|[^\s]+)/i,
369
+ /(?:^|\s)(?:-H|--header)(?:=|\s+)(('Cookie:\s*[^']*')|("Cookie:\s*[^"]*"))/i,
370
+ ];
371
+ for (const pattern of patterns) {
372
+ const raw = pattern.exec(input)?.[1];
373
+ if (raw === undefined) continue;
374
+ return { value: unquote(raw).replace(/^Cookie:\s*/i, ""), curl: true };
375
+ }
376
+ const header = /^Cookie:\s*([^\r\n]*)$/im.exec(input)?.[1];
377
+ if (header !== undefined) return { value: header, curl: false };
378
+ const trimmed = input.trim();
379
+ return !trimmed.includes("\n") && trimmed.includes("=")
380
+ ? { value: trimmed, curl: false }
381
+ : null;
382
+ }
383
+
384
+ /** Parse supported explicit cookie payloads, then apply the same strict request filter used for browsers. */
385
+ export function parseCookiePayload(
386
+ input: string,
387
+ target: URL,
388
+ nowSeconds = Math.floor(Date.now() / 1_000),
389
+ ): CookieFileResult {
390
+ if (Buffer.byteLength(input, "utf8") > MAX_COOKIE_BYTES) return { ok: false, reason: "too-large" };
391
+ if (input.trim() === "") return { ok: false, reason: "empty" };
392
+ let values = parseJson(input);
393
+ let format: Extract<CookieFileResult, { readonly ok: true }>["format"] = "json";
394
+ if (values === null) {
395
+ values = parseBase64Json(input);
396
+ format = "base64-json";
397
+ }
398
+ if (values === null) {
399
+ values = parseNetscape(input);
400
+ format = "netscape";
401
+ }
402
+ if (values === null) {
403
+ const header = curlCookieValue(input);
404
+ if (header !== null) {
405
+ values = parseCookieHeaderValue(header.value, target);
406
+ format = header.curl ? "curl" : "cookie-header";
407
+ }
408
+ }
409
+ if (values === null) return { ok: false, reason: "invalid" };
410
+ const scopeProvenance = format === "netscape"
411
+ || ((format === "json" || format === "base64-json") && values.every(hasExplicitCookieScope))
412
+ ? "explicit"
413
+ : "target-inferred";
414
+ const filtered = filterCookies(values, target, nowSeconds);
415
+ return filtered.cookies.length === 0
416
+ ? { ok: false, reason: "empty" }
417
+ : { ok: true, format, scopeProvenance, ...filtered };
418
+ }
419
+
420
+ /** Read only one explicit bounded regular file. It never falls back to browser providers. */
421
+ export function readCookieFile(
422
+ path: string,
423
+ target: URL,
424
+ options: {
425
+ /** Test seam. Production callers must leave this unset. */
426
+ readonly afterOpen?: () => void;
427
+ /** Authenticated API callers require a current-user-owned mode-0600-style file. */
428
+ readonly requirePrivate?: boolean;
429
+ } = {},
430
+ ): CookieFileResult {
431
+ let descriptor: number;
432
+ try {
433
+ const absolute = resolve(path);
434
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
435
+ const nonBlocking = "O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0;
436
+ descriptor = openSync(absolute, constants.O_RDONLY | noFollow | nonBlocking);
437
+ } catch {
438
+ return { ok: false, reason: "unavailable" };
439
+ }
440
+ try {
441
+ options.afterOpen?.();
442
+ const stats = fstatSync(descriptor);
443
+ if (!stats.isFile()) return { ok: false, reason: "unavailable" };
444
+ if (
445
+ options.requirePrivate === true
446
+ && ((stats.mode & 0o077) !== 0
447
+ || (typeof process.getuid === "function" && stats.uid !== process.getuid()))
448
+ ) return { ok: false, reason: "unsafe-permissions" };
449
+ if (stats.size > MAX_COOKIE_BYTES) return { ok: false, reason: "too-large" };
450
+ const chunks: Buffer[] = [];
451
+ const buffer = Buffer.allocUnsafe(64 * 1024);
452
+ let total = 0;
453
+ for (;;) {
454
+ const count = readSync(descriptor, buffer, 0, buffer.byteLength, null);
455
+ if (count === 0) break;
456
+ total += count;
457
+ if (total > MAX_COOKIE_BYTES) return { ok: false, reason: "too-large" };
458
+ chunks.push(Buffer.from(buffer.subarray(0, count)));
459
+ }
460
+ let text: string;
461
+ try {
462
+ text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total));
463
+ } catch {
464
+ return { ok: false, reason: "invalid" };
465
+ }
466
+ return parseCookiePayload(text, target);
467
+ } catch {
468
+ return { ok: false, reason: "unavailable" };
469
+ } finally {
470
+ closeSync(descriptor);
471
+ }
472
+ }
473
+
474
+ /** Parse a foreign Sweet Cookie result without retaining its warning strings or leaking provider errors. */
475
+ export function filterCookieProviderResult(value: unknown, target: URL): CookieProviderResult {
476
+ if (!isRecord(value) || !Array.isArray(value.cookies)) {
477
+ return { validShape: false, cookies: [], rejected: 0, providerWarningCount: 0 };
478
+ }
479
+ // Browser databases distinguish host-only, Domain, partitioned, and container
480
+ // cookies. A provider that drops that provenance cannot be replayed safely.
481
+ // Require the hostOnly field retained by the pinned provider fork; this also
482
+ // makes an install with an incompatible provider fail closed.
483
+ const provenancePreserving = value.cookies.filter((cookie) =>
484
+ isRecord(cookie) && typeof cookie.hostOnly === "boolean");
485
+ const missingProvenance = value.cookies.length - provenancePreserving.length;
486
+ const filtered = filterCookies(provenancePreserving, target, Math.floor(Date.now() / 1_000));
487
+ return {
488
+ validShape: true,
489
+ ...filtered,
490
+ rejected: filtered.rejected + missingProvenance,
491
+ providerWarningCount: Array.isArray(value.warnings) ? value.warnings.length : 0,
492
+ };
493
+ }
494
+
495
+ /** Render a browser-ordered Cookie header from already filtered records. */
496
+ export function renderCookieHeader(cookies: readonly StrictCookie[]): string {
497
+ return cookies.map(({ name, value }) => `${name}=${value}`).join("; ");
498
+ }
499
+
500
+ /** Render a host-pinned private Netscape jar for yt-dlp. */
501
+ export function renderNetscapeCookieJar(cookies: readonly StrictCookie[], target: URL): string {
502
+ const hostname = target.hostname.toLowerCase().replace(/^\[|\]$/g, "");
503
+ return [
504
+ "# Netscape HTTP Cookie File",
505
+ "# Created temporarily by kb clip; deleted after media capture.",
506
+ ...cookies.map((cookie) => {
507
+ const domain = `${cookie.httpOnly ? "#HttpOnly_" : ""}${hostname}`;
508
+ return `${domain}\tFALSE\t${cookie.path}\t${cookie.secure ? "TRUE" : "FALSE"}\t${cookie.expires}\t${cookie.name}\t${cookie.value}`;
509
+ }),
510
+ "",
511
+ ].join("\n");
512
+ }