Public: anyone can read this; the crew can change it.

A test proves who may dismiss a feed item or mute its app: the route and MCP `feed_dismiss` refuse the same things through src/feed/interact.ts #7530

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The route and MCP `feed_dismiss` now refuse a feed act through one function, `readInteraction` in `src/feed/interact.ts`, and give the same error code and words. A new `test/feed-interact.test.ts` covers `interactionTarget` directly and calls both doors with the same refused inputs.

shipped production is serving it deploy 24f2911

opened this

The route and MCP feed_dismiss now refuse a feed act through one function, readInteraction in src/feed/interact.ts, and give the same error code and words. A new test/feed-interact.test.ts covers interactionTarget directly and calls both doors with the same refused inputs.

The fault the test found: the two doors disagreed. Both already called interactionTarget, but each checked the input itself and refused in its own words:

  • Error codes: the route said invalid_item, invalid_kind or not_found ("no such resource here…"). The tool said invalid for both a bad item and a bad kind, and not_found: no such item.
  • Whitespace: the tool trimmed its strings and the route did not, so " event:x " was accepted by one door and refused by the other.

The fix is in src/feed/interact.ts. readInteraction(db, viewer, { item, kind, app }, kinds) checks the item id, the kind and the target, in that order. It returns the refusal as data: invalid_item, invalid_kind, or not_found with NO_SUCH_ITEM ("no such item, or it is not visible to you"). The route and the tool both call it. The route keeps its status codes (422 and 404) and the error codes its API description lists. The tool now uses those codes too. Both doors trim item and app.

One difference is kept on purpose. Each door passes its own list of kinds. The route records every act a card reports (opened, dwelled, …), while the tool only dismisses or mutes. So a kind outside a door's list is invalid_kind at both, but the message names that door's own kinds. The test checks that opened is accepted by the route and refused by the tool.

What the new test covers:

  • An event on a repository the viewer can read resolves to its repository and actor.
  • An event the viewer cannot read is refused exactly as an unknown event is, even when it names a readable app.
  • An item that is not an event needs a readable owner/name app; empty, partial, extra-segment, missing and private apps are all refused.
  • A kind outside dismissed or muted is refused, and so is a bad item id.
  • For each refused input, the HTTP route and MCP feed_dismiss give the same code and message, and both accept the same good input.

No existing test changed; test/mcp-feed-dismiss.test.ts still passes, since the new not-found message still contains "no such item".

Checked: bun x tsc --noEmit is clean. These suites pass:

  • test/feed-interact.test.ts, test/mcp-feed-dismiss.test.ts, test/feed.test.ts, test/openapi-oa-feed.test.ts, test/mcp-size.test.ts
  • the watches, feed-page and offline suites
  • the mcp, mcp-tools, mcp-docs, api-errors, openapi, cross-origin-mutations, feed-choose and share suites

scripts/fast.ts named 929 test files for this change, far over its 5-minute budget, so I ran the suites above instead.

To try it, POST { "item": "ask:look:x", "kind": "muted", "app": "alice/secret" } as someone who cannot read that repository. /api/v1/feed/interactions and MCP feed_dismiss both answer not_found: no such item, or it is not visible to you.

🤖 Generated with Claude Code

Closes #7524

commented

The route and MCP feed_dismiss now refuse a feed act through one function, readInteraction in src/feed/interact.ts, and give the same error code and words. A new test/feed-interact.test.ts covers interactionTarget directly and calls both doors with the same refused inputs.

The fault the test found: the two doors disagreed. Both already called interactionTarget, but each checked the input itself and refused in its own words:

  • Error codes: the route said invalid_item, invalid_kind or not_found ("no such resource here…"). The tool said invalid for both a bad item and a bad kind, and not_found: no such item.
  • Whitespace: the tool trimmed its strings and the route did not, so " event:x " was accepted by one door and refused by the other.

The fix is in src/feed/interact.ts. readInteraction(db, viewer, { item, kind, app }, kinds) checks the item id, the kind and the target, in that order. It returns the refusal as data: invalid_item, invalid_kind, or not_found with NO_SUCH_ITEM ("no such item, or it is not visible to you"). The route and the tool both call it. The route keeps its status codes (422 and 404) and the error codes its API description lists. The tool now uses those codes too. Both doors trim item and app.

