feat(credentials): saved credentials for 21 token-and-host blocks on one host-aware check factory

Merged
WA
1d ago
+4058-1229

Summary

  • 21 more blocks get a saved credential and a credential-only version. This time the connection is a token plus its host, region, or account ID: Algolia, Azure DevOps, Convex, Devin, Dynatrace, Email Bison, Grafana, Infisical, Jotform, Jupyter (jupyter_v3), Logfire, Mailgun, New Relic, Okta, Plane, Supabase, Thrive, Tinybird, Twilio SMS and Twilio Voice (one shared Twilio credential), and Upstash.

  • One check factory for every vendor. tokenCheckValidator gains:

    • domain and workspaceId normalizers: the normalized domain is what the credential stores and what runs use;
    • an egress profile, which sends checks to a user-chosen host through the same SSRF-guarded egress the tools use (no redirects, size cap, ENOTFOUND → site_not_found);
    • errorBodyEchoesToken, which keeps a vendor's token-echoing error body out of logs (Tinybird);
    • acceptsBody can name the credential from the verified reply.

    Shared normalizeHttpBaseUrl, allowedDomain and refuseUrlCredentials helpers replace the per-vendor URL and region rules.

    GitLab moves onto it, so every token check is a short declaration in validators/token-checks.ts.

  • Every host-deciding param comes from the credential.

    • A typed host is normalized the way the tools build their URLs, then verified at that host.
    • A region or account ID is checked before any request, so a value that could steer the host or path is refused before the key is sent.
    • The registry contract check enforces this for every successor.
  • The connect form sends URL-shaped domains as typed. Instance and deployment URLs keep their scheme and path, and the server check normalizes them. Before this, it stripped them to a bare host, which broke Convex and dropped Jupyter and Grafana sub-paths.

  • Azure DevOps URLs are traversal-safe. Every project-scoped URL goes through one builder that runs the organization, project and ids through safeUrlPathSegment, so a project like ../../other-org can't carry a saved token into another organization. A legitimate name, including one with spaces or pasted already encoded, builds the same URL as before.

  • Integration skills teach the pattern. add-integration, add-block, add-tools and validate-integration now describe token credentials (descriptor, check declaration, the shared policy mapping every host-deciding param) and credential-only successors for blocks that take a secret inline.

  • New credentials default to a name with their domain or account ID (e.g. "Azure DevOps (contoso)"), and the audit log records it.

  • Databricks keyless tokens last up to an hour. Databricks gives its access token the same expiry as Sim's subject token, so long Genie polls no longer lose auth. The issuer caps any requested lifetime at an hour.

Release-audit fixes (credentials as a whole)

  • No stray host in user params. A policy that maps credential fields owns them, so the generic domain/instanceUrl/apiDomain/cloudId fills no longer write a stored host into a same-named param. Before this, Thrive create/update user sent its region host as the user's domain.
  • Agent tool schema. With a saved credential selected, the model is neither asked for nor allowed to send what the credential supplies (Azure DevOps organization, Infisical base URL).
  • Mistyped host under Bun. ENOTFOUND is read from the error or its cause, so a mistyped host or account id gives site_not_found under Bun too, not a retryable outage.
  • Saved secrets need a bound tool. A service-account secret is released only to a tool bound to its provider. The token route refuses a bare credential ID.
  • Unsafe typed URLs refused. The connect check refuses a typed instance URL with a non-http(s) scheme or embedded credentials (Email Bison, Jupyter, Logfire).
  • Docs and copy. Successor docs (Jotform, Dynatrace, Logfire, Plane, Infisical, Fathom, Supabase), self-hosting, the AWS keyless step, and ElevenLabs/Instantly/Okta copy.

Existing workflows don't change

  • Legacy blocks only gain hideFromToolbar: true and sunset; jupyter_v2 becomes legacy for jupyter_v3, and jupyter (v1) now points at jupyter_v3.
  • A parity run dumped every block and tool at staging and at this branch, using the real registries, and compared them:
    • Blocks: builder output for every operation, serializer output, and missing-field errors.
    • Tools: request URL, method, headers and body.
    • Everything is identical except the new successors, the legacy flags, the added oauth (required: false) on these tools, and Azure DevOps URLs (below).
  • All 74 existing tokenCheckValidator validators were run through 11 reply scenarios at both revisions: 814 cases, no differences.

Held back

Each of these doesn't fit the pattern cleanly:

  • Optional key or keyless servers: Dagster, Temporal, Qdrant (its operation values don't match its tool IDs either).
  • Two secrets or multiple auth modes: Splunk, PostHog, Zendesk, Elasticsearch.
  • No side-effect-free check: Google AppSheet, RevenueCat.
  • A2A: no reliable key check.
  • Host or IDs that don't fit: LogRocket (key plus two IDs and a host), Pinecone (host chosen per call).
  • PitchBook: its check may cost credits; unverified.
  • Downdetector: the auth model is unclear.
  • GitHub: it already has OAuth repository and app-installation connections, so a token credential needs a design pass of its own.

Type of Change

  • New feature

Testing

  • New tests:

    • Factory: guarded egress at the normalized host, normalizer refusal before any request, site_not_found, no token in error details.
    • A table of vendors that refuse a host, region or account ID that could steer the request.
    • Every user-chosen-host vendor refuses credentials embedded in the URL before any request.
    • Azure DevOps anonymous-identity refusal.
    • GitLab display name and audit domain.
    • Issuer lifetime cap.
    • Serializer contract rows for all 21 blocks.

    Each guard was reverted to watch its test go red.

  • Targeted Vitest (99 files, 1,760 tests), test:scripts, bun run lint, type-check, check-block-registry.ts origin/staging (incl. the saved-credential contract), and the canvas-sentences, docs, integration-catalog, deployment-config, block-successors, tool-metadata, boundaries, client-boundary, tool-registry-boundary, react-query, test-pattern, test-types, comment, utils, explicit-any, file-names, source-conventions and unused-exports audits.

  • Parallel audits: per block (all 21), the factory and every existing validator, backwards compatibility (parity dump), and a coverage sweep of every block that still takes an inline secret. Fake keys were sent to each public vendor endpoint to confirm it rejects them.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing (new tests pass the test-audit authoring gate)
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)