harnery 0.37.0 → 0.38.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/dist/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +7 -2
- package/dist/commands/admission.d.ts +21 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +565 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +61 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +48 -2
- package/dist/commands/browse-ai.d.ts +2 -2
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +6 -4
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +349 -21
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/qa-record.d.ts +144 -0
- package/dist/commands/qa-record.d.ts.map +1 -0
- package/dist/commands/qa-record.js +0 -0
- package/dist/commands/qa-run.d.ts +7 -3
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +268 -13
- package/dist/commands/qa-status.d.ts +71 -0
- package/dist/commands/qa-status.d.ts.map +1 -0
- package/dist/commands/qa-status.js +490 -0
- package/dist/commands/qa-verify.d.ts +40 -0
- package/dist/commands/qa-verify.d.ts.map +1 -0
- package/dist/commands/qa-verify.js +180 -0
- package/dist/commands/review-pack.d.ts +4 -0
- package/dist/commands/review-pack.d.ts.map +1 -0
- package/dist/commands/review-pack.js +1001 -0
- package/dist/core/agents/qa-signal.d.ts +111 -0
- package/dist/core/agents/qa-signal.d.ts.map +1 -0
- package/dist/core/agents/qa-signal.js +231 -0
- package/dist/core/agents/session-name-display.d.ts +20 -5
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +67 -7
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +17 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/artifacts/constants.d.ts +1 -1
- package/dist/core/artifacts/constants.js +1 -1
- package/dist/core/artifacts/index.d.ts +57 -6
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +265 -10
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -0
- package/dist/core/diagnostics/bundle.d.ts +16 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +101 -7
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +10 -0
- package/dist/core/events/v3/coordination-view.d.ts +3 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-view.js +69 -8
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +9 -2
- package/dist/core/events/v3/producers/recorder.d.ts +23 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +264 -18
- package/dist/core/hooks/cli.js +120 -27
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +10 -3
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +4 -1
- package/dist/core/qa-artifacts.d.ts +20 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -0
- package/dist/core/qa-artifacts.js +110 -0
- package/dist/core/resources/contract.d.ts +6 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/sampler.d.ts +7 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +106 -5
- package/dist/lib/admission.d.ts +71 -0
- package/dist/lib/admission.d.ts.map +1 -0
- package/dist/lib/admission.js +264 -0
- package/dist/lib/agent-browser/client.d.ts +1 -1
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +1 -5
- package/dist/lib/browser/capture-fidelity.d.ts +39 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
- package/dist/lib/browser/capture-fidelity.js +84 -0
- package/dist/lib/browser/client.d.ts +41 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +167 -9
- package/dist/lib/browser/critique.d.ts +38 -1
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +34 -6
- package/dist/lib/browser/index.d.ts +4 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/page-review-judge.d.ts +64 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
- package/dist/lib/browser/page-review-judge.js +270 -0
- package/dist/lib/browser/page-review-pack.d.ts +613 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
- package/dist/lib/browser/page-review-pack.js +1751 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +136 -1
- package/dist/lib/browser/qa-run.d.ts +100 -10
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +768 -169
- package/dist/lib/browser/request-diagnostics.d.ts +13 -0
- package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
- package/dist/lib/browser/request-diagnostics.js +18 -0
- package/dist/lib/browser/tiling.d.ts +19 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +28 -0
- package/dist/lib/cookies/client.d.ts +9 -0
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +197 -44
- package/dist/lib/cookies/extra.d.ts +18 -0
- package/dist/lib/cookies/extra.d.ts.map +1 -0
- package/dist/lib/cookies/extra.js +14 -0
- package/dist/lib/cookies/index.d.ts +2 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +2 -1
- package/dist/lib/durable-job.d.ts +124 -0
- package/dist/lib/durable-job.d.ts.map +1 -0
- package/dist/lib/durable-job.js +296 -0
- package/dist/lib/http/client.d.ts +7 -1
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +2 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +5 -2
- package/package.json +8 -2
- package/src/commander.ts +50 -2
- package/src/commands/admission.ts +699 -0
- package/src/commands/agents.ts +87 -1
- package/src/commands/artifacts.ts +97 -21
- package/src/commands/browse-ai.ts +10 -5
- package/src/commands/browse.ts +481 -20
- package/src/commands/fetch.ts +1 -0
- package/src/commands/qa-record.ts +682 -0
- package/src/commands/qa-run.ts +335 -16
- package/src/commands/qa-status.ts +608 -0
- package/src/commands/qa-verify.ts +238 -0
- package/src/commands/review-pack.ts +1281 -0
- package/src/core/agents/qa-signal.ts +261 -0
- package/src/core/agents/session-name-display.ts +78 -7
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +23 -0
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +5 -0
- package/src/core/artifacts/constants.ts +1 -1
- package/src/core/artifacts/index.ts +370 -21
- package/src/core/config.ts +23 -0
- package/src/core/diagnostics/bundle.ts +119 -11
- package/src/core/events/v3/bootstrap.ts +10 -0
- package/src/core/events/v3/coordination-view.ts +100 -11
- package/src/core/events/v3/producers/intake.ts +9 -2
- package/src/core/events/v3/producers/recorder.ts +312 -18
- package/src/core/hooks/cli.ts +140 -32
- package/src/core/hooks/resolve/transcript.ts +10 -3
- package/src/core/hooks/session-name-presence.ts +6 -1
- package/src/core/qa-artifacts.ts +126 -0
- package/src/core/resources/contract.ts +7 -0
- package/src/core/resources/sampler.ts +138 -6
- package/src/lib/admission.ts +347 -0
- package/src/lib/agent-browser/client.ts +2 -10
- package/src/lib/browser/capture-fidelity.ts +98 -0
- package/src/lib/browser/client.ts +206 -10
- package/src/lib/browser/critique.ts +62 -7
- package/src/lib/browser/index.ts +36 -0
- package/src/lib/browser/page-review-judge.ts +360 -0
- package/src/lib/browser/page-review-pack.ts +2384 -0
- package/src/lib/browser/qa-run-contracts.ts +366 -3
- package/src/lib/browser/qa-run.ts +868 -190
- package/src/lib/browser/request-diagnostics.ts +27 -0
- package/src/lib/browser/tiling.ts +32 -0
- package/src/lib/cookies/client.ts +228 -42
- package/src/lib/cookies/extra.ts +28 -0
- package/src/lib/cookies/index.ts +2 -0
- package/src/lib/durable-job.ts +407 -0
- package/src/lib/http/client.ts +13 -1
- package/src/lib/instructions/templates.ts +5 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface FailedRequestCandidate {
|
|
2
|
+
url: string;
|
|
3
|
+
method: string;
|
|
4
|
+
failure: string;
|
|
5
|
+
resourceType: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Next.js may start a speculative React Server Component fetch and cancel it
|
|
10
|
+
* once the destination is no longer needed. Chromium reports that deliberate
|
|
11
|
+
* cancellation as net::ERR_ABORTED even though the rendered page is healthy.
|
|
12
|
+
*/
|
|
13
|
+
export function isBenignNextRscAbort(request: FailedRequestCandidate): boolean {
|
|
14
|
+
if (
|
|
15
|
+
request.failure !== "net::ERR_ABORTED" ||
|
|
16
|
+
request.method !== "GET" ||
|
|
17
|
+
request.resourceType !== "fetch"
|
|
18
|
+
) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return new URL(request.url).searchParams.has("_rsc");
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -162,3 +162,35 @@ export function bandOversizedRect(
|
|
|
162
162
|
height: r.height,
|
|
163
163
|
}));
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Indices of bands past the kept prefix (`allBands[0..keptCount)`) that
|
|
168
|
+
* intersect any of `rects`. A tile cap keeps the top of a tall page and drops
|
|
169
|
+
* the rest; a deterministic gate that already found a defect below the cap
|
|
170
|
+
* still deserves a tile, so the caller cuts these bands as well and labels
|
|
171
|
+
* them as hit bands. Pure: the result is deduplicated, ascending, and holds
|
|
172
|
+
* at most `maxExtra` entries. Rects and bands share one pixel space.
|
|
173
|
+
*/
|
|
174
|
+
export function bandsCoveringRects(
|
|
175
|
+
allBands: ReadonlyArray<{ x: number; y: number; width: number; height: number }>,
|
|
176
|
+
keptCount: number,
|
|
177
|
+
rects: ReadonlyArray<{ x: number; y: number; width: number; height: number }>,
|
|
178
|
+
maxExtra = 50,
|
|
179
|
+
): number[] {
|
|
180
|
+
const out: number[] = [];
|
|
181
|
+
if (rects.length === 0 || maxExtra <= 0) return out;
|
|
182
|
+
for (let index = Math.max(0, keptCount); index < allBands.length; index++) {
|
|
183
|
+
if (out.length >= maxExtra) break;
|
|
184
|
+
const band = allBands[index];
|
|
185
|
+
if (!band) continue;
|
|
186
|
+
const hit = rects.some((rect) => {
|
|
187
|
+
const x1 = rect.x + Math.max(rect.width, 1);
|
|
188
|
+
const y1 = rect.y + Math.max(rect.height, 1);
|
|
189
|
+
return (
|
|
190
|
+
band.x < x1 && band.x + band.width > rect.x && band.y < y1 && band.y + band.height > rect.y
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
if (hit) out.push(index);
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
statSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { basename, dirname, join } from "node:path";
|
|
3
14
|
|
|
4
15
|
/**
|
|
5
16
|
* Shared cookie-store library.
|
|
@@ -58,6 +69,160 @@ export interface InfoResult {
|
|
|
58
69
|
exportedFrom?: string;
|
|
59
70
|
}
|
|
60
71
|
|
|
72
|
+
export class CookieStoreParseError extends Error {
|
|
73
|
+
readonly path: string;
|
|
74
|
+
|
|
75
|
+
constructor(path: string, cause: unknown) {
|
|
76
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
77
|
+
super(
|
|
78
|
+
`Cookie-store parse failed at "${path}" after a stable read: ${detail}. ` +
|
|
79
|
+
"The original cookie-store file was left unchanged.",
|
|
80
|
+
{ cause },
|
|
81
|
+
);
|
|
82
|
+
this.name = "CookieStoreParseError";
|
|
83
|
+
this.path = path;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const LOCK_WAIT_MS = 10_000;
|
|
88
|
+
const LOCK_POLL_MS = 10;
|
|
89
|
+
const STABLE_READ_ATTEMPTS = 5;
|
|
90
|
+
const STABLE_READ_POLL_MS = 10;
|
|
91
|
+
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
92
|
+
|
|
93
|
+
function sleepSync(ms: number): void {
|
|
94
|
+
Atomics.wait(sleepBuffer, 0, 0, ms);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isErrorCode(error: unknown, code: string): boolean {
|
|
98
|
+
return (error as NodeJS.ErrnoException).code === code;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface FileSnapshot {
|
|
102
|
+
dev: number;
|
|
103
|
+
ino: number;
|
|
104
|
+
size: number;
|
|
105
|
+
mtimeMs: number;
|
|
106
|
+
ctimeMs: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function snapshot(path: string): FileSnapshot {
|
|
110
|
+
const stat = statSync(path);
|
|
111
|
+
return {
|
|
112
|
+
dev: stat.dev,
|
|
113
|
+
ino: stat.ino,
|
|
114
|
+
size: stat.size,
|
|
115
|
+
mtimeMs: stat.mtimeMs,
|
|
116
|
+
ctimeMs: stat.ctimeMs,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sameSnapshot(left: FileSnapshot, right: FileSnapshot): boolean {
|
|
121
|
+
return (
|
|
122
|
+
left.dev === right.dev &&
|
|
123
|
+
left.ino === right.ino &&
|
|
124
|
+
left.size === right.size &&
|
|
125
|
+
left.mtimeMs === right.mtimeMs &&
|
|
126
|
+
left.ctimeMs === right.ctimeMs
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readStableFile(path: string): string | undefined {
|
|
131
|
+
for (let attempt = 1; attempt <= STABLE_READ_ATTEMPTS; attempt++) {
|
|
132
|
+
let before: FileSnapshot;
|
|
133
|
+
try {
|
|
134
|
+
before = snapshot(path);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (isErrorCode(error, "ENOENT")) return undefined;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const raw = readFileSync(path, "utf8");
|
|
142
|
+
const after = snapshot(path);
|
|
143
|
+
if (sameSnapshot(before, after)) return raw;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (!isErrorCode(error, "ENOENT")) throw error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (attempt < STABLE_READ_ATTEMPTS) sleepSync(STABLE_READ_POLL_MS);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Cookie-store read failed at "${path}": the file did not remain stable across ` +
|
|
153
|
+
`${STABLE_READ_ATTEMPTS} attempts. The original cookie-store file was left unchanged.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function atomicWrite(path: string, contents: string): void {
|
|
158
|
+
const dir = dirname(path);
|
|
159
|
+
mkdirSync(dir, { recursive: true });
|
|
160
|
+
const temporary = join(dir, `.${basename(path)}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`);
|
|
161
|
+
try {
|
|
162
|
+
writeFileSync(temporary, contents, { mode: 0o600 });
|
|
163
|
+
renameSync(temporary, path);
|
|
164
|
+
} finally {
|
|
165
|
+
try {
|
|
166
|
+
unlinkSync(temporary);
|
|
167
|
+
} catch {
|
|
168
|
+
// A successful rename already removed the temporary path. A failed
|
|
169
|
+
// cleanup must not hide the original write error.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function withStoreLock<T>(path: string, operation: () => T): T {
|
|
175
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
176
|
+
const lockPath = `${path}.lock`;
|
|
177
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
178
|
+
let lockFd: number | undefined;
|
|
179
|
+
|
|
180
|
+
while (lockFd === undefined) {
|
|
181
|
+
try {
|
|
182
|
+
lockFd = openSync(lockPath, "wx", 0o600);
|
|
183
|
+
try {
|
|
184
|
+
writeFileSync(
|
|
185
|
+
lockFd,
|
|
186
|
+
`${JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() })}\n`,
|
|
187
|
+
);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
closeSync(lockFd);
|
|
190
|
+
lockFd = undefined;
|
|
191
|
+
try {
|
|
192
|
+
unlinkSync(lockPath);
|
|
193
|
+
} catch {
|
|
194
|
+
// Preserve the lock-write error.
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (!isErrorCode(error, "EEXIST")) {
|
|
200
|
+
if (lockFd !== undefined) closeSync(lockFd);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
if (Date.now() >= deadline) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Cookie-store update timed out waiting for lock "${lockPath}" after ${LOCK_WAIT_MS}ms. ` +
|
|
206
|
+
"The cookie-store file was left unchanged.",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
sleepSync(LOCK_POLL_MS);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
closeSync(lockFd);
|
|
214
|
+
try {
|
|
215
|
+
return operation();
|
|
216
|
+
} finally {
|
|
217
|
+
try {
|
|
218
|
+
unlinkSync(lockPath);
|
|
219
|
+
} catch {
|
|
220
|
+
// The update has already committed. Do not replace its result with a
|
|
221
|
+
// cleanup error; a later writer will report the exact lock path.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
61
226
|
export class CookieJar {
|
|
62
227
|
readonly path: string;
|
|
63
228
|
private readonly source: string;
|
|
@@ -72,11 +237,14 @@ export class CookieJar {
|
|
|
72
237
|
}
|
|
73
238
|
|
|
74
239
|
load(): CookieStore {
|
|
75
|
-
|
|
76
|
-
|
|
240
|
+
const raw = readStableFile(this.path);
|
|
241
|
+
if (raw === undefined) return { cookies: [], origins: [] };
|
|
242
|
+
let parsed: CookieStore;
|
|
243
|
+
try {
|
|
244
|
+
parsed = JSON.parse(raw) as CookieStore;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw new CookieStoreParseError(this.path, error);
|
|
77
247
|
}
|
|
78
|
-
const raw = readFileSync(this.path, "utf-8");
|
|
79
|
-
const parsed = JSON.parse(raw);
|
|
80
248
|
return {
|
|
81
249
|
cookies: parsed.cookies ?? [],
|
|
82
250
|
origins: parsed.origins ?? [],
|
|
@@ -90,13 +258,29 @@ export class CookieJar {
|
|
|
90
258
|
* (constructor-provided source). Creates the parent directory if needed.
|
|
91
259
|
*/
|
|
92
260
|
save(store: CookieStore): void {
|
|
93
|
-
|
|
261
|
+
withStoreLock(this.path, () => this.saveUnlocked(store));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private saveUnlocked(store: CookieStore): CookieStore {
|
|
265
|
+
const stamped = this.stamp(store);
|
|
266
|
+
atomicWrite(this.path, `${JSON.stringify(stamped, null, 2)}\n`);
|
|
267
|
+
return stamped;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private stamp(store: CookieStore): CookieStore {
|
|
271
|
+
return {
|
|
94
272
|
...store,
|
|
95
273
|
exportedAt: new Date().toISOString(),
|
|
96
274
|
exportedFrom: this.source,
|
|
97
275
|
};
|
|
98
|
-
|
|
99
|
-
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private update<T>(operation: (store: CookieStore) => { store: CookieStore; result: T }): T {
|
|
279
|
+
return withStoreLock(this.path, () => {
|
|
280
|
+
const updated = operation(this.load());
|
|
281
|
+
this.saveUnlocked(updated.store);
|
|
282
|
+
return updated.result;
|
|
283
|
+
});
|
|
100
284
|
}
|
|
101
285
|
|
|
102
286
|
/**
|
|
@@ -118,10 +302,18 @@ export class CookieJar {
|
|
|
118
302
|
* Returns the saved store.
|
|
119
303
|
*/
|
|
120
304
|
set(cookie: Cookie): CookieStore {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
305
|
+
return this.merge([cookie]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Merge cookies and origins in one interprocess-coordinated update. */
|
|
309
|
+
merge(cookies: Cookie[], origins: OriginEntry[] = []): CookieStore {
|
|
310
|
+
return this.update((store) => {
|
|
311
|
+
const merged = mergeCookies(store, cookies);
|
|
312
|
+
const byOrigin = new Map(store.origins.map((origin) => [origin.origin, origin]));
|
|
313
|
+
for (const origin of origins) byOrigin.set(origin.origin, origin);
|
|
314
|
+
merged.origins = [...byOrigin.values()];
|
|
315
|
+
return { store: merged, result: merged };
|
|
316
|
+
});
|
|
125
317
|
}
|
|
126
318
|
|
|
127
319
|
/**
|
|
@@ -133,24 +325,27 @@ export class CookieJar {
|
|
|
133
325
|
if (!opts.domain && !opts.all) {
|
|
134
326
|
throw new Error("Specify { domain } or { all: true }");
|
|
135
327
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
328
|
+
return this.update((store) => {
|
|
329
|
+
const before = store.cookies.length;
|
|
330
|
+
if (opts.all) {
|
|
331
|
+
store.cookies = [];
|
|
332
|
+
store.origins = [];
|
|
333
|
+
} else if (opts.domain) {
|
|
334
|
+
const dom = opts.domain;
|
|
335
|
+
store.cookies = store.cookies.filter((c) => !domainMatches(c.domain, dom));
|
|
336
|
+
store.origins = store.origins.filter((o) => {
|
|
337
|
+
try {
|
|
338
|
+
return !domainMatches(dom, new URL(o.origin).hostname);
|
|
339
|
+
} catch {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
store,
|
|
346
|
+
result: { before, after: store.cookies.length },
|
|
347
|
+
};
|
|
348
|
+
});
|
|
154
349
|
}
|
|
155
350
|
|
|
156
351
|
/**
|
|
@@ -188,15 +383,7 @@ export class CookieJar {
|
|
|
188
383
|
const store: CookieStore = { cookies, origins: incoming.origins ?? [] };
|
|
189
384
|
this.save(store);
|
|
190
385
|
} else {
|
|
191
|
-
|
|
192
|
-
const merged = mergeCookies(store, cookies);
|
|
193
|
-
if (incoming.origins) {
|
|
194
|
-
const seen = new Set(store.origins.map((o) => o.origin));
|
|
195
|
-
for (const o of incoming.origins) {
|
|
196
|
-
if (!seen.has(o.origin)) merged.origins.push(o);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
this.save(merged);
|
|
386
|
+
this.merge(cookies, incoming.origins ?? []);
|
|
200
387
|
}
|
|
201
388
|
return { count: cookies.length };
|
|
202
389
|
}
|
|
@@ -209,8 +396,7 @@ export class CookieJar {
|
|
|
209
396
|
exportedAt: new Date().toISOString(),
|
|
210
397
|
exportedFrom: `${this.source}-export`,
|
|
211
398
|
};
|
|
212
|
-
|
|
213
|
-
writeFileSync(filePath, `${JSON.stringify(stamped, null, 2)}\n`);
|
|
399
|
+
atomicWrite(filePath, `${JSON.stringify(stamped, null, 2)}\n`);
|
|
214
400
|
return { count: stamped.cookies.length };
|
|
215
401
|
}
|
|
216
402
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Cookie, CookieJar } from "./client.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Host callback that can mint or refresh cookies for one request URL.
|
|
5
|
+
* Must stay synchronous: `fetch` and `browse` attach cookies before they
|
|
6
|
+
* leave this process, the same constraint as `extraHeaders`.
|
|
7
|
+
*
|
|
8
|
+
* Return cookies to add; do not mutate the jar. The caller persists them
|
|
9
|
+
* with `jar.set()` so `--no-cookies` (no jar) skips the callback entirely.
|
|
10
|
+
*/
|
|
11
|
+
export type ExtraCookies = (url: string, jar: CookieJar) => Cookie[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run a host `extraCookies` callback and persist whatever it returns.
|
|
15
|
+
* No-op when the callback is omitted. Order matters: this must run
|
|
16
|
+
* before `jar.header()` or Playwright `addCookies`, because an already
|
|
17
|
+
* populated `Cookie` header cannot pick up extra cookies later.
|
|
18
|
+
*/
|
|
19
|
+
export function applyExtraCookies(
|
|
20
|
+
url: string,
|
|
21
|
+
jar: CookieJar,
|
|
22
|
+
extraCookies?: ExtraCookies,
|
|
23
|
+
): Cookie[] {
|
|
24
|
+
if (!extraCookies) return [];
|
|
25
|
+
const cookies = extraCookies(url, jar);
|
|
26
|
+
for (const cookie of cookies) jar.set(cookie);
|
|
27
|
+
return cookies;
|
|
28
|
+
}
|
package/src/lib/cookies/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export {
|
|
|
3
3
|
CookieJar,
|
|
4
4
|
type CookieJarOptions,
|
|
5
5
|
type CookieStore,
|
|
6
|
+
CookieStoreParseError,
|
|
6
7
|
domainMatches,
|
|
7
8
|
type InfoResult,
|
|
8
9
|
isExpired,
|
|
@@ -11,3 +12,4 @@ export {
|
|
|
11
12
|
type OriginEntry,
|
|
12
13
|
pathMatches,
|
|
13
14
|
} from "./client.js";
|
|
15
|
+
export { applyExtraCookies, type ExtraCookies } from "./extra.js";
|