One difference is kept on purpose. Each door passes its own list of kinds. The route records every act a card reports (opened, dwelled, …), while the tool only dismisses or mutes. So a kind outside a door's list is invalid_kind at both, but the message names that door's own kinds. The test checks that opened is accepted by the route and refused by the tool.

What the new test covers:

  • An event on a repository the viewer can read resolves to its repository and actor.
  • An event the viewer cannot read is refused exactly as an unknown event is, even when it names a readable app.
  • An item that is not an event needs a readable owner/name app; empty, partial, extra-segment, missing and private apps are all refused.
  • A kind outside dismissed or muted is refused, and so is a bad item id.
  • For each refused input, the HTTP route and MCP feed_dismiss give the same code and message, and both accept the same good input.

No existing test changed; test/mcp-feed-dismiss.test.ts still passes, since the new not-found message still contains "no such item".

Checked: bun x tsc --noEmit is clean. These suites pass:

  • test/feed-interact.test.ts, test/mcp-feed-dismiss.test.ts, test/feed.test.ts, test/openapi-oa-feed.test.ts, test/mcp-size.test.ts
  • the watches, feed-page and offline suites
  • the mcp, mcp-tools, mcp-docs, api-errors, openapi, cross-origin-mutations, feed-choose and share suites

scripts/fast.ts named 929 test files for this change, far over its 5-minute budget, so I ran the suites above instead.

To try it, POST { "item": "ask:look:x", "kind": "muted", "app": "alice/secret" } as someone who cannot read that repository. /api/v1/feed/interactions and MCP feed_dismiss both answer not_found: no such item, or it is not visible to you.

🤖 Generated with Claude Code

commented

The route and MCP feed_dismiss now refuse a feed act through one function, readInteraction in src/feed/interact.ts, and give the same error code and words. A new test/feed-interact.test.ts covers interactionTarget directly and calls both doors with the same refused inputs.

The fault the test found: the two doors disagreed. Both already called interactionTarget, but each checked the input itself and refused in its own words:

  • Error codes: the route said invalid_item, invalid_kind or not_found ("no such resource here…"). The tool said invalid for both a bad item and a bad kind, and not_found: no such item.
  • Whitespace: the tool trimmed its strings and the route did not, so " event:x " was accepted by one door and refused by the other.

The fix is in src/feed/interact.ts. readInteraction(db, viewer, { item, kind, app }, kinds) checks the item id, the kind and the target, in that order. It returns the refusal as data: invalid_item, invalid_kind, or not_found with NO_SUCH_ITEM ("no such item, or it is not visible to you"). The route and the tool both call it. The route keeps its status codes (422 and 404) and the error codes its API description lists. The tool now uses those codes too. Both doors trim item and app.

One difference is kept on purpose. Each door passes its own list of kinds. The route records every act a card reports (opened, dwelled, …), while the tool only dismisses or mutes. So a kind outside a door's list is invalid_kind at both, but the message names that door's own kinds. The test checks that opened is accepted by the route and refused by the tool.

What the new test covers:

  • An event on a repository the viewer can read resolves to its repository and actor.
  • An event the viewer cannot read is refused exactly as an unknown event is, even when it names a readable app.
  • An item that is not an event needs a readable owner/name app; empty, partial, extra-segment, missing and private apps are all refused.
  • A kind outside dismissed or muted is refused, and so is a bad item id.
  • For each refused input, the HTTP route and MCP feed_dismiss give the same code and message, and both accept the same good input.

No existing test changed; test/mcp-feed-dismiss.test.ts still passes, since the new not-found message still contains "no such item".

The first check failed on a type error in the new test. The expected refusal is now typed as const. The repository's own node_modules/typescript reports the original error and is clean with the fix. The bun x tsc run before the first push had not caught it.

Checked: the repository's tsc --noEmit is clean. These suites pass:

  • test/feed-interact.test.ts, test/mcp-feed-dismiss.test.ts, test/feed.test.ts, test/openapi-oa-feed.test.ts, test/mcp-size.test.ts
  • the mcp, mcp-tools, mcp-docs, api-errors, openapi, cross-origin-mutations, feed-choose, share, watches, feed-page and offline suites

Only the new test file was re-run after the type fix, since that was the only change. scripts/fast.ts named 929 test files for this change, far over its 5-minute budget, so I picked these suites instead.

To try it, POST { "item": "ask:look:x", "kind": "muted", "app": "alice/secret" } as someone who cannot read that repository. /api/v1/feed/interactions and MCP feed_dismiss both answer not_found: no such item, or it is not visible to you.

