You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(ai): extend the injection guard to the stream
The one-shot path scans the finished answer before returning it. A streamed answer leaves the server while it is still being written, so that scan cannot be applied: a nonce split across two deltas passes a per-delta check untouched and reaches the client.
Run the stream through a guard that withholds the last nonce.length - 1 characters and scans them together with each new delta, releasing only text that can no longer begin the nonce. That length is the exact minimum, since an occurrence spans nonce.length characters and holding one less leaves it inside a single scanned window. The holdback is released at the end of a text block, because that text cannot be emitted under the next block's id, and its tail is kept as scanning context, without which a nonce split across two blocks would pass unseen.
The guard runs in experimental_transform, over the model's typed parts rather than the encoded SSE bytes, where JSON envelopes and escaping would split a marker beyond the reach of any substring scan. stopStream is not used: it obliges the caller to synthesize finish chunks whose shape follows the SDK version, while suppressing the remaining text keeps the stream well formed.
A rejection is sent as an error part, not as another text delta. The client concatenates deltas, so a fallback message sent that way would land glued to the truncated prefix already on screen; on its own channel it tells the client to drop what it has. The prefix itself cannot be taken back, and the holdback cuts it at an arbitrary character.
Reasoning is turned off at the route. Those deltas carry model text built from the same untrusted payload and the UI message stream sends them by default, so they would reach the client without passing the nonce check. Guarding them instead would mean a second holdback over a second stream, for text nothing renders.
TransformStream is declared as an eslint global: it is on globalThis since Node 18, but eslint's node env predates the WHATWG Streams API.
0 commit comments