feat(service,features,web): the account's integrations on oRPC

Merged
CH
1d ago
+1365-1217

W5 of the oRPC migration. This moves account/integrations and its settle, connect, tools, cancel, disconnect and rename routes onto the hosted oRPC router, server and client together. account/integrations/connected is the OAuth browser callback, a 303 redirect, and stays on its HttpRouter group unchanged. settle was missing from the plan's list; the orchestrator approved including it. HostedIntegrationsClient on main now calls it for a cookie caller.

What changed

  • Contract. New packages/features/src/contract/integrations.ts, registered under the integrations key.
    • The provider routes declare /api/account/integrations/{provider}/<action>.
    • The POSTs use inputStructure: "detailed", because a POST's query (connection, add) is not merged into compact input. tools (a GET) stays compact.
    • provider is read as a plain string, so an unknown provider is still the service's 404 not-found and not a 400.
    • Every output is its wire schema flipped, except the statuses read and settle. Those declare the desktop's lenient read schema (integrationStatusesReadSchema, moved here from the client), so a newer service's status or provider still reads on an older desktop, as before.
  • Server. New integrations-procedures.ts carries the old endpoints' logic nearly unchanged.
    • Caller middleware sits on the resource implementer (hosted.integrations.use(accountMiddleware(…))), so it runs before input validation.
    • integrations-app.ts now holds only the connected endpoint and what both modules share (seams, sign-in window, Connect's sign-in result).
    • HostedRouterSeams gains IntegrationsProcedureSeams, and DEPLOYMENT_SEAMS spreads integrationsAppSeams().
  • Body limits (shared with W3–W17).
    • oRPC's documented RequestLimitHandlerPlugin in orpc/handler.ts, at HOSTED_BODY_CEILING_BYTES (4.5 MB). That is Vercel's own body limit and above the largest current cap (a task Start, about 4.2 MB). The plugin's PAYLOAD_TOO_LARGE is encoded as 413 {"error":"request-too-large"}.
    • The per-procedure bodyLimit and exactInput / exactBodyInput helpers are W4's (#3900), which landed first; this PR uses them as they are.
    • Rename uses bodyLimit(2048) and exactInput over a detailed input that still reads its query. That is exactBodyInput's shape plus a typed query, since the account a rename is about is in its query.
  • Client. integration-client.ts asks every call through askHosted. unavailable maps to UNAVAILABLE and everything else to UNANSWERED, as before. The readAnswer copy and the five path builders in service-paths.ts are deleted.
  • Rewrites. The five provider rows are marked orpc: true; api-rewrites.json and vercel.json are regenerated.

Tests

  • integration-client.test.ts passes unchanged over fakeCloudApi, including the exact query strings and main's new cookie-settle cases.
  • integrations-app.test.ts (30 cases) now drives the oRPC router for every migrated path, and the old group for connected. The assertions are untouched; only the harness and the request paths changed (/api/account/integrations/slack/connect instead of the flattened …/connect?provider=slack). The harness now sends content-type: application/json with a body.
  • New tests:
    • Refusals byte for byte: 401, 404, 405 and 503, each bare and with no cache-control.
    • An unsigned caller sending a malformed body is still answered 401.
    • A rename over its cap answers 413, and one with an excess key answers 400.
    • handler.test.ts: a streamed body past the ceiling with no stated length answers 413.
    • orpc-route.test.ts: a parameterized integrations path goes through the committed table, the dispatcher and the deployed route (401, and 405 on the wrong method).
  • models-procedures.test.ts now builds hostedFetch({ models: modelsProcedures(…) }), following the shared convention.

Known wire differences (accepted for every sibling PR)

  • Ordering. The caller is now resolved before the provider is read. An unsigned request to an unknown provider gets 401 where it used to get 404. Signed-in callers see the same answers as before.
  • Content-type and non-JSON bodies. A rename body sent without content-type: application/json, or one that is not JSON at all, is refused 400 before the procedure runs. Every client of ours sends the header.
  • Chunked bodies. A rename body sent chunked with no content-length is bounded by the handler-wide ceiling, then refused by its schema (400), not 413.

Checks run locally

  • Typecheck: features, service, host, web.
  • Vitest: the features, host and service projects, and the web project (all passing).
  • pnpm lint, pnpm knip, pnpm discarded-effect, scripts/repository-checks.sh, and the function-rewrites.ts --check.
  • pnpm check:affected was not run (it gets OOM-killed in the sandbox); CI runs the whole.

No JSON-schema golden changed.

Made with Luke


Devin Review