Migrates the Claude, Grok, LMArena, Notion and Perplexity web-cookie transports from the tls-client-node/Koffi sidecar to the exactly pinned wreq-js 3.2.0 runtime, keeping the per-provider browser/OS profiles, making request cookies ephemeral, bounding and generation-protecting the shared native transport pool, removing the legacy downloader and repair path, and carrying the native binding and license evidence through the npm, standalone, Electron, Docker and Bun packaging surfaces.
This is the consolidation of the two competing migrations, and the consolidation was decided by evidence rather than by preference. #11753's six suites were installed over this implementation and run as an independent specification: 31 of 36 passed. All five failures are artefacts of #11753 being the older design, not coverage gaps —
- two hardcode the 3.0.0 pin in their assertions (this branch pins 3.2.0, which is what the release tip already resolves; #11753's 3.0.0 would have conflicted);
- one reads open-sse/services/chatgptTlsClient.ts, deleted when #11754 retired ChatGPT Web, so the test is stale against the current tip;
- two import WREQ_JS_NATIVE_BINARY_NAMES / resolveWreqJsNativeBinaryName, which this branch redesigned into WREQ_JS_NATIVE_BINDINGS / resolveWreqJsNativeBinding plus WREQ_JS_VERSION — a rename from modelling natives as file names to modelling them as package bindings, verified as an API difference rather than a lost capability (the linux-x64-gnu .node is present and serviceable).
This branch is also the strict superset by scope: 7 files exclusive to it, including the wreq-js Rust license inventory and notices, .trivyignore, open-sse/utils/tlsClient.ts and assembleStandalone.mjs. #11753 had one exclusive file, its changelog fragment. Nothing needed porting, so #11753 is superseded rather than merged, and the changelog entry credits both.
Reconciled on merge: clean against the tip. The new migration suite (tests/unit/tls-client-wreq-migration.test.ts, 1374 lines, 31 cases) is frozen at its exact LOC with the rationale — it shares one native-transport harness, so splitting it mid-merge would duplicate that harness for no coverage gain. Verified that no existing cap moves.
Verified: 182/182 across the eight TLS, native-manifest, postinstall, standalone-bundle, pack-artifact and provider-validation suites, typecheck:core clean, check:cycles OK, check-changelog-integrity OK, check-file-size OK, and every changed TypeScript file parses.
Node/undici fetch is rejected by Notion's edge with HTTP 200
temporarily-unavailable and empty assistant text (messages appear in the
thread, UI shows 502 No response from Notion AI). The same cookie and body
succeed via curl/Schannel and a browser Chrome JA3 handshake.
Route inference through tls-client-node (chrome_146), matching Claude/
Perplexity web providers. Also detect nested patch-start error objects so
operators see temporarily-unavailable instead of a misleading empty-body
502, and treat that subtype as retryable.
Verified live: notion-web/fable-5, hyperagent/fable, and
promptql/vertex-claude-fable-5 all return PONG through the packaged
backend; unit tests 83/83.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(notion-web): reuse threadId across OpenAI multi-turn (no new chat each request)
Root cause: every execute() minted a random threadId with createThread:true,
so each OpenAI messages[] turn became a brand-new Notion AI chat. That broke
multi-turn agent flows (tool result follow-ups looked like cold starts).
- History-keyed in-memory session cache (spaceId + conversation prefix hash)
- First user turn: createThread true + new UUID
- Follow-up with prior turns: createThread false + same threadId
- Optional client continuity: body.notion_thread_id / X-Notion-Thread-Id
- Echo thread id on chat.completion (notion_thread_id + response header)
- Also accept OpenAI content-parts arrays for message content
- Unit tests: 34/34 (session lookup/store + createThread false on turn 2)
* fix(notion-web): read X-Notion-Thread-Id from clientHeaders
ExecuteInput exposes client request headers as clientHeaders, not headers.
input.headers was always undefined so client-supplied thread pins were ignored.
* fix(notion-web): prefer clientHeaders with defensive headers fallback
* fix(notion-web): sticky threads on errors + partial follow-ups
- Bind conversation root (first user) to a threadId *before* upstream call so
temporarily-unavailable / empty replies never mint a new Notion chat on retry
- Persist sticky map under DATA_DIR so multi-turn survives process restarts
- Follow-ups use createThread:false, isPartialTranscript:true, and only the
steps after the last assistant (full re-transcript was overloading Notion)
- Detect in-band Notion error objects (subType temporarily-unavailable) and
retry once with the same threadId
- Keep custom-agent workflowId support and clientHeaders thread pin
* refactor(notion-web): split thread-session/stream-parser/transcript-builder into services
The merged notion-web.ts (1490 lines) and its test file (1000 lines) tripped
the file-size gate (cap 800 for new/uncapped files). Extract three
self-contained pieces into open-sse/services/, no behavior change:
- notionThreadSessions.ts: sticky thread-session cache, disk persistence,
conversation hashing, client thread-id pin (body/header)
- notionStreamParser.ts: NDJSON runInferenceTranscript response parsing +
in-band upstream error detection
- notionTranscriptBuilder.ts: config/context/message-step transcript building
Split the corresponding "Notion thread session continuity" describe block
into tests/unit/executor-notion-web-thread-sessions.test.ts. All symbols
previously reachable via the notion-web.ts namespace import stay reachable
(re-exported) so existing test destructuring is unaffected. 44/44 tests pass.
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Artur <artur@local>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
Agent clients often send message.content as [{type:\"text\",text:\"...\"}]
instead of a plain string. buildNotionMessageStep previously required a
string and silently dropped those turns, so system injects (jailbreak /
agentic conversion) and multimodal user messages never reached Notion.
Normalize string | content-parts | bare string parts via
extractNotionMessageText, and add regression coverage in the transcript
unit suite.
* fix(notion-web): add browser fingerprint headers to reduce Cloudflare challenges
Adds sec-ch-ua, sec-fetch-*, cache-control, pragma, and priority headers
that real Chromium browsers send. Without these, Cloudflare may challenge
or block requests that look like non-browser clients.
Applied to:
- buildNotionExecuteHeaders (inference requests)
- buildNotionBrowserHeaders (workspace discovery)
- buildNotionModelsDiscoveryHeaders (model discovery)
Headers match the real browser capture from Chrome 149 on Linux.
Addresses gemini-code-assist review:
- Fixed platform mismatch: sec-ch-ua-platform now matches USER_AGENT (Windows)
- Deduplicated headers via shared BROWSER_HEADERS constant in notionWebModels.ts
- Both executor and model discovery use the same constant
* fix(notion-web): align Chrome version to 149 and add browser header tests
Addresses maintainer review feedback on #7864:
- Align User-Agent and NOTION_USER_AGENT to Chrome/149 (was 145 and 150)
matching sec-ch-ua already declaring v="149"
- Add test assertions that browser fingerprint headers (sec-ch-ua,
sec-fetch-mode, cache-control, pragma) are sent on both executor
and models-discovery requests
* refactor(providers): extract notion-web fallback catalog to its own module
notionWebModels.ts crossed the 800-line new-file cap (875) once the browser
header tests landed; move the NOTION_WEB_FALLBACK_MODELS catalog + its type to
notionWebFallbackModels.ts (pure data, re-exported for existing consumers).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(notion-web): use real picker labels as primary model ids
Catalog /v1/models now surfaces web-picker names (fable-5, gpt-5.6-sol)
instead of Notion food codenames (acai-budino-high, orange-mousse).
Food codenames stay internal via notionCodename + resolveNotionCodename
for runInferenceTranscript. Legacy codename requests still work; responses
echo the client-facing id.
Also points discovery/inference at app.notion.com (same host as the AI picker).
Follow-up to #7696.
* fix(notion-web): explain plan-locked models like Fable 5
Notion returns Fable 5 (acai-budino-high) with isDisabled=true and
disabledReason=business_or_enterprise_plan_required. Keep it out of the
enabled catalog (requests would fail) but surface a discovery warning so
operators know why it is missing. Also warn when space_id is resolved via
getSpaces instead of the cookie.
* feat(notion-web): auto-detect workspace without pasting space_id
Operators only need the raw token_v2 value. When space_id is omitted:
- getSpaces loads all workspaces (browser-like headers + user id)
- each workspace is probed via getAvailableModels
- the richest AI catalog wins
Also softens auth hints so they no longer demand a cookie blob with =.
* fix(notion-web): pick Business workspace so Fable 5 is listed
Probe ALL workspaces instead of early-exiting on the first catalog with
>=8 models. Prefer spaces where Fable is enabled over personal spaces
where Notion returns isDisabled=business_or_enterprise_plan_required.
Cache the chosen spaceId for inference when cookie has no space_id.
* fix(notion-web): working inference + honest token estimates
- runInferenceTranscript: createThread+threadId, config/context/user
transcript, space/user headers (fixes ValidationError 400)
- Parse modern NDJSON patch/record-map; strip lang tags
- Estimate usage from text (Notion has no metering); mark estimated
- Treat all-zero usage as missing; skip USAGE_TOKEN_BUFFER on estimated
- Keep estimated flag through response sanitizer (was stripped -> flat 2000)
Verified live: fable-5/gpt-5.6-sol chat 200; usage 7 / 65 not constant 2000.
* refactor(notion-web): extract helpers to keep discoverNotionWebModels/execute under the complexity cap
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: artickc <artickc@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(providers): notion-web live models via getAvailableModels
Cookie-auth discovery against POST /api/v3/getAvailableModels (spaceId from
cookie or getSpaces) so /api/providers/{id}/models and /v1/models can surface
the real Notion AI picker catalog instead of a single stub notion-ai id.
Also injects a config transcript entry with the selected model codename on
runInferenceTranscript, seeds an offline fallback catalog, and documents that
space_id is needed for reliable discovery.
* fix(providers): address notion-web review + docs provider count
- Safe decodeURIComponent for malformed cookie values
- Use extractSpaceIdFromNotionCookie instead of case-sensitive space_id= includes
- Single trim in buildNotionTranscript
- Sync STRICT docs counts to 265 providers (README/AGENTS/CLAUDE)
* refactor(notion-web): extract helpers to keep parseNotionAvailableModels/pickFirstSpaceId under complexity cap
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: artickc <artickc@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Notion AI has no public inference API (see closed request #3272), so this
adds it as a new entry in the established web-cookie provider category
(chatgpt-web, claude-web, grok-web, ...): cookie-based auth via the
token_v2 session cookie posted to Notion's undocumented internal
POST /api/v3/runInferenceTranscript endpoint, translating its NDJSON
transcript-patch stream into OpenAI-compatible chat completions.
- NotionWebExecutor (open-sse/executors/notion-web.ts): resolves the
token_v2 cookie (+ optional space_id/notion_browser_id), builds a
Notion transcript from the chat messages, parses the NDJSON response
(cumulative-snapshot semantics, mirroring gemini-web.ts's handling of
#7163), and returns a chat.completion or pseudo-streamed SSE response.
All error paths route through makeExecutorErrorResult (sanitized).
- RegistryEntry under open-sse/config/providers/registry/notion-web/,
registered in providers/index.ts REGISTRY and executors/index.ts
(alias "nw").
- WEB_COOKIE_PROVIDERS entry (src/shared/constants/providers/web-cookie.ts)
with subscriptionRisk + webCookie risk notice, clearly labeled
"(Unofficial/Experimental)".
- Cookie-probe validator (validateNotionWebProvider) against Notion's
getSpaces endpoint, and a webSessionCredentials.ts UI entry for the
"Add session cookie" flow.
- Regenerated docs/reference/PROVIDER_REFERENCE.md and the
provider/translate-path golden snapshot (purely additive diffs); synced
the "251 providers" count across README/AGENTS/CLAUDE.md
(check:docs-counts STRICT gate).
Tests: tests/unit/executor-notion-web.test.ts (22 cases — registry
consistency, mocked-upstream request/response translation, NDJSON
snapshot parsing, cookie resolution, sanitized error paths) plus the
existing executor-web-cookie-sweep, provider-alias-uniqueness,
check-provider-consistency, web-session-credentials, and
provider-translate-path-golden suites all pass with notion-web included.