@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.
- package/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
package/src/clip/cli.ts
ADDED
|
@@ -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
|
+
}
|