pi-cursor-sdk 0.1.62 → 0.2.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/CHANGELOG.md +23 -0
- package/README.md +5 -5
- package/docs/cursor-live-smoke-checklist.md +4 -4
- package/docs/cursor-model-ux-spec.md +5 -3
- package/docs/cursor-native-tool-visual-audit.md +1 -1
- package/docs/cursor-testing-lessons.md +1 -1
- package/docs/platform-smoke.md +3 -3
- package/node_modules/fast-uri/index.js +37 -1
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/security.test.js +136 -0
- package/node_modules/hono/dist/cjs/context.js +28 -13
- package/node_modules/hono/dist/cjs/helper/proxy/index.js +4 -0
- package/node_modules/hono/dist/cjs/helper/streaming/sse.js +1 -1
- package/node_modules/hono/dist/cjs/hono-base.js +3 -2
- package/node_modules/hono/dist/cjs/jsx/base.js +27 -23
- package/node_modules/hono/dist/cjs/jsx/hooks/index.js +16 -13
- package/node_modules/hono/dist/cjs/middleware/cache/index.js +103 -8
- package/node_modules/hono/dist/cjs/middleware/compress/index.js +5 -0
- package/node_modules/hono/dist/cjs/middleware/cors/index.js +2 -2
- package/node_modules/hono/dist/cjs/middleware/etag/index.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/jwk/jwk.js +9 -4
- package/node_modules/hono/dist/cjs/middleware/jwt/jwt.js +9 -4
- package/node_modules/hono/dist/cjs/middleware/language/language.js +10 -6
- package/node_modules/hono/dist/cjs/middleware/method-not-allowed/index.js +90 -0
- package/node_modules/hono/dist/cjs/middleware/secure-headers/secure-headers.js +10 -4
- package/node_modules/hono/dist/cjs/request.js +7 -9
- package/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +55 -56
- package/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +64 -85
- package/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +13 -5
- package/node_modules/hono/dist/cjs/router.js +1 -1
- package/node_modules/hono/dist/cjs/utils/accept.js +1 -1
- package/node_modules/hono/dist/cjs/utils/cookie.js +3 -2
- package/node_modules/hono/dist/cjs/utils/url.js +8 -8
- package/node_modules/hono/dist/context.js +28 -13
- package/node_modules/hono/dist/helper/proxy/index.js +4 -0
- package/node_modules/hono/dist/helper/streaming/sse.js +1 -1
- package/node_modules/hono/dist/hono-base.js +3 -2
- package/node_modules/hono/dist/jsx/base.js +27 -23
- package/node_modules/hono/dist/jsx/hooks/index.js +16 -13
- package/node_modules/hono/dist/middleware/cache/index.js +103 -8
- package/node_modules/hono/dist/middleware/compress/index.js +5 -0
- package/node_modules/hono/dist/middleware/cors/index.js +2 -2
- package/node_modules/hono/dist/middleware/etag/index.js +1 -1
- package/node_modules/hono/dist/middleware/jwk/jwk.js +9 -4
- package/node_modules/hono/dist/middleware/jwt/jwt.js +9 -4
- package/node_modules/hono/dist/middleware/language/language.js +10 -6
- package/node_modules/hono/dist/middleware/method-not-allowed/index.js +68 -0
- package/node_modules/hono/dist/middleware/secure-headers/secure-headers.js +10 -4
- package/node_modules/hono/dist/request.js +8 -10
- package/node_modules/hono/dist/router/reg-exp-router/node.js +55 -56
- package/node_modules/hono/dist/router/reg-exp-router/router.js +64 -85
- package/node_modules/hono/dist/router/reg-exp-router/trie.js +13 -5
- package/node_modules/hono/dist/router.js +1 -1
- package/node_modules/hono/dist/types/adapter/aws-lambda/types.d.ts +9 -0
- package/node_modules/hono/dist/types/client/types.d.ts +1 -1
- package/node_modules/hono/dist/types/hono-base.d.ts +4 -3
- package/node_modules/hono/dist/types/jsx/base.d.ts +4 -2
- package/node_modules/hono/dist/types/jsx/dom/index.d.ts +5 -5
- package/node_modules/hono/dist/types/jsx/dom/intrinsic-element/components.d.ts +2 -2
- package/node_modules/hono/dist/types/jsx/dom/server.d.ts +5 -5
- package/node_modules/hono/dist/types/jsx/hooks/index.d.ts +8 -6
- package/node_modules/hono/dist/types/jsx/index.d.ts +5 -5
- package/node_modules/hono/dist/types/middleware/cache/index.d.ts +6 -4
- package/node_modules/hono/dist/types/middleware/cors/index.d.ts +1 -1
- package/node_modules/hono/dist/types/middleware/jsx-renderer/index.d.ts +2 -2
- package/node_modules/hono/dist/types/middleware/jwk/jwk.d.ts +2 -0
- package/node_modules/hono/dist/types/middleware/jwt/jwt.d.ts +2 -0
- package/node_modules/hono/dist/types/middleware/method-not-allowed/index.d.ts +49 -0
- package/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +1 -1
- package/node_modules/hono/dist/types/router/reg-exp-router/trie.d.ts +2 -1
- package/node_modules/hono/dist/types/router.d.ts +1 -1
- package/node_modules/hono/dist/types/utils/headers.d.ts +2 -2
- package/node_modules/hono/dist/types/utils/url.d.ts +1 -0
- package/node_modules/hono/dist/utils/accept.js +1 -1
- package/node_modules/hono/dist/utils/cookie.js +4 -3
- package/node_modules/hono/dist/utils/url.js +6 -7
- package/node_modules/hono/package.json +10 -3
- package/node_modules/ip-address/README.md +146 -126
- package/node_modules/ip-address/dist/common.d.ts +29 -0
- package/node_modules/ip-address/dist/common.js +44 -5
- package/node_modules/ip-address/dist/common.js.map +1 -1
- package/node_modules/ip-address/dist/ipv4.d.ts +8 -1
- package/node_modules/ip-address/dist/ipv4.js +24 -20
- package/node_modules/ip-address/dist/ipv4.js.map +1 -1
- package/node_modules/ip-address/dist/ipv6.d.ts +68 -3
- package/node_modules/ip-address/dist/ipv6.js +180 -53
- package/node_modules/ip-address/dist/ipv6.js.map +1 -1
- package/node_modules/ip-address/dist/v4/constants.js +5 -1
- package/node_modules/ip-address/dist/v4/constants.js.map +1 -1
- package/node_modules/ip-address/dist/v6/constants.js +3 -2
- package/node_modules/ip-address/dist/v6/constants.js.map +1 -1
- package/node_modules/ip-address/package.json +6 -4
- package/package.json +5 -5
- package/scripts/platform-smoke/live-suite-runner.mjs +15 -39
- package/scripts/platform-smoke/platform-build-windows.ps1 +1 -1
- package/scripts/platform-smoke/scenarios.mjs +1 -1
- package/scripts/platform-smoke.mjs +5 -5
- package/scripts/refresh-cursor-model-snapshots.mjs +16 -7
- package/shared/cursor-model-selection-identities.d.mts +21 -0
- package/shared/cursor-model-selection-identities.mjs +95 -0
- package/src/bundled-context-windows.ts +18 -12
- package/src/context.ts +1 -1
- package/src/cursor-context-tools.ts +1 -1
- package/src/cursor-display-only-trace.ts +1 -1
- package/src/cursor-fallback-models.generated.ts +875 -361
- package/src/cursor-live-run-accounting.ts +1 -1
- package/src/cursor-live-run-coordinator.ts +1 -1
- package/src/cursor-native-replay-routing.ts +1 -1
- package/src/cursor-partial-content-emitter.ts +1 -1
- package/src/cursor-pi-tool-bridge-mcp.ts +1 -1
- package/src/cursor-pi-tool-bridge-run.ts +1 -1
- package/src/cursor-pi-tool-bridge-types.ts +1 -1
- package/src/cursor-provider-lazy.ts +1 -1
- package/src/cursor-provider-live-run-drain.ts +1 -1
- package/src/cursor-provider-overflow.ts +1 -1
- package/src/cursor-provider-run-finalizer.ts +1 -1
- package/src/cursor-provider-turn-coordinator.ts +1 -1
- package/src/cursor-provider-turn-lifecycle-emitter.ts +1 -1
- package/src/cursor-provider-turn-prepare.ts +1 -1
- package/src/cursor-provider-turn-types.ts +1 -1
- package/src/cursor-provider.ts +1 -1
- package/src/cursor-run-final-text.ts +1 -1
- package/src/cursor-sdk-event-debug.ts +1 -1
- package/src/cursor-session-agent.ts +1 -1
- package/src/cursor-session-send-policy.ts +1 -1
- package/src/cursor-usage-accounting.ts +1 -1
- package/src/model-discovery.ts +32 -89
- package/node_modules/cross-spawn/node_modules/which/CHANGELOG.md +0 -166
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 - 2026-08-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add a dated, hash-verified evidence bundle for Cursor's persisted system messages and reconstructed tool guidance for Grok 4.5, Claude Opus/Fable 5, and GPT-5.6 Sol/Terra/Luna.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Require Pi 0.84.0 or later, pin the local Pi validation packages to exact 0.84.0 with TypeBox 1.3.7, move pi-ai imports from the temporary compatibility entrypoint to the supported root API, and update test harness contexts for Pi's scoped-model and native-provider registration types.
|
|
12
|
+
- Bound Vitest concurrency to four workers so process and filesystem contract tests remain reliable on high-core platform-smoke hosts.
|
|
13
|
+
- Refresh the 34-model Cursor fallback catalog and checkpoint-derived context-window snapshot from the live `@cursor/sdk` 1.0.23 runtime.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- Refresh vulnerable transitive releases within their existing ranges: bundled Hono, fast-uri, and ip-address plus development-only PostCSS and protobufjs. The remaining production audit findings are confined to the pinned Cursor SDK → ConnectRPC → undici chain, which has no compatible fix.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Normalize checkpoint context-window keys to current selectable model identities, collapse redundant default `:fast`/`:slow` aliases, reject conflicting equivalent selections, remove stale or ambiguous aliases, and reuse base-model context evidence for unobserved equivalent aliases.
|
|
22
|
+
- Give Windows platform-build checks the same 15-second Vitest scheduling headroom as the full Windows test run, while preserving normal local test timeouts.
|
|
23
|
+
- Pass live PTY smoke prompts as direct Node argv through Pi's interactive initial-message contract, isolate unrelated startup probes with `PI_OFFLINE=1`, and keep final markers out of prompt echoes.
|
|
24
|
+
- Run required platform targets sequentially so concurrent VM/container load and Cursor API calls cannot starve otherwise healthy smoke lanes.
|
|
25
|
+
|
|
3
26
|
## 0.1.62 - 2026-07-29
|
|
4
27
|
|
|
5
28
|
### Added
|
package/README.md
CHANGED
|
@@ -51,10 +51,10 @@ If pi started without a key, run `/cursor-refresh-models` after `/login` to refr
|
|
|
51
51
|
## Requirements
|
|
52
52
|
|
|
53
53
|
- Node.js 22.19+
|
|
54
|
-
-
|
|
54
|
+
- Pi 0.84.0 or later; pi core peer metadata remains optional and uses `"*"` ranges per Pi package guidance
|
|
55
55
|
- a Cursor SDK API key saved through `/login`, available as `CURSOR_API_KEY`, or passed with pi's `--api-key`
|
|
56
56
|
|
|
57
|
-
No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.23`, so normal package installation brings in the SDK version this extension was built and tested against. Cursor SDK 1.0.23 declares its Node ConnectRPC transport dependency directly, so npm installs place `@connectrpc/connect-node` where the SDK can resolve it. The extension intentionally does not bundle `@cursor/sdk` or its platform packages, because packing from one maintainer OS can otherwise ship the wrong optional SDK binary for another OS. Cursor SDK 1.0.23 keeps the older `sqlite3 -> node-gyp@8` dependency chain out of the runtime tree, so deprecated install warnings for `inflight`, `rimraf`, `glob@7`, `npmlog`, `gauge`, `are-we-there-yet`, and `tar@6` from that chain are not expected.
|
|
57
|
+
No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.23`, so normal package installation brings in the SDK version this extension was built and tested against. Cursor SDK 1.0.23 declares its Node ConnectRPC transport dependency directly, so npm installs place `@connectrpc/connect-node` where the SDK can resolve it. The extension intentionally does not bundle `@cursor/sdk` or its platform packages, because packing from one maintainer OS can otherwise ship the wrong optional SDK binary for another OS. Cursor SDK 1.0.23 keeps the older `sqlite3 -> node-gyp@8` dependency chain out of the runtime tree, so deprecated install warnings for `inflight`, `rimraf`, `glob@7`, `npmlog`, `gauge`, `are-we-there-yet`, and `tar@6` from that chain are not expected. Older Pi and Cursor SDK compatibility paths are not maintained.
|
|
58
58
|
|
|
59
59
|
## Install
|
|
60
60
|
|
|
@@ -78,7 +78,7 @@ Use `-l` if you want the package recorded in the current project's `.pi/settings
|
|
|
78
78
|
pi install -l npm:pi-cursor-sdk
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Pi 0.
|
|
81
|
+
Pi 0.84.0 loads project-local extensions only after project trust is resolved, so this extension cannot observe that trust event. When a project-local install needs to read or write `.pi/cursor-sdk.json`, start every such run with explicit approval:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
84
|
pi --approve --model cursor/composer-2-5
|
|
@@ -318,7 +318,7 @@ PI_CURSOR_LOCAL_RESUME=0 pi --model cursor/composer-2-5
|
|
|
318
318
|
|
|
319
319
|
Resume is strict: the current pi session file/id, branch path prefix, cwd/repo root, model/API/tool-surface pool key, SDK store identity, and compaction generation must match. Each persisted pi session gets a SQLite store under `<getDefaultSdkStateRoot(cwd)>/pi-sessions/<session-hash>/`, and that same store is used for create/resume, transcript reads, checkpoint lookup, and exact-ID cleanup so parallel pi sessions do not contend on one workspace `index.db`. Fileless sessions use a unique OS-temporary store per acquisition, remove it on graceful disposal, and start a fresh agent after invalidation instead of reopening a disposed temporary store. Legacy resume entries still try the SDK's default workspace store; if that resume fails or the agent is later replaced, the new agent moves to the per-session store. A trailing user message already present at process startup is crash-ambiguous and invalidates the old handle; only a user message appended in the current process may span a recorded handle, preventing restart from resending an already-submitted prompt. A successful process reattachment bootstraps the current pi transcript once while retaining the resumed Cursor agent's native state; later in-process turns remain incremental. If `Agent.resume()` fails, pi bootstraps a new local Cursor agent from the current transcript and streams one display-only continuity note. Superseded local agents can be cleaned up explicitly with `/cursor-local-resume-cleanup --dry-run` and `/cursor-local-resume-cleanup --yes`; cleanup only deletes exact recorded `agent-*` IDs from their recorded store. Cloud resume remains disabled; `/cursor-cloud list|archive|delete` only manages recorded cloud agents.
|
|
320
320
|
|
|
321
|
-
Config can also set non-secret defaults in `~/.pi/agent/cursor-sdk.json` or trusted `.pi/cursor-sdk.json`. Project config activates only when Pi's project-trust flow reached this extension and approved the project, or the run started with explicit `--approve`; Pi's implicit trust for a project with no recognized resources is not enough. Because Pi 0.
|
|
321
|
+
Config can also set non-secret defaults in `~/.pi/agent/cursor-sdk.json` or trusted `.pi/cursor-sdk.json`. Project config activates only when Pi's project-trust flow reached this extension and approved the project, or the run started with explicit `--approve`; Pi's implicit trust for a project with no recognized resources is not enough. Because Pi 0.84.0 loads project-local package extensions after the trust event, `pi install -l` users must pass `--approve` on every run that reads or writes `.pi/cursor-sdk.json`. A trust resource added after trust resolution requires restarting pi. `/cursor-runtime ... --save-project` requires the same trust provenance and does not create Pi trust resources automatically. Explicit runtime, fast-default, and HTTP transport saves preserve unrecognized fields, reject malformed or non-object JSON without rewriting it, and serialize concurrent writers. A completed global preference write is retained if Pi's subsequent session-journal append fails, because Pi may already have mutated the in-memory branch; the command reports that partial journal failure and ignores the uncertain session entry until a later successful save or session restart. If a process is force-killed during the tiny update window, the next save reports the `.lock` path; remove it only after confirming no pi process is writing that config.
|
|
322
322
|
|
|
323
323
|
```json
|
|
324
324
|
{
|
|
@@ -519,7 +519,7 @@ See [Cursor testing lessons](docs/cursor-testing-lessons.md#cursor-sdk-event-cap
|
|
|
519
519
|
|
|
520
520
|
If startup has no stored `/login` key or `CURSOR_API_KEY`, model discovery fails, or discovery returns no models, the extension registers a bundled fallback snapshot of the latest reviewed Cursor SDK model catalog and notifies interactive users when possible. Pi CLI `--api-key` remains available to provider turns but is not parsed independently during startup discovery.
|
|
521
521
|
|
|
522
|
-
The fallback snapshot includes Composer 2.5 (`composer-2.5` and `composer-2-5`), Composer 2, Cursor's GPT-5.6 Luna/Sol/Terra models, Claude, Gemini, Grok, Kimi, and other model IDs exposed by the reviewed `Cursor.models.list()` output. Pi's separate `openai-codex` catalog is owned by Pi itself; Pi 0.
|
|
522
|
+
The fallback snapshot includes Composer 2.5 (`composer-2.5` and `composer-2-5`), Composer 2, Cursor's GPT-5.6 Luna/Sol/Terra models, Claude, Gemini, Grok, Kimi, and other model IDs exposed by the reviewed `Cursor.models.list()` output. Pi's separate `openai-codex` catalog is owned by Pi itself; Pi 0.84.0 includes native `gpt-5.6-luna`, `gpt-5.6-sol`, and `gpt-5.6-terra` support. The exact checked-in Cursor snapshot lives in `src/cursor-fallback-models.generated.ts`. A dated maintainer capture documents the assistant-visible [Cursor system prompts and tool guidance](https://github.com/fitchmultz/pi-cursor-sdk/blob/main/docs/evidence/cursor-system-prompts-2026-08-02/README.md) for Grok 4.5, Opus 5, Fable 5, and the GPT-5.6 Sol/Terra/Luna family.
|
|
523
523
|
|
|
524
524
|
Actual Cursor runs still need a key from `/login`, `CURSOR_API_KEY`, or `--api-key`. If you add auth after startup, run `/cursor-refresh-models` to refresh the full live Cursor model catalog without restarting pi.
|
|
525
525
|
|
|
@@ -67,8 +67,8 @@ The replay scan flags only error `toolResult` / error assistant messages with `T
|
|
|
67
67
|
|
|
68
68
|
Pass criteria:
|
|
69
69
|
|
|
70
|
-
- `pi --version` reports
|
|
71
|
-
- `npm ls` shows `@cursor/sdk@1.0.23` and local `@earendil-works/*@0.
|
|
70
|
+
- `pi --version` reports Pi 0.84.0 for this cutover baseline.
|
|
71
|
+
- `npm ls` shows `@cursor/sdk@1.0.23` and local `@earendil-works/*@0.84.0` packages.
|
|
72
72
|
- `cursor/composer-2-5` appears in the model list.
|
|
73
73
|
- No Cursor key or auth token is printed.
|
|
74
74
|
- If neither `~/.pi/agent/auth.json` cursor auth nor `CURSOR_API_KEY` is available, stop and report the live smoke as blocked.
|
|
@@ -125,7 +125,7 @@ Observe with `tmux capture-pane -pt "$SESSION"` or attach manually.
|
|
|
125
125
|
Pass criteria:
|
|
126
126
|
|
|
127
127
|
- Footer shows `(cursor) composer-2-5`. With `--cursor-no-fast`, Cursor fast mode is off and the Cursor extension status should show `cursor:local · fast:off`; ignore unrelated status text from other extensions.
|
|
128
|
-
- The run uses
|
|
128
|
+
- The run uses Pi 0.84.0 `--session-id` successfully.
|
|
129
129
|
- Assistant answer appears correctly.
|
|
130
130
|
- `/session` shows one user and one assistant message for the simple run.
|
|
131
131
|
- Persisted JSONL has one assistant message. If the screen appears duplicated, inspect JSONL before deciding whether it is a rendering bug.
|
|
@@ -133,7 +133,7 @@ Pass criteria:
|
|
|
133
133
|
|
|
134
134
|
## 4. Focused visual card/color rendering check
|
|
135
135
|
|
|
136
|
-
This is the canonical inner-loop visual debug path for Cursor provider/runtime changes. It requires offscreen TUI visual inspection, not only JSONL or code review. Use
|
|
136
|
+
This is the canonical inner-loop visual debug path for Cursor provider/runtime changes. It requires offscreen TUI visual inspection, not only JSONL or code review. Use Pi 0.84.0, `@cursor/sdk@1.0.23`, a fresh temporary session dir, Cursor SDK `plan` mode, native replay enabled, and the checked-in visual runner. The runner resolves `pi` by directly walking the parent `PATH`, uses `process.execPath` for Node, and prepends that Node directory for both prereq checks and tmux launches so `#!/usr/bin/env node` shims use the validated Node. The default matrix is native replay only: native replay registration is forced on, settings sources are `none`, the pi bridge is off, overlapping built-in pi tools are not exposed, and inherited Cursor SDK event-debug artifact env is cleared. With `--event-debug`, debug capture writes to a deterministic directory under `VISUAL_DIR`.
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
139
|
VISUAL_DIR="$(mktemp -d /tmp/pi-cursor-sdk-1016-visual.XXXXXX)"
|
|
@@ -15,7 +15,7 @@ Current implementation notes:
|
|
|
15
15
|
- Cursor status uses one coordinated `ctx.ui.setStatus("cursor", ...)` value for fast, non-default plan mode, and the local-only `http1` transport marker; the default pi footer remains intact.
|
|
16
16
|
- Installed `@cursor/sdk` user messages accept images, and Cursor models are treated as image-capable; registered input metadata is `text` plus `image`.
|
|
17
17
|
- Image payload forwarding sends images only from the latest user message. If the latest user turn is plain text after an earlier image turn, the transcript keeps an `[image omitted from transcript]` placeholder but no image bytes are sent to Cursor. The prompt explicitly tells Cursor that prior image bytes are unavailable and to ask the user to reattach or describe a prior image when needed. Carrying images forward across turns remains a future product decision because it affects token cost, privacy, stale visual context, and expected multimodal follow-up behavior.
|
|
18
|
-
- Exact `@cursor/sdk@1.0.23` is a package dependency of this extension; users should not need a global SDK install.
|
|
18
|
+
- Exact `@cursor/sdk@1.0.23` is a package dependency of this extension; users should not need a global SDK install. Pi 0.84.0 is the minimum supported and current validation baseline, while optional published Pi core peer dependencies use `"*"` ranges per current Pi package guidance.
|
|
19
19
|
- Startup discovery does not duplicate Pi CLI parsing: it uses stored `~/.pi/agent/auth.json` API-key auth from `/login`, then `CURSOR_API_KEY`, otherwise it registers the bundled fallback catalog. Provider turns keep Pi's resolved `options.apiKey`. `/cursor-refresh-models` and `/cursor-cloud` mutations resolve provider `cursor` through the command context's Pi ModelRegistry, then normalize placeholders through `CURSOR_API_KEY`. The extension config file stores only non-secret Cursor-only state such as fast defaults and the user-level local HTTP transport preference.
|
|
20
20
|
- Cursor Cloud repository overrides accept only HTTPS repository URLs without userinfo, query parameters, or fragments. Invalid values fail during preflight before `Agent.create()`, messages never echo the supplied URL, and shared provider/maintainer scrubbing removes URL/SCP-style userinfo defensively.
|
|
21
21
|
- Cursor Cloud requires a persisted pi session. Immediately after remote `Agent.create()` returns, before debug work or abort checks, the provider appends a branch-local pi lifecycle entry, fsyncs the existing Pi session JSONL anchor through a read-write descriptor, and then fsyncs a newline-framed sidecar keyed by the stable pi session ID (POSIX mode `0600`; Windows inherits the user session directory ACL) in the session directory. Journal creation is exclusive, and existing append/read opens reject symlinks and require matching regular-file descriptor/path identity before using the descriptor. Existing session files use the exact lifecycle entry ID as anchor; fileless first turns use an orphan marker that a same-session-ID restart durably claims onto exactly one matching or replacement branch, surviving the timestamped path change and later JSONL creation without granting sibling access; returned run IDs are fsynced before abort/wait handling, readers skip malformed/truncated lines independently, and branch-only history events are reduced after deduplicated sidecar history, and tombstones are tracked before records exist. A durable sidecar result remains authoritative if its optional Pi mirror append fails; if the sidecar result itself fails, the prior durable intent remains pending and blocks retry rather than claiming success. `--no-session` and durable-ledger failures fail closed before send, while post-send ledger failure requests bounded cancellation. `/cursor-cloud` always validates the embedded session ID, accepts only null anchors while truly fileless, and reconciles each orphan to one branch before listing or mutation. Successive record events merge run/branch metadata monotonically even when mirrored sources arrive out of order. Archive/delete require resolved Cursor auth, fsync a durable intent before the SDK call, and fsync a durable success result afterward; unresolved intent blocks repeat mutation and requires dashboard inspection.
|
|
@@ -29,7 +29,7 @@ Current implementation notes:
|
|
|
29
29
|
- The provider also registers `cursor_ask_question` for Cursor models when the bridge and default-on `PI_CURSOR_ASK_QUESTION` control are enabled. While the tool awaits pi UI input it emits package event `pi-cursor-sdk:ask-question:blocked` with `{ active: true }` and clears `{ active: false }` in `finally`; the tool runs with `executionMode: "sequential"` so parallel sibling calls cannot overlap dialogs. Cursor sees it as `pi__cursor_ask_question`, and pi executes it through the normal tool path so interactive users can choose options from pi UI. `PI_CURSOR_ASK_QUESTION=0` removes only this tool while preserving the rest of the bridge. In non-UI modes it reports that UI is unavailable so Cursor can state a default assumption instead. When pi has visible Agent Skills loaded, the provider rewrites the skill catalog for Cursor and registers `cursor_activate_skill` as `pi__cursor_activate_skill`; pi executes it through the normal tool path so Cursor can load the full `SKILL.md` and skill resource list for the current pi-loaded skill source of truth. `PI_CURSOR_PI_TOOL_BRIDGE=0` disables the local bridge, including question and skill activation bridging. Cloud Cursor agents remain out of scope for the bridge.
|
|
30
30
|
- The bridge queues MCP calls, emits provider `toolcall_*` events, waits for matching pi `toolResult` messages by `toolCallId`, resolves the result back into the same live Cursor SDK run without creating a new `Agent`, and never calls tool `execute()` handlers directly. The same-run resume invariant holds unless the run was disposed, aborted, or cancelled.
|
|
31
31
|
- Cursor SDK MCP tool calls use a guarded timeout override because installed `@cursor/sdk` 1.0.23 still has a 60-second MCP request default with no public per-server timeout option. The extension extends the verified Cursor SDK MCP `callTool` timeout path to 3600 seconds by default and shortens the verified first-send MCP initialize/listTools timeout paths to 10 seconds by default so unavailable configured MCP servers do not block the first reply for a full minute; unknown MCP protocol timeout stacks keep the SDK default. Users can override tool-call timeouts with `PI_CURSOR_MCP_TOOL_TIMEOUT_MS` or `PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS`, and initialize/listTools timeouts with `PI_CURSOR_MCP_CONNECT_TIMEOUT_MS` or `PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS`. Bridged `CallTool` waits also have a local fail-closed deadline that defaults to and cannot exceed the effective MCP tool timeout; `PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS` can lower it, expiry or MCP cancellation aborts active pi execution when available, and expired bridge events are dropped before pi tool emission.
|
|
32
|
-
- Cursor SDK local safety controls are off by default. `--cursor-auto-review` / `PI_CURSOR_AUTO_REVIEW` and `--cursor-sandbox` / `PI_CURSOR_SANDBOX` pass only explicit enabled values into `Agent.create({ local })`; user or trusted project config can set `local.autoReview` and `local.sandboxOptions.enabled`; project config is active only when Pi's project-trust flow reached the extension and approved the project or the run used explicit `--approve`, and project saves require the same immutable trust provenance rather than creating Pi trust resources automatically. Pi 0.
|
|
32
|
+
- Cursor SDK local safety controls are off by default. `--cursor-auto-review` / `PI_CURSOR_AUTO_REVIEW` and `--cursor-sandbox` / `PI_CURSOR_SANDBOX` pass only explicit enabled values into `Agent.create({ local })`; user or trusted project config can set `local.autoReview` and `local.sandboxOptions.enabled`; project config is active only when Pi's project-trust flow reached the extension and approved the project or the run used explicit `--approve`, and project saves require the same immutable trust provenance rather than creating Pi trust resources automatically. Pi 0.84.0 loads `pi install -l` project-local extensions after the trust event, so those installs require `--approve` on every run that reads or writes `.pi/cursor-sdk.json`. Explicit runtime, fast-default, and HTTP transport saves preserve unrecognized config fields, reject malformed or non-object JSON without rewriting it, and use one lock-protected read-modify-write path; fast saves mutate only the selected model key. Because Pi can mutate its in-memory session branch before a journal append throws, a completed global save is authoritative and the command reports the partial journal failure instead of attempting an ambiguous rollback; the new global value stays authoritative over stale branch entries until a later successful save or session restart.
|
|
33
33
|
- Local HTTP/1.1/SSE compatibility is strictly opt-in through `PI_CURSOR_HTTP_1_1`, `/cursor-http on|off|toggle`, or user `cursor-sdk.json` `local.useHttp1ForAgent`. Precedence is session, environment, user, then the built-in unset default; project config is excluded. Unset makes no `Cursor.configure()` call. Explicit values configure the installed SDK before local `Agent.create()`, extension-owned explicit state is cleared with the SDK's documented `null` reset when returning to unset and during session shutdown before module reload, and default/HTTP2/HTTP1 choices split pooled local agents. Pi's supported CLI/TUI/print/RPC lifecycle has one active session runtime per process; concurrent independent `AgentSession` embedding in one process is outside this transport toggle's contract because the installed SDK setting and executor cache are module-global. The footer adds `http1` only for enabled local runtime; cloud creation and status remain untouched.
|
|
34
34
|
- Bridge diagnostics are opt-in only: `PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1` writes typed, allowlisted, scrubbed single-line JSONL records to `process.stderr` with prefix `[pi-cursor-sdk:bridge]`. Diagnostics are scrubbed operational logs, not anonymous telemetry. They intentionally include tool names, safe correlation IDs, run lifecycle, exposed pi↔MCP name pairs, queued requests, result resolution, rejection, cancellation, and pending counts. Correlation IDs are generated independently from the tokenized endpoint path, and Cursor MCP call IDs are hashed before serialization. Diagnostics must not include endpoint paths/URLs/path components/tokens, API keys, bearer tokens, cookies, session credentials, raw args/results, stdout/stderr payloads, file contents, Cursor settings output, or local private session paths in tracked docs, and they must not call pi UI status, notification, or footer APIs. If tool names themselves are unacceptable for a release target, bridge debug diagnostics are not safe for shared logs under the current contract.
|
|
35
35
|
- This repo does not provide a generic desktop-automation, browser-driver, or CDP recipe. Provider docs should describe pi-cursor-sdk's Cursor provider/bridge contract only.
|
|
@@ -134,7 +134,9 @@ CURSOR_API_KEY="your-key" npm run refresh:cursor-snapshots -- --write \
|
|
|
134
134
|
--context-windows ~/.pi/agent/cursor-sdk-context-windows.json
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
Both modes call `Cursor.models.list({ apiKey })` and use the same sanitizer and stable sort. `--check` byte-compares `src/cursor-fallback-models.generated.ts` without writing; `--write` refreshes it and updates `src/bundled-context-windows.ts` only when `--context-windows` is provided. Generated provenance and command output record the installed `@cursor/sdk` version and model count. The script prints model IDs/counts only and scrubs known auth material from SDK errors; it must not print or store API keys. Review generated diffs before committing because Cursor can change aliases, defaults, and parameter meanings.
|
|
137
|
+
Both modes call `Cursor.models.list({ apiKey })` and use the same sanitizer and stable sort. `--check` byte-compares `src/cursor-fallback-models.generated.ts` without writing; `--write` refreshes it and updates `src/bundled-context-windows.ts` only when `--context-windows` is provided. Context-window inputs are limited to current selectable model IDs; redundant default `:fast`/`:slow` aliases collapse to one key, conflicting equivalent selections fail generation, and stale or ambiguous aliases are omitted. Generated provenance and command output record the installed `@cursor/sdk` version and model count. The script prints model IDs/counts only and scrubs known auth material from SDK errors; it must not print or store API keys. Review generated diffs before committing because Cursor can change aliases, defaults, and parameter meanings.
|
|
138
|
+
|
|
139
|
+
Dated evidence for Cursor's assistant-visible, model-specific system text and reconstructed tool guidance lives in [Cursor System Prompts and Tool Guidance — 2026-08-02](https://github.com/fitchmultz/pi-cursor-sdk/blob/main/docs/evidence/cursor-system-prompts-2026-08-02/README.md). Keep that evidence separate from pi-cursor-sdk's own bootstrap prompt: Cursor persists its base system message in the local SDK checkpoint, while this extension sends Pi context and bridge instructions as user content.
|
|
138
140
|
|
|
139
141
|
## Design Direction
|
|
140
142
|
|
|
@@ -6,7 +6,7 @@ This workflow is the canonical repo path for verifying Cursor SDK tool replay th
|
|
|
6
6
|
|
|
7
7
|
Use it before accepting replay-card commits or PRs, and for every Cursor provider/runtime release where TUI card/color behavior could regress. Text logs and JSONL are necessary, but they are not enough when the claim is visual parity: always keep PNGs for the exact prompt, and keep before/after PNGs when reviewing a rendering change.
|
|
8
8
|
|
|
9
|
-
Current validation baseline:
|
|
9
|
+
Current validation baseline: Pi 0.84.0 or later, exact `@cursor/sdk@1.0.23`, and local validation packages `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` at exact 0.84.0. Optional published Pi core peer dependencies use `"*"` ranges per current Pi package guidance.
|
|
10
10
|
|
|
11
11
|
## Cursor SDK 1.0.17 / pi 0.79.0 cutover visual record
|
|
12
12
|
|
|
@@ -257,7 +257,7 @@ The script writes timestamped artifacts under `--out` (default `/tmp/pi-cursor-s
|
|
|
257
257
|
|
|
258
258
|
Stdout prints artifact paths and summary counts only. Raw payloads stay on disk and may contain local paths, project text, tool args/results, or secrets — do not commit or share them.
|
|
259
259
|
|
|
260
|
-
Hard repo rule: Cursor SDK behavior claims must come from the installed `@cursor/sdk` package and/or https://cursor.com/docs/sdk/typescript, not from memory or ad-hoc probes alone. Current cutover validation targets exact `@cursor/sdk@1.0.23` and
|
|
260
|
+
Hard repo rule: Cursor SDK behavior claims must come from the installed `@cursor/sdk` package and/or https://cursor.com/docs/sdk/typescript, not from memory or ad-hoc probes alone. Current cutover validation targets exact `@cursor/sdk@1.0.23` and Pi 0.84.0 local packages.
|
|
261
261
|
|
|
262
262
|
## Pi provider SDK event capture
|
|
263
263
|
|
package/docs/platform-smoke.md
CHANGED
|
@@ -106,7 +106,7 @@ Rendering is host-side. Targets capture the real ANSI stream; the macOS host ren
|
|
|
106
106
|
|
|
107
107
|
## Target session model
|
|
108
108
|
|
|
109
|
-
Each target opens one Crabbox target session, syncs once, runs all suites for that target under one coherent target run id, collects artifacts, and stops/releases the target. The release-gate entrypoint runs required targets
|
|
109
|
+
Each target opens one Crabbox target session, syncs once, runs all suites for that target under one coherent target run id, collects artifacts, and stops/releases the target. The release-gate entrypoint runs required targets sequentially to prevent shared host, VM/container, and Cursor API contention; each target runs its own suites in order and fails fast within that target. Platform smoke disables Crabbox git-seed sync (`CRABBOX_SYNC_GIT_SEED=false`) so every run tests the current local checkout and uncommitted smoke-runner changes rather than a remote Git seed.
|
|
110
110
|
|
|
111
111
|
```text
|
|
112
112
|
start target session
|
|
@@ -135,13 +135,13 @@ start target session
|
|
|
135
135
|
end target session
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
The target session fails fast. The release-gate path warms one Crabbox lease
|
|
138
|
+
The target session fails fast. The release-gate path handles one target at a time: it warms one Crabbox lease, performs one fresh sync, runs suites in order, and stops that target after the first failure before starting the next target. Total wall time is therefore additive across required targets; this avoids nondeterministic worker loss and live-run stalls from competing VM/container load and concurrent Cursor API calls. Per-suite commands remain available for diagnosis, but they are intentionally not the normal release path because repeated warmup/sync/install cycles make releases too slow.
|
|
139
139
|
|
|
140
140
|
Runtime budget is part of the contract:
|
|
141
141
|
|
|
142
142
|
- `smoke:platform:doctor` never calls Cursor.
|
|
143
143
|
- `platform-build` runs once per target and is the only suite that performs the full local CI/build/typecheck/package gate. Its Windows VM invocation raises only Vitest's default per-test timeout to 15 seconds for host-contention headroom; normal `npm test` keeps the 5-second default, and explicit longer integration-test timeouts still apply.
|
|
144
|
-
- Live suites reuse the target checkout and prepared `node_modules` when run after `platform-build`; they do not repeat `npm ci` in a target-session release run.
|
|
144
|
+
- Live suites reuse the target checkout and prepared `node_modules` when run after `platform-build`; they do not repeat `npm ci` in a target-session release run. Their interactive Pi process sets `PI_OFFLINE=1` to skip unrelated startup catalog/update probes; the scenario's explicit Cursor provider turn remains live. The PTY launches Pi's JavaScript entry with direct Node argv so multiline prompts remain one positional message on every target.
|
|
145
145
|
- Live and local-resume suites share one target-local packed-install prep directory per target-session release run. The first such suite runs `npm pack` and `npm install --no-save <tarball>` once. Visual/abort suites install that packed path with `pi install --approve -l`; local-resume lanes pass the same packed package path to their source-tree smoke harness instead of loading the checkout extension.
|
|
146
146
|
- Visual coverage is batched into one native prompt, one focused HTTP/1.1 transport prompt, one bridge prompt, and one abort/cleanup prompt per target. Do not split the card matrices into one prompt per card.
|
|
147
147
|
- The gate is fail-fast by target to avoid burning Cursor calls after a platform has already failed.
|
|
@@ -26,7 +26,12 @@ function normalize (uri, options) {
|
|
|
26
26
|
*/
|
|
27
27
|
function resolve (baseURI, relativeURI, options) {
|
|
28
28
|
const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' }
|
|
29
|
-
const
|
|
29
|
+
const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions)
|
|
30
|
+
const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions)
|
|
31
|
+
if (baseMalformed || relativeMalformed) {
|
|
32
|
+
throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
|
|
33
|
+
}
|
|
34
|
+
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true)
|
|
30
35
|
schemelessOptions.skipEscape = true
|
|
31
36
|
return serialize(resolved, schemelessOptions)
|
|
32
37
|
}
|
|
@@ -206,6 +211,15 @@ const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:
|
|
|
206
211
|
// with or without a scheme prefix, for the literal-backslash rejection below.
|
|
207
212
|
const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/
|
|
208
213
|
|
|
214
|
+
// Captures the leading authority-introducer region after an optional scheme: a
|
|
215
|
+
// run of forward slashes, backslashes, and the characters the WHATWG URL parser
|
|
216
|
+
// removes before parsing (TAB U+0009, LF U+000A, CR U+000D). A valid introducer
|
|
217
|
+
// is exactly "//". Node treats "\" as "/" on special schemes and strips those
|
|
218
|
+
// characters first, so forms like "\\", "/\", "\/", "/<TAB>/", or a leading
|
|
219
|
+
// "<TAB>//" reach an authority in Node while fast-uri's URI_PARSE folds them into
|
|
220
|
+
// the path group (host confusion / SSRF / redirect bypass).
|
|
221
|
+
const AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/
|
|
222
|
+
|
|
209
223
|
/**
|
|
210
224
|
* @param {import('./types/index').URIComponent} parsed
|
|
211
225
|
* @param {RegExpMatchArray} matches
|
|
@@ -265,6 +279,28 @@ function parseWithStatus (uri, opts) {
|
|
|
265
279
|
malformedAuthorityOrPort = true
|
|
266
280
|
}
|
|
267
281
|
|
|
282
|
+
// Reject a malformed or whitespace-smuggled authority introducer. fast-uri
|
|
283
|
+
// only recognizes a literal "//"; anything else in the leading separator run
|
|
284
|
+
// (a backslash, or a "//" that appears only after removing the TAB/LF/CR that
|
|
285
|
+
// Node strips) means the authority fast-uri parses differs from the one Node's
|
|
286
|
+
// URL resolves. Reject rather than rewrite, mirroring the literal-backslash
|
|
287
|
+
// guard above. Percent-encoded forms (%5C, %09) are untouched, valid data.
|
|
288
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION)
|
|
289
|
+
if (introducerMatch !== null) {
|
|
290
|
+
const region = introducerMatch[1]
|
|
291
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, '')
|
|
292
|
+
// Two or more leading separators introduce an authority.
|
|
293
|
+
if (normalizedRegion.length >= 2) {
|
|
294
|
+
if (normalizedRegion.slice(0, 2) !== '//') {
|
|
295
|
+
parsed.error = parsed.error || 'URI authority must not contain a literal backslash.'
|
|
296
|
+
malformedAuthorityOrPort = true
|
|
297
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
298
|
+
parsed.error = parsed.error || 'URI authority introducer must not contain whitespace.'
|
|
299
|
+
malformedAuthorityOrPort = true
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
268
304
|
const matches = uri.match(URI_PARSE)
|
|
269
305
|
|
|
270
306
|
if (matches) {
|
|
@@ -221,3 +221,139 @@ test('parse does not reject a literal backslash in the query or fragment', (t) =
|
|
|
221
221
|
t.notOk(parsed.error, 'backslash in query/fragment does not mark the URI malformed')
|
|
222
222
|
t.equal(parsed.host, 'host.example.com', 'host parsed normally')
|
|
223
223
|
})
|
|
224
|
+
|
|
225
|
+
test('parse rejects a malformed authority introducer (\\\\, /\\, \\/) in place of //', (t) => {
|
|
226
|
+
// Regression: "\\", "/\\", "\\/" after the scheme colon are not valid authority
|
|
227
|
+
// introducers. Node's URL treats "\\" as interchangeable with "/" on special
|
|
228
|
+
// schemes, so "http:\\\\evil.com/path" would be parsed as host "evil.com" by
|
|
229
|
+
// Node, but fast-uri must reject it as malformed to prevent SSRF/redirect bypass.
|
|
230
|
+
const cases = [
|
|
231
|
+
'http:\\\\evil.com/path',
|
|
232
|
+
'http:/\\evil.com/path',
|
|
233
|
+
'http:\\/evil.com/path',
|
|
234
|
+
'ws:\\\\evil.com/chat',
|
|
235
|
+
'wss:\\\\evil.com/chat',
|
|
236
|
+
'ftp:\\\\evil.com/',
|
|
237
|
+
'\\\\evil.com/path'
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
t.plan(cases.length)
|
|
241
|
+
|
|
242
|
+
cases.forEach((input) => {
|
|
243
|
+
t.equal(
|
|
244
|
+
fastURI.parse(input).error,
|
|
245
|
+
'URI authority must not contain a literal backslash.',
|
|
246
|
+
input
|
|
247
|
+
)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('normalize does not canonicalize a malformed-authority-introducer URI', (t) => {
|
|
252
|
+
const cases = [
|
|
253
|
+
'http:\\\\evil.com/path',
|
|
254
|
+
'http:/\\evil.com/path'
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
t.plan(cases.length)
|
|
258
|
+
|
|
259
|
+
cases.forEach((input) => {
|
|
260
|
+
t.equal(fastURI.normalize(input), input, input)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('equal returns false for malformed-authority-introducer URIs', (t) => {
|
|
265
|
+
const pairs = [
|
|
266
|
+
['http:\\\\evil.com/path', 'http://evil.com/path'],
|
|
267
|
+
['http:/\\evil.com/path', 'http://evil.com/path']
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
t.plan(pairs.length)
|
|
271
|
+
|
|
272
|
+
pairs.forEach(([left, right]) => {
|
|
273
|
+
t.equal(fastURI.equal(left, right), false, `${left} != ${right}`)
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('resolve throws on malformed authority introducer', (t) => {
|
|
278
|
+
// resolve() returns a plain string with no error field, so the only safe
|
|
279
|
+
// behavior is to throw when either component has a malformed authority.
|
|
280
|
+
const pairs = [
|
|
281
|
+
['https://allowed.com/', '\\\\evil.com/path'],
|
|
282
|
+
['\\\\evil.com/path', 'https://allowed.com/'],
|
|
283
|
+
['https://allowed.com/', 'http:/\\evil.com/path'],
|
|
284
|
+
['https://allowed.com/', 'http:\\/evil.com/path']
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
t.plan(pairs.length)
|
|
288
|
+
|
|
289
|
+
pairs.forEach(([base, rel]) => {
|
|
290
|
+
t.throws(
|
|
291
|
+
() => fastURI.resolve(base, rel),
|
|
292
|
+
/URI authority must not contain a literal backslash/,
|
|
293
|
+
`${base} + ${rel}`
|
|
294
|
+
)
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('parse rejects a whitespace-split authority introducer (TAB, LF, CR)', (t) => {
|
|
299
|
+
// The WHATWG URL parser removes TAB (U+0009), LF (U+000A) and CR (U+000D) from
|
|
300
|
+
// the input before parsing, so a stripped character wedged into the introducer
|
|
301
|
+
// ("/<TAB>\\", "/<TAB>/", or a leading "<TAB>//") reaches an authority in Node
|
|
302
|
+
// while fast-uri would otherwise fold it into the path. These must be rejected
|
|
303
|
+
// like the adjacent "\\", "/\\", "\\/" forms.
|
|
304
|
+
const cases = [
|
|
305
|
+
{ input: '/\t\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
|
|
306
|
+
{ input: '/\t/evil.com/path', expectedError: 'URI authority introducer must not contain whitespace.' },
|
|
307
|
+
{ input: '/\n\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
|
|
308
|
+
{ input: '/\r\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
|
|
309
|
+
{ input: '\t//evil.com/path', expectedError: 'URI authority introducer must not contain whitespace.' },
|
|
310
|
+
{ input: '\t/\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
|
|
311
|
+
{ input: 'https:/\t/evil.com/path', expectedError: 'URI authority introducer must not contain whitespace.' }
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
t.plan(cases.length)
|
|
315
|
+
|
|
316
|
+
cases.forEach(({ input, expectedError }) => {
|
|
317
|
+
t.equal(fastURI.parse(input).error, expectedError, JSON.stringify(input))
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('resolve throws on a whitespace-split authority introducer', (t) => {
|
|
322
|
+
const pairs = [
|
|
323
|
+
['https://allowed.com/', '/\t\\evil.com/path'],
|
|
324
|
+
['https://allowed.com/', '/\t/evil.com/path'],
|
|
325
|
+
['https://allowed.com/', '/\n\\evil.com/path'],
|
|
326
|
+
['/\t/evil.com/path', 'https://allowed.com/']
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
t.plan(pairs.length)
|
|
330
|
+
|
|
331
|
+
pairs.forEach(([base, rel]) => {
|
|
332
|
+
t.throws(
|
|
333
|
+
() => fastURI.resolve(base, rel),
|
|
334
|
+
/URI authority (must not contain a literal backslash|introducer must not contain whitespace)/,
|
|
335
|
+
`${JSON.stringify(base)} + ${JSON.stringify(rel)}`
|
|
336
|
+
)
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('parse does not reject valid authority introducer patterns', (t) => {
|
|
341
|
+
// No false positives: "//" introducer and scheme-less "//" must be valid.
|
|
342
|
+
const cases = [
|
|
343
|
+
'http://good.com/',
|
|
344
|
+
'https://good.com/',
|
|
345
|
+
'ws://good.com/chat',
|
|
346
|
+
'wss://good.com/chat',
|
|
347
|
+
'ftp://good.com/',
|
|
348
|
+
'//good.com/path',
|
|
349
|
+
'/absolute/path',
|
|
350
|
+
'relative/path'
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
t.plan(cases.length)
|
|
354
|
+
|
|
355
|
+
cases.forEach((input) => {
|
|
356
|
+
const parsed = fastURI.parse(input)
|
|
357
|
+
t.notOk(parsed.error, input)
|
|
358
|
+
})
|
|
359
|
+
})
|
|
@@ -295,11 +295,11 @@ class Context {
|
|
|
295
295
|
return Object.fromEntries(this.#var);
|
|
296
296
|
}
|
|
297
297
|
#newResponse(data, arg, headers) {
|
|
298
|
-
|
|
299
|
-
if (typeof arg === "object" &&
|
|
300
|
-
|
|
301
|
-
for (const [key, value] of
|
|
302
|
-
if (key
|
|
298
|
+
let responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders;
|
|
299
|
+
if (typeof arg === "object" && arg.headers) {
|
|
300
|
+
responseHeaders ??= new Headers();
|
|
301
|
+
for (const [key, value] of new Headers(arg.headers)) {
|
|
302
|
+
if (key === "set-cookie") {
|
|
303
303
|
responseHeaders.append(key, value);
|
|
304
304
|
} else {
|
|
305
305
|
responseHeaders.set(key, value);
|
|
@@ -307,19 +307,34 @@ class Context {
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
if (headers) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
310
|
+
if (!responseHeaders) {
|
|
311
|
+
let count = 0;
|
|
312
|
+
for (const k in headers) {
|
|
313
|
+
if (++count > 1 || typeof headers[k] !== "string") {
|
|
314
|
+
responseHeaders = new Headers();
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (responseHeaders) {
|
|
320
|
+
for (const k in headers) {
|
|
321
|
+
const v = headers[k];
|
|
322
|
+
if (typeof v === "string") {
|
|
323
|
+
responseHeaders.set(k, v);
|
|
324
|
+
} else {
|
|
325
|
+
responseHeaders.delete(k);
|
|
326
|
+
for (const v2 of v) {
|
|
327
|
+
responseHeaders.append(k, v2);
|
|
328
|
+
}
|
|
317
329
|
}
|
|
318
330
|
}
|
|
319
331
|
}
|
|
320
332
|
}
|
|
321
333
|
const status = typeof arg === "number" ? arg : arg?.status ?? this.#status;
|
|
322
|
-
return createResponseInstance(data, {
|
|
334
|
+
return createResponseInstance(data, {
|
|
335
|
+
status,
|
|
336
|
+
headers: responseHeaders ?? headers
|
|
337
|
+
});
|
|
323
338
|
}
|
|
324
339
|
newResponse = (...args) => this.#newResponse(...args);
|
|
325
340
|
/**
|
|
@@ -92,6 +92,10 @@ const proxy = async (input, proxyInit) => {
|
|
|
92
92
|
req.headers.delete("accept-encoding");
|
|
93
93
|
const res = await (customFetch || fetch)(req);
|
|
94
94
|
const resHeaders = new Headers(res.headers);
|
|
95
|
+
const connectionValue = resHeaders.get("connection");
|
|
96
|
+
if (connectionValue) {
|
|
97
|
+
connectionValue.split(",").map((h) => h.trim()).filter((h) => ALLOWED_TOKEN_PATTERN.test(h)).forEach((h) => resHeaders.delete(h));
|
|
98
|
+
}
|
|
95
99
|
hopByHopHeaders.forEach((header) => {
|
|
96
100
|
resHeaders.delete(header);
|
|
97
101
|
});
|
|
@@ -42,7 +42,7 @@ class SSEStreamingApi extends import_stream.StreamingApi {
|
|
|
42
42
|
const sseData = [
|
|
43
43
|
message.event && `event: ${message.event}`,
|
|
44
44
|
dataLines,
|
|
45
|
-
message.id && `id: ${message.id}`,
|
|
45
|
+
message.id !== void 0 && `id: ${message.id}`,
|
|
46
46
|
message.retry !== void 0 && `retry: ${message.retry}`
|
|
47
47
|
].filter(Boolean).join("\n") + "\n\n";
|
|
48
48
|
await this.write(sseData);
|
|
@@ -43,6 +43,7 @@ class Hono {
|
|
|
43
43
|
delete;
|
|
44
44
|
options;
|
|
45
45
|
patch;
|
|
46
|
+
query;
|
|
46
47
|
all;
|
|
47
48
|
on;
|
|
48
49
|
use;
|
|
@@ -342,8 +343,8 @@ class Hono {
|
|
|
342
343
|
* @see {@link https://hono.dev/docs/api/hono#fetch}
|
|
343
344
|
*
|
|
344
345
|
* @param {Request} request - request Object of request
|
|
345
|
-
* @param {Env}
|
|
346
|
-
* @param {ExecutionContext} - context of execution
|
|
346
|
+
* @param {Env} env - env Object
|
|
347
|
+
* @param {ExecutionContext} executionCtx - context of execution
|
|
347
348
|
* @returns {Response | Promise<Response>} response of request
|
|
348
349
|
*
|
|
349
350
|
*/
|
|
@@ -100,6 +100,18 @@ const booleanAttributes = [
|
|
|
100
100
|
"reversed",
|
|
101
101
|
"selected"
|
|
102
102
|
];
|
|
103
|
+
const resolveFunctionComponentResult = (result, suspendedContext) => result.then((resolved) => {
|
|
104
|
+
if (!Array.isArray(resolved) && !(resolved instanceof JSXNode)) {
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
const children = Array.isArray(resolved) ? resolved : [resolved];
|
|
108
|
+
const render = () => {
|
|
109
|
+
const buffer = [""];
|
|
110
|
+
childrenToStringToBuffer(children, buffer);
|
|
111
|
+
return buffer.length === 1 ? (0, import_html.raw)(buffer[0], buffer.callbacks) : (0, import_html2.stringBufferToString)(buffer, buffer.callbacks);
|
|
112
|
+
};
|
|
113
|
+
return suspendedContext ? suspendedContext(render) : (0, import_context.runWithRenderContext)(render);
|
|
114
|
+
});
|
|
103
115
|
const childrenToStringToBuffer = (children, buffer) => {
|
|
104
116
|
for (let i = 0, len = children.length; i < len; i++) {
|
|
105
117
|
const child = children[i];
|
|
@@ -109,9 +121,17 @@ const childrenToStringToBuffer = (children, buffer) => {
|
|
|
109
121
|
continue;
|
|
110
122
|
} else if (child instanceof JSXNode) {
|
|
111
123
|
child.toStringToBuffer(buffer);
|
|
112
|
-
} else if (typeof child === "number"
|
|
124
|
+
} else if (typeof child === "number") {
|
|
125
|
+
;
|
|
126
|
+
buffer[0] += child;
|
|
127
|
+
} else if (child.isEscaped) {
|
|
113
128
|
;
|
|
114
129
|
buffer[0] += child;
|
|
130
|
+
const callbacks = child.callbacks;
|
|
131
|
+
if (callbacks) {
|
|
132
|
+
buffer.callbacks ||= [];
|
|
133
|
+
buffer.callbacks.push(...callbacks);
|
|
134
|
+
}
|
|
115
135
|
} else if (child instanceof Promise) {
|
|
116
136
|
buffer.unshift("", child);
|
|
117
137
|
} else {
|
|
@@ -125,7 +145,6 @@ class JSXNode {
|
|
|
125
145
|
key;
|
|
126
146
|
children;
|
|
127
147
|
isEscaped = true;
|
|
128
|
-
suspendedContext;
|
|
129
148
|
constructor(tag, props, children) {
|
|
130
149
|
if (typeof tag !== "function" && !(0, import_utils.isValidTagName)(tag)) {
|
|
131
150
|
throw new Error(`Invalid JSX tag name: ${tag}`);
|
|
@@ -148,7 +167,7 @@ class JSXNode {
|
|
|
148
167
|
this.toStringToBuffer(buffer);
|
|
149
168
|
return buffer.length === 1 ? "callbacks" in buffer ? (0, import_html2.resolveCallbackSync)((0, import_html.raw)(buffer[0], buffer.callbacks)).toString() : buffer[0] : (0, import_html2.stringBufferToString)(buffer, buffer.callbacks);
|
|
150
169
|
};
|
|
151
|
-
return
|
|
170
|
+
return (0, import_context.runWithRenderContext)(render);
|
|
152
171
|
}
|
|
153
172
|
toStringToBuffer(buffer) {
|
|
154
173
|
const tag = this.tag;
|
|
@@ -222,21 +241,14 @@ class JSXFunctionNode extends JSXNode {
|
|
|
222
241
|
return;
|
|
223
242
|
} else if (res instanceof Promise) {
|
|
224
243
|
if (import_context.globalContexts.length === 0) {
|
|
225
|
-
buffer.unshift("", res);
|
|
244
|
+
buffer.unshift("", resolveFunctionComponentResult(res));
|
|
226
245
|
} else {
|
|
227
|
-
|
|
228
|
-
buffer.unshift(
|
|
229
|
-
"",
|
|
230
|
-
res.then((childRes) => {
|
|
231
|
-
if (childRes instanceof JSXNode) {
|
|
232
|
-
childRes.suspendedContext = suspendedContext;
|
|
233
|
-
}
|
|
234
|
-
return childRes;
|
|
235
|
-
})
|
|
236
|
-
);
|
|
246
|
+
buffer.unshift("", resolveFunctionComponentResult(res, (0, import_context.captureRenderContext)()));
|
|
237
247
|
}
|
|
238
248
|
} else if (res instanceof JSXNode) {
|
|
239
249
|
res.toStringToBuffer(buffer);
|
|
250
|
+
} else if (Array.isArray(res)) {
|
|
251
|
+
childrenToStringToBuffer(res, buffer);
|
|
240
252
|
} else if (typeof res === "number" || res.isEscaped) {
|
|
241
253
|
buffer[0] += res;
|
|
242
254
|
if (res.callbacks) {
|
|
@@ -315,15 +327,7 @@ const shallowEqual = (a, b) => {
|
|
|
315
327
|
return true;
|
|
316
328
|
};
|
|
317
329
|
const memo = (component, propsAreEqual = shallowEqual) => {
|
|
318
|
-
|
|
319
|
-
let prevProps = void 0;
|
|
320
|
-
const wrapper = ((props) => {
|
|
321
|
-
if (prevProps && !propsAreEqual(prevProps, props)) {
|
|
322
|
-
computed = null;
|
|
323
|
-
}
|
|
324
|
-
prevProps = props;
|
|
325
|
-
return computed ||= component(props);
|
|
326
|
-
});
|
|
330
|
+
const wrapper = ((props) => component(props));
|
|
327
331
|
wrapper[import_constants.DOM_MEMO] = propsAreEqual;
|
|
328
332
|
wrapper[import_constants.DOM_RENDERER] = component;
|
|
329
333
|
return wrapper;
|