Repository navigation
Keep hosted write previews redeemable across MCP sessions of one consent - #41
christfarah wants to merge 1 commit into
Conversation
Hosted previews lived in the memory of the Durable Object for one MCP session. Clients that open a new MCP session for each tool call could issue a preview but never redeem it: every approved write failed with "Preview token is missing, expired, already used, or belongs to another authenticated session". The hosted connector now stores previews in the consent's credential Durable Object, as a token hash, plan fingerprint and expiry only. Any session of the same consent can redeem a preview once; other consents cannot see it, and removing a consent removes its previews. Expiry, single use, the fresh-state fingerprint check and separate confirmed:true are unchanged. Local stdio previews stay session-bound in memory.
9a5af1c to
098aacf
Compare
|
The consent-scoped durable-preview behavior landed on main in 2ddad4d on October 25, so this PR is superseded. The implementation stores only a token hash, plan fingerprint and expiry in the consent's credential Durable Object. It retains single use, five-minute expiry, exact fresh-state validation and separate I rechecked current main today: all 25 preview/MCP integration tests and six selected Worker tests pass. They cover approval from a new session of the same consent, rejection across clients and users, changed plans, replay, expiry, consent removal and the 100-preview bound. Issue #40 records the October 25 deployment and remains open for the signed-in ChatGPT write acceptance check. Thanks for the fix and the reproduction. Closing this duplicate implementation preserves the version already on main; the contributor's fork branch is unchanged. |
Fixes #40
Problem
Hosted previews lived in the memory of the Durable Object for one MCP session. Clients that open a new MCP session per tool call (ChatGPT's connector gateway does) could issue a preview but never redeem it, so every approved write failed with "Preview token is missing, expired, already used, or belongs to another authenticated session". Details and evidence are in the linked issue.
Change
lib/write-previews.mjs: newcreateStoredWritePreviews({ tenantId, store, ttlMs, now }). It has the same issue/claim/consume contract and result shape, with an async consent-scoped store. The in-memorycreateWritePreviewsis unchanged apart from sharing two private helpers with it.index.js:createYnabServeraccepts an optionalpreviewStore. Without one (local stdio), previews stay in session memory exactly as before.claimandissueare awaited.worker/src/credential-store-core.js:/preview/issueand/preview/claimon the existing per-userOAuthCredentialsDurable Object, behind the same consent check as/tokenand the journal. It stores onlysha256(token), the plan fingerprint and the expiry, rejects malformed or over-long records (15-minute ceiling), and keeps at most 100 live previews per consent./removedeletes a consent's previews./delete-allalready wiped them.worker/src/ynab-oauth.js/hosted-options.js:createCredentialPreviewStore(env, binding), supplied aspreviewStorefor credential-bound hosted sessions only. Legacy grants get none.What stays the same
Single use (claim deletes atomically), the five-minute expiry, the exact fresh-state fingerprint check, the separate
confirmed: true, write enablement and read-only defaults. A preview cannot cross consents: each consent has its own key prefix behind the credential binding check, and the fingerprint also binds the consent identity.Trade-off
A preview from one MCP session can now be redeemed once by another session of the same consent (same YNAB user, client and credential). Hosted clients need this, and it matches how the operation journal already handles session rebinding.
Tests
test/write-preview.test.mjs: the stored manager works across two instances, is single-use, expires, rejects stale plans and other tenants, and never hands the plan to the store.test/integration-mcp-http.test.mjs: preview on one MCP session, approved write on another, with the same consent sharing a store. Exactly one HTTP write; reuse and other-tenant attempts are rejected. Before the fix this test failed with the production error.worker/test/worker.test.mjs: through the realCredentialStoreCoreandhostedServerOptions, previews cross sessions of one consent only. Storage holds no raw token or plan text,/removeclears previews, legacy grants get no store, and malformed, expired, over-long and unbounded records are rejected or evicted.Local checks:
test:unit(242 pass),test:safety,test:eval,release:check,smoke:list-tools, workernpm test(62 pass) andwrangler deploy --dry-run.