@lincoln504/pi-research 1.5.3 → 1.6.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/README.md +11 -11
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/dist/cli.mjs +204 -132
- package/dist/prompts/researcher.md +2 -0
- package/dist/thread-worker.mjs +112 -17
- package/docs/CHANGELOG.md +63 -0
- package/docs/SDK.md +72 -9
- package/package.json +2 -2
- package/src/constants.ts +30 -0
- package/src/infrastructure/browser/browser-client.ts +24 -6
- package/src/infrastructure/browser/browser-error-utils.ts +53 -0
- package/src/infrastructure/browser/browser-server.ts +9 -1
- package/src/infrastructure/browser/thread-worker-browser.ts +4 -1
- package/src/infrastructure/browser/thread-worker-messaging.ts +196 -3
- package/src/infrastructure/browser/thread-worker.mjs +112 -17
- package/src/knowledge/store-operations.ts +18 -7
- package/src/knowledge/store.ts +21 -3
- package/src/orchestration/deep-research-orchestrator.ts +37 -11
- package/src/orchestration/quick-research-orchestrator.ts +40 -2
- package/src/orchestration/research-synthesis-service.ts +6 -1
- package/src/prompts/researcher.md +2 -0
- package/src/utils/text-utils.ts +99 -0
- package/src/web-research/scraper-utils.ts +4 -36
- package/src/youtube/potoken.ts +47 -34
|
@@ -17,8 +17,8 @@ import { recordLlmUsage } from '../utils/llm-usage.ts';
|
|
|
17
17
|
import { logger } from '../logger.ts';
|
|
18
18
|
import { getConfig, type Config } from '../config.ts';
|
|
19
19
|
import { createResearcherSession } from './researcher.ts';
|
|
20
|
-
import { ensureAssistantResponse, parseCitations } from '../utils/text-utils.ts';
|
|
21
|
-
import { getMaxScrapeBatches, resolveExcludedTools } from '../constants.ts';
|
|
20
|
+
import { ensureAssistantResponse, parseCitations, isAnalysisFreeSynthesis, synthesisProseBody, stripReportPreamble } from '../utils/text-utils.ts';
|
|
21
|
+
import { getMaxScrapeBatches, resolveExcludedTools, QUICK_THIN_REPORT_NOTICE } from '../constants.ts';
|
|
22
22
|
import { resolveResearchModel, isPromptCachingActiveForModel } from '../core/llm/research-model-resolver.ts';
|
|
23
23
|
import type { ResearchObserver } from '../core/interfaces/observer-interfaces.ts';
|
|
24
24
|
import { HeadlessObserver, makeSafeObserver, type HeadlessObserverOptions } from './headless-observer.ts';
|
|
@@ -354,6 +354,44 @@ export class QuickResearchOrchestrator {
|
|
|
354
354
|
// digest out of the delivered document.
|
|
355
355
|
result = synthesisService.getReport(this.options.researchId, 'quick') ?? result;
|
|
356
356
|
|
|
357
|
+
// Drop a leading line that announces the report instead of being part of it
|
|
358
|
+
// ("I have gathered sufficient material… I will now synthesize the full
|
|
359
|
+
// report."). The prompt forbids it and the instruction is not reliably obeyed;
|
|
360
|
+
// quick mode has nothing behind it to absorb the line, so it lands as the
|
|
361
|
+
// reader's first sentence. Removed BEFORE the analysis check below, so that
|
|
362
|
+
// check measures the report rather than the narration about it.
|
|
363
|
+
const { body: withoutPreamble, preamble } = stripReportPreamble(result);
|
|
364
|
+
if (preamble) {
|
|
365
|
+
logger.debug(`[QuickOrchestrator] Dropped a ${preamble.length}-char narration preamble from the report: ${preamble.slice(0, 120)}`);
|
|
366
|
+
metrics.increment('research_report_preamble_stripped_total', 1, { mode: 'quick' });
|
|
367
|
+
result = withoutPreamble;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Same disguise as the deep path, and here it is the DELIVERABLE: a report with
|
|
371
|
+
// no analysis in it — a bare heading, a fragment — is about to have the rebuilt
|
|
372
|
+
// source list appended below, which pads it to kilobytes and makes a failed run
|
|
373
|
+
// read as a complete one. Quick mode has no second layer of researcher reports to
|
|
374
|
+
// fall back on, so the honest response is to say so in the report itself rather
|
|
375
|
+
// than hand back a heading over a link dump and call it a success.
|
|
376
|
+
//
|
|
377
|
+
// The VOLUME signal is deliberately disabled here (0). The 800-character floor was
|
|
378
|
+
// calibrated on deep reports; quick mode answers a single question and a correct
|
|
379
|
+
// reply can legitimately be one sentence, and there is no measured distribution of
|
|
380
|
+
// quick-report lengths to justify any floor. Since the only available response is
|
|
381
|
+
// a warning, a false positive would tell a user their correct answer is untrustworthy
|
|
382
|
+
// — strictly worse than missing a thin one. The structural signal is length-independent
|
|
383
|
+
// and carries no such risk: "Paris is the capital of France." ends a sentence and
|
|
384
|
+
// passes, a bare title does not and is flagged.
|
|
385
|
+
//
|
|
386
|
+
// Added to the body BEFORE ensureCitedLinks so the links land beneath it — the
|
|
387
|
+
// same ordering constraint the weak-grounding notice observes, because
|
|
388
|
+
// parseCitations absorbs any text trailing the last entry into its description.
|
|
389
|
+
if (isAnalysisFreeSynthesis(result, 0)) {
|
|
390
|
+
logger.warn(`[QuickOrchestrator] Report contains no complete sentence (${synthesisProseBody(result).length} char(s) of prose) — flagging it in the report`);
|
|
391
|
+
metrics.increment('research_synthesis_analysis_free_total', 1, { mode: 'quick' });
|
|
392
|
+
result = `${result.trim()}\n\n${QUICK_THIN_REPORT_NOTICE}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
357
395
|
// Ensure CITED LINKS section is accurate and consistent
|
|
358
396
|
result = synthesisService.ensureCitedLinks(this.options.researchId, result);
|
|
359
397
|
// Snapshot BEFORE steering guidance is appended: parseCitations absorbs
|
|
@@ -277,7 +277,12 @@ export class ResearchSynthesisService implements IService {
|
|
|
277
277
|
if (reportCount === 0) {
|
|
278
278
|
synthesis += 'No researcher reports were generated before the process stopped.';
|
|
279
279
|
} else {
|
|
280
|
-
|
|
280
|
+
// Neutral about WHY the synthesis step produced nothing usable, because there are
|
|
281
|
+
// now two callers: a run interrupted mid-flight, and one whose synthesis LLM returned
|
|
282
|
+
// a report with no analysis in it. Asserting an interruption would be inaccurate for
|
|
283
|
+
// the second, and telling a user the run was interrupted when it was not is exactly
|
|
284
|
+
// the kind of misleading output this fallback exists to avoid.
|
|
285
|
+
synthesis += `This is an automated compilation of ${reportCount} individual researcher report(s). The synthesis step did not produce a usable summary for this run, so the findings are presented as collected.\n\n`;
|
|
281
286
|
synthesis += Array.from(reports.entries())
|
|
282
287
|
.map(([id, report]) => `Researcher ${id}\n\n${report}`)
|
|
283
288
|
.join('\n\n');
|
|
@@ -62,6 +62,8 @@ Write your report immediately after scraping is complete, or as soon as a tool r
|
|
|
62
62
|
## Report Format
|
|
63
63
|
|
|
64
64
|
{{digest_section}}
|
|
65
|
+
Emit the report and nothing else. No preamble, and no narration of what you are about to do — not "I have gathered extensive material…", not "Let me now synthesize my findings…". Begin at the report's own first line.
|
|
66
|
+
|
|
65
67
|
Use [N] inline citations throughout. Write in plain prose — no markdown headings, no bullet points, no bold or italic text. Use clear paragraphs separated by blank lines. The full CITED LINKS list goes at the very end.
|
|
66
68
|
|
|
67
69
|
[Topic Title]
|
package/src/utils/text-utils.ts
CHANGED
|
@@ -280,6 +280,105 @@ export function lastCitedLinksHeader(text: string): { lineStart: number; textSta
|
|
|
280
280
|
return { lineStart, textStart };
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* A first-person opener that announces the report instead of being part of it:
|
|
285
|
+
* "I have gathered extensive material… Let me now synthesize my findings into a
|
|
286
|
+
* comprehensive report."
|
|
287
|
+
*
|
|
288
|
+
* Deliberately narrow. It has to match a line that is BOTH first-person AND about the
|
|
289
|
+
* act of reporting, because the only available response is to delete the line, and the
|
|
290
|
+
* cost of deleting a real one is losing content nothing else can recover.
|
|
291
|
+
*/
|
|
292
|
+
const REPORT_NARRATION_OPENER = /^(?:I|I'll|I'm|Let me|Now I|Next I|First,? I)\b/i;
|
|
293
|
+
const REPORT_NARRATION_SUBJECT =
|
|
294
|
+
/\b(?:report|synthesi[sz]\w*|findings|material|sources|scraped|gathered|research\w*|summar\w+)\b/i;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Remove a leading line that narrates the report rather than being part of it.
|
|
298
|
+
*
|
|
299
|
+
* The researcher prompt forbids this, and the instruction is not reliably obeyed: across
|
|
300
|
+
* live quick runs the delivered document repeatedly opened with "I have gathered
|
|
301
|
+
* sufficient material… I will now synthesize the full report." Quick mode has no
|
|
302
|
+
* synthesis step behind it to absorb that, so it reaches the reader as the report's
|
|
303
|
+
* first line.
|
|
304
|
+
*
|
|
305
|
+
* Every condition here narrows a DESTRUCTIVE action, so each one is required rather than
|
|
306
|
+
* scored: the segment must be a single line, must be separated from the report by a
|
|
307
|
+
* blank line, must read as first-person narration ABOUT reporting, and must leave
|
|
308
|
+
* something behind. A report that opens correctly — with a topic title, as the format
|
|
309
|
+
* requires — matches none of them.
|
|
310
|
+
*
|
|
311
|
+
* @returns the body with the preamble removed, and the preamble itself (empty when
|
|
312
|
+
* nothing was stripped) so callers can log and count what they dropped.
|
|
313
|
+
*/
|
|
314
|
+
export function stripReportPreamble(text: string): { body: string; preamble: string } {
|
|
315
|
+
const trimmed = text.trimStart();
|
|
316
|
+
const breakAt = trimmed.indexOf('\n\n');
|
|
317
|
+
if (breakAt < 0) return { body: text, preamble: '' };
|
|
318
|
+
|
|
319
|
+
const head = trimmed.slice(0, breakAt).trim();
|
|
320
|
+
const rest = trimmed.slice(breakAt + 2).trimStart();
|
|
321
|
+
if (head.includes('\n')) return { body: text, preamble: '' };
|
|
322
|
+
// Something has to be left. There is deliberately no LENGTH floor beyond that: a
|
|
323
|
+
// short remainder is the answer ("Paris is the capital of France."), and keeping the
|
|
324
|
+
// narration to pad it out would both deliver the wrong text and let that narration's
|
|
325
|
+
// full stop satisfy the analysis check that runs on the result.
|
|
326
|
+
if (!rest) return { body: text, preamble: '' };
|
|
327
|
+
if (!REPORT_NARRATION_OPENER.test(head)) return { body: text, preamble: '' };
|
|
328
|
+
if (!REPORT_NARRATION_SUBJECT.test(head)) return { body: text, preamble: '' };
|
|
329
|
+
|
|
330
|
+
return { body: rest, preamble: head };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The prose body of a synthesis: everything before its CITED LINKS section, trimmed.
|
|
335
|
+
* Returns the whole text when there is no such section.
|
|
336
|
+
*
|
|
337
|
+
* Exists to measure how much the model ACTUALLY wrote. Length alone cannot do that,
|
|
338
|
+
* because the engine rebuilds and appends the citation list itself — a report whose
|
|
339
|
+
* body is one title line still comes out kilobytes long once that block lands, which
|
|
340
|
+
* is precisely how a synthesis containing no analysis reached users looking complete.
|
|
341
|
+
*/
|
|
342
|
+
export function synthesisProseBody(text: string): string {
|
|
343
|
+
const { lineStart } = lastCitedLinksHeader(text);
|
|
344
|
+
return (lineStart < 0 ? text : text.slice(0, lineStart)).trim();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Whether a synthesis carries no actual analysis — a heading with nothing under it,
|
|
349
|
+
* a stray sentence fragment, or an empty string.
|
|
350
|
+
*
|
|
351
|
+
* Two INDEPENDENT signals, either of which is enough, so the check does not rest on a
|
|
352
|
+
* single tuned number:
|
|
353
|
+
*
|
|
354
|
+
* - Volume. Less prose than a real report could possibly contain. Pass 0 to DISABLE
|
|
355
|
+
* this signal, which callers must do wherever no length data justifies a floor:
|
|
356
|
+
* a wrongly-flagged report is worse than a missed one when the only available
|
|
357
|
+
* response is to warn the reader about output that is actually fine.
|
|
358
|
+
* - Structure. No sentence ever ends. Every observed failure was a bare title line,
|
|
359
|
+
* which has no terminator at all; any genuine report has many. This one needs no
|
|
360
|
+
* threshold and would catch a title of any length.
|
|
361
|
+
*
|
|
362
|
+
* Measured on the prose only — the text before CITED LINKS — because the engine
|
|
363
|
+
* rebuilds and appends the source list itself, which pads a one-line failure out to
|
|
364
|
+
* kilobytes and is exactly how such a report reached users looking complete.
|
|
365
|
+
*/
|
|
366
|
+
export function isAnalysisFreeSynthesis(text: string, minProseChars: number): boolean {
|
|
367
|
+
const prose = synthesisProseBody(text);
|
|
368
|
+
// Drop markdown heading markers and blockquote/bold decoration before judging, so a
|
|
369
|
+
// model that writes "## Title" is measured the same as one that writes "Title".
|
|
370
|
+
const stripped = prose.replace(/^[ \t]*(?:#{1,6}[ \t]*|>[ \t]*|\*\*)/gm, '').trim();
|
|
371
|
+
if (minProseChars > 0 && stripped.length < minProseChars) return true;
|
|
372
|
+
// A report with no sentence-ending punctuation anywhere is a heading, a fragment or a
|
|
373
|
+
// list of bare labels — never analysis. The terminator set must not assume Latin
|
|
374
|
+
// punctuation: a report written in a script that ends sentences with a danda, an
|
|
375
|
+
// Arabic full stop or a CJK full stop is a correct report, and this signal is the only
|
|
376
|
+
// one quick mode runs, where a false positive tells the reader a good answer is
|
|
377
|
+
// untrustworthy.
|
|
378
|
+
if (!/[.!?。!?।॥۔؟።]/.test(stripped)) return true;
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
|
|
283
382
|
/**
|
|
284
383
|
* Parses the CITED LINKS section from a researcher report, extracting URLs, sources, and their descriptions.
|
|
285
384
|
* Handles both single-line 'URL - description' and multi-line 'Source:'/'Description:' formats.
|
|
@@ -19,45 +19,13 @@ import {
|
|
|
19
19
|
INTERNAL_NETWORK_PATTERNS,
|
|
20
20
|
type NativeHtmlToMarkdownModule,
|
|
21
21
|
} from './scraper-types.ts';
|
|
22
|
-
import { isCloudflareBlockError, isTaskTimeoutError, isPoolShutdownError } from '../infrastructure/browser/browser-error-utils.ts';
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* races. None of these is a pi-research bug: the URL is already accounted for as
|
|
29
|
-
* "not scraped" (scrape_results_total{outcome=total_failure}) and surfaced to the
|
|
30
|
-
* user that way, and no operator action follows. Callers use this to log such
|
|
31
|
-
* failures at debug instead of error — keeping the ERROR log (and, via
|
|
32
|
-
* logger.error's error-tracker re-track, the diagnostic error count) focused on
|
|
33
|
-
* genuine faults. The messages matched here are produced by validateContent
|
|
34
|
-
* (`Fetch blocked:` / `Fetch returned stub:`), scrapeWithFetch (`HTTP <code>`)
|
|
35
|
-
* and extractPdfToMarkdown (`Could not extract content from PDF`, `PDF too
|
|
36
|
-
* large`) — a malformed/encrypted/scanned-image-only PDF, or one that simply
|
|
37
|
-
* exceeds the deliberate 100MB policy cap, is routine on the open web and
|
|
38
|
-
* just as non-actionable as a stub page or a 404; the 25MB HTML policy caps
|
|
39
|
-
* (`HTML response too large` from the fetch layer, `Browser HTML too large`
|
|
40
|
-
* from the browser layer) are the same deliberate-cap class; the remaining
|
|
41
|
-
* cases delegate to the shared browser-layer predicates. Deliberately does NOT
|
|
42
|
-
* match 'PDF extraction unavailable' (a pdf-oxide-wasm native-module load
|
|
43
|
-
* failure) — that is an infrastructure fault affecting every URL, not a
|
|
44
|
-
* routine per-PDF outcome, and must still surface at ERROR.
|
|
24
|
+
* Re-exported from the browser error utilities, where it lives beside the three
|
|
25
|
+
* predicates it delegates to. Kept exported here because this is where every scrape
|
|
26
|
+
* caller already imports its error helpers from.
|
|
45
27
|
*/
|
|
46
|
-
export
|
|
47
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
48
|
-
return (
|
|
49
|
-
msg.startsWith('Fetch blocked:') || // bot-protection interstitial (Cloudflare, DDoS-Guard, …)
|
|
50
|
-
msg.startsWith('Fetch returned stub:') || // nav-only / near-empty page
|
|
51
|
-
msg.startsWith('Could not extract content from PDF') || // malformed/encrypted/scanned-only PDF
|
|
52
|
-
msg.startsWith('PDF too large') || // exceeds the deliberate 100MB policy cap
|
|
53
|
-
msg.startsWith('HTML response too large') || // fetch-layer 25MB policy cap — same class as the PDF cap
|
|
54
|
-
msg.startsWith('Browser HTML too large') || // browser-layer 25MB policy cap
|
|
55
|
-
/^HTTP \d{3}\b/.test(msg) || // remote 4xx/5xx status
|
|
56
|
-
isCloudflareBlockError(error) ||
|
|
57
|
-
isTaskTimeoutError(error) ||
|
|
58
|
-
isPoolShutdownError(error)
|
|
59
|
-
);
|
|
60
|
-
}
|
|
28
|
+
export { isBenignScrapeFailure } from '../infrastructure/browser/browser-error-utils.ts';
|
|
61
29
|
|
|
62
30
|
/**
|
|
63
31
|
* Get a random user agent
|
package/src/youtube/potoken.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* HTTP 200 with an EMPTY body (a silent failure). This module mints tokens with
|
|
7
7
|
* `bgutils-js`, running the BotGuard VM under a `jsdom` DOM.
|
|
8
8
|
*
|
|
9
|
-
* Hard-won implementation details (validated live against youtubei.js@
|
|
10
|
-
* bgutils-js@
|
|
9
|
+
* Hard-won implementation details (re-validated live against youtubei.js@18 /
|
|
10
|
+
* bgutils-js@4 / jsdom@29):
|
|
11
11
|
*
|
|
12
12
|
* 1. Cross-realm typed arrays. The BotGuard VM must run in the SAME JS realm as
|
|
13
13
|
* bgutils' `base64ToU8` (the Node realm). So the interpreter is eval'd via
|
|
@@ -171,9 +171,17 @@ async function doMint(
|
|
|
171
171
|
const started = Date.now();
|
|
172
172
|
|
|
173
173
|
// Lazy-load heavy deps — never imported at module load (keeps startup native-free).
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
// bgutils-js 4 dropped the barrel entry (package.json "exports" has no "."), so each
|
|
175
|
+
// piece comes from its own subpath: the challenge fetcher and VM client from
|
|
176
|
+
// /botguard, the minter from /webpo, the attestation URL/header helpers from /utils.
|
|
177
|
+
const [bgGuard, bgWebPo, bgUtils] = (await Promise.all([
|
|
178
|
+
import('bgutils-js/botguard'),
|
|
179
|
+
import('bgutils-js/webpo'),
|
|
180
|
+
import('bgutils-js/utils'),
|
|
181
|
+
])) as unknown as [BgGuardModule, BgWebPoModule, BgUtilsModule];
|
|
182
|
+
const { getChallenge, BotGuardClient } = bgGuard;
|
|
183
|
+
const { WebPoMinter } = bgWebPo;
|
|
184
|
+
const { buildURL, getHeaders } = bgUtils;
|
|
177
185
|
const { JSDOM } = await import('jsdom');
|
|
178
186
|
|
|
179
187
|
const dom = new JSDOM('<!DOCTYPE html><html lang="en"><body></body></html>', {
|
|
@@ -211,18 +219,18 @@ async function doMint(
|
|
|
211
219
|
|
|
212
220
|
bridge();
|
|
213
221
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
// bgutils-js 4 replaced BG.Challenge.create(config) with a free getChallenge()
|
|
223
|
+
// whose config is narrower: no globalObj and no identifier (the content binding is
|
|
224
|
+
// supplied per-mint, which is what this module already did), and `fetch` renamed
|
|
225
|
+
// to `fetchFunction`.
|
|
226
|
+
const challenge = await getChallenge({
|
|
227
|
+
requestKey,
|
|
228
|
+
fetchFunction: ((input: unknown, init?: unknown) =>
|
|
216
229
|
fetchImpl(input as Parameters<typeof fetch>[0], {
|
|
217
230
|
...(init as RequestInit | undefined),
|
|
218
231
|
signal: (init as RequestInit | undefined)?.signal ?? opts.signal,
|
|
219
|
-
}),
|
|
220
|
-
|
|
221
|
-
identifier: identifiers[0]!,
|
|
222
|
-
requestKey,
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const challenge = await BG.Challenge.create(bgConfig);
|
|
232
|
+
})) as unknown as typeof fetch,
|
|
233
|
+
});
|
|
226
234
|
if (!challenge) throw new Error('BotGuard challenge unavailable');
|
|
227
235
|
|
|
228
236
|
const interpreter = challenge.interpreterJavascript?.privateDoNotAccessOrElseSafeScriptWrappedValue;
|
|
@@ -233,10 +241,16 @@ async function doMint(
|
|
|
233
241
|
// PoToken generation; trust rests on that TLS connection.
|
|
234
242
|
new Function(interpreter)();
|
|
235
243
|
|
|
236
|
-
|
|
244
|
+
if (!challenge.program || !challenge.globalName) {
|
|
245
|
+
throw new Error('BotGuard challenge missing program/globalName');
|
|
246
|
+
}
|
|
247
|
+
// `globalObj` was renamed `globalObject` in bgutils-js 4. Passing the old name
|
|
248
|
+
// leaves the VM without a global to attach to and the minter factory silently
|
|
249
|
+
// returns a non-function, so this rename is load-bearing, not cosmetic.
|
|
250
|
+
const botguard = await BotGuardClient.create({
|
|
237
251
|
program: challenge.program,
|
|
238
252
|
globalName: challenge.globalName,
|
|
239
|
-
|
|
253
|
+
globalObject: globalThis,
|
|
240
254
|
});
|
|
241
255
|
|
|
242
256
|
const webPoSignalOutput: unknown[] = [];
|
|
@@ -260,7 +274,7 @@ async function doMint(
|
|
|
260
274
|
throw new Error('Integrity token rejected by attestation server (likely a non-residential IP)');
|
|
261
275
|
}
|
|
262
276
|
|
|
263
|
-
const minter = await
|
|
277
|
+
const minter = await WebPoMinter.create({ integrityToken }, webPoSignalOutput);
|
|
264
278
|
|
|
265
279
|
const tokens = new Map<string, string>();
|
|
266
280
|
for (const id of identifiers) {
|
|
@@ -294,26 +308,27 @@ async function doMint(
|
|
|
294
308
|
// Minimal structural types for the loosely-typed bgutils-js surface we touch.
|
|
295
309
|
// ---------------------------------------------------------------------------
|
|
296
310
|
|
|
297
|
-
interface
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
requestKey: string;
|
|
311
|
+
interface BgChallenge {
|
|
312
|
+
interpreterJavascript?: { privateDoNotAccessOrElseSafeScriptWrappedValue?: string };
|
|
313
|
+
program?: string;
|
|
314
|
+
globalName?: string;
|
|
302
315
|
}
|
|
303
316
|
|
|
304
|
-
interface
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
};
|
|
317
|
+
interface BgGuardModule {
|
|
318
|
+
getChallenge(config: {
|
|
319
|
+
requestKey: string;
|
|
320
|
+
fetchFunction: typeof fetch;
|
|
321
|
+
interpreterHash?: string;
|
|
322
|
+
useYouTubeAPI?: boolean;
|
|
323
|
+
}): Promise<BgChallenge | undefined>;
|
|
312
324
|
BotGuardClient: {
|
|
313
|
-
create(opts: { program: string; globalName: string;
|
|
325
|
+
create(opts: { program: string; globalName: string; globalObject: unknown }): Promise<{
|
|
314
326
|
snapshot(args: { webPoSignalOutput: unknown[] }): Promise<string>;
|
|
315
327
|
}>;
|
|
316
328
|
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
interface BgWebPoModule {
|
|
317
332
|
WebPoMinter: {
|
|
318
333
|
create(
|
|
319
334
|
integrity: { integrityToken: string },
|
|
@@ -322,9 +337,7 @@ interface BgNamespace {
|
|
|
322
337
|
};
|
|
323
338
|
}
|
|
324
339
|
|
|
325
|
-
interface
|
|
326
|
-
BG?: BgNamespace;
|
|
327
|
-
default: BgNamespace;
|
|
340
|
+
interface BgUtilsModule {
|
|
328
341
|
buildURL(endpoint: string, useYouTubeAPI?: boolean): string;
|
|
329
342
|
getHeaders(): Record<string, string>;
|
|
330
343
|
}
|