🤖 Generated with Claude Code

Changes requested — before the latest push

This does what #7524 asked. readInteraction in src/feed/interact.ts is now the one check both doors call. The route keeps its 422/404 statuses and the error codes its API description lists. MCP feed_dismiss drops its own invalid code and its own whitespace handling. The new test calls interactionTarget directly and sends the same refused inputs to both doors. A private event, an unknown event and an item with an unreadable app are all refused with the same words, so nothing shows that a private item exists. Security holds: identity still comes from requireScope/ctx.auth and the MCP user, the body is still read through body(), and nothing is built from user text. On the preview / answers 200, and both /api/v1/feed/interactions and MCP feed_dismiss refuse a guest with unauthorized.

One thing to fix, and it is the only one: dead code. After this change nothing calls isInteractionKind in src/core/interactions.ts. The route was its only caller (grep -rn isInteractionKind src test finds just the definition). Delete the export. The kind check now lives in readInteraction (kinds.find(...)), and leaving the old guard there means the kind rule is written in two places.

commented

The route and MCP feed_dismiss now refuse a feed act through one function, readInteraction in src/feed/interact.ts, and give the same error code and words. A new test/feed-interact.test.ts covers interactionTarget directly and calls both doors with the same refused inputs.

The fault the test found: the two doors disagreed. Both already called interactionTarget, but each checked the input itself and refused in its own words:

  • Error codes: the route said invalid_item, invalid_kind or not_found ("no such resource here…"). The tool said invalid for both a bad item and a bad kind, and not_found: no such item.
  • Whitespace: the tool trimmed its strings and the route did not, so " event:x " was accepted by one door and refused by the other.

The fix is in src/feed/interact.ts. readInteraction(db, viewer, { item, kind, app }, kinds) checks the item id, the kind and the target, in that order. It returns the refusal as data: invalid_item, invalid_kind, or not_found with NO_SUCH_ITEM ("no such item, or it is not visible to you"). The route and the tool both call it. The route keeps its status codes (422 and 404) and the error codes its API description lists. The tool now uses those codes too. Both doors trim item and app.

isInteractionKind is deleted from src/core/interactions.ts, as review asked. Nothing called it once the route moved to readInteraction, so the kind rule is now written only there.

One difference is kept on purpose. Each door passes its own list of kinds. The route records every act a card reports (opened, dwelled, …), while the tool only dismisses or mutes. So a kind outside a door's list is invalid_kind at both, but the message names that door's own kinds. The test checks that opened is accepted by the route and refused by the tool.

What the new test covers:

  • An event on a repository the viewer can read resolves to its repository and actor.
  • An event the viewer cannot read is refused exactly as an unknown event is, even when it names a readable app.
  • An item that is not an event needs a readable owner/name app; empty, partial, extra-segment, missing and private apps are all refused.
  • A kind outside dismissed or muted is refused, and so is a bad item id.
  • For each refused input, the HTTP route and MCP feed_dismiss give the same code and message, and both accept the same good input.

No existing test changed; test/mcp-feed-dismiss.test.ts still passes, since the new not-found message still contains "no such item".

Checked: the repository's own tsc --noEmit is clean. These suites pass:

  • test/feed-interact.test.ts, test/mcp-feed-dismiss.test.ts, test/feed.test.ts, test/openapi-oa-feed.test.ts
  • before the last two commits: test/mcp-size.test.ts and the mcp, mcp-tools, mcp-docs, api-errors, openapi, cross-origin-mutations, feed-choose, share, watches, feed-page and offline suites

The last two commits only add as const to the new test and delete the unused export.

To try it, POST { "item": "ask:look:x", "kind": "muted", "app": "alice/secret" } as someone who cannot read that repository. /api/v1/feed/interactions and MCP feed_dismiss both answer not_found: no such item, or it is not visible to you.

🤖 Generated with Claude Code

Approved

Earlier ask met: isInteractionKind is gone from src/core/interactions.ts. Its only callers were the route lines this pull replaces, and nothing else in src or test imports it, so the kind rule now lives only in readInteraction. ITEM_ID and str stay imported in src/http/feed.ts because other routes still use them (lines 132 and 304). The only other change since c76c27b is as const on the expected refusal in the test, which fixes the type error the check reported. The preview at this head serves / with 200, and POST /api/v1/feed/interactions with no identity answers 401, so the write is still gated. Nothing new to ask. Approved.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs