Files
sim/apps/docs/lib/redirects.ts
T
Waleed c8f559ae77 fix(workflows,connectors): close pre-merge audit findings (#6783)
* fix(workflows,connectors): close pre-merge audit findings

Recover subblock values orphaned by the id renames in this release, and
stop truncated knowledge-base listings from reporting themselves complete.

- Add operation-scoped subblock id migrations so a saved workflow's stored
  value survives a rename. Cloudflare create/update DNS record, ServiceNow
  read record, and Okta deactivate/delete previously lost their stored value:
  the create path substituted a seeded default (an A record where the user
  chose CNAME, and unproxied where they chose proxied), and the update path
  silently no-opped while reporting success. A migration is used rather than
  a legacy-id fallback so no subblock id carries two value spaces at runtime.
- Webflow, Zendesk: a listing that stops for a reason the connector cannot
  rule out now reports as capped instead of exhausted. A malformed envelope,
  an unfollowable continuation link, or an absent collection list previously
  read as a complete listing and let deletion reconciliation hard-delete
  every document past the truncation point.
- Sentry: pin the listing window in the request rather than inheriting the
  server default, so the range cannot silently narrow into hard deletes.
- Fork sync: a parent re-pick no longer writes a blank over a hidden optional
  dependent's stored target value, and a required field stays on screen once
  it is filled. Add hook-level coverage for the submitted payload.
- Fork file copy: a file whose name is already taken in a reused target folder
  is de-duplicated instead of dropped.
- Delete an orphaned Shopify OAuth route that built a credential from unsigned
  cookies. It had no writer, no caller, and no inbound link.
- Tailwind: drop two content globs that scanned 5.4k files to emit one unused
  rule, keeping the ones that fix brand tile icon color.
- Correct the API route-count baseline, add an Evernote docs redirect, align
  library copy with the language rules, and fix a stale turbo filter.

* fix(connectors,forking): trim the audit fixes to their minimum

A legitimacy review found several changes closed no live defect, and two
introduced problems of their own.

- Zendesk: narrow the cursor fix to a signal change. Treating a missing meta
  envelope as truncation had also made the walk follow links.next and keep
  paginating, and the ticket cursor has no page-depth valve, so a source
  advertising a next page with no meta could loop without terminating. The
  page-fetch set now matches the previous behavior; only the flag is new.
- Zendesk: drop the search next_page branch. The existing count check already
  caps every case where a missing key could lose documents.
- Webflow: drop the empty-collections flag. The sync engine already blocks the
  first sync on an empty listing and reconciles only when a second sync agrees,
  which handles a transient fault better and still removes documents when a
  source is genuinely emptied. The flag short-circuited that and suppressed
  reconciliation permanently. Restore the previous loud failure on a non-array
  envelope, and drop the unreachable collection-id filter.
- Webflow: soften a docstring that claimed pagination.total is always present.
  It is documented optional, so its absence proves nothing either way and
  treating it as unprovable truncation is the fail-safe reading.
- Sentry: drop the pinned statsPeriod. Sentry's issue search floors every query
  at 90 days in the executor regardless of the request, and the endpoint this
  release moved away from hit the same floor, so there was no window to close.
  Keep the tests and the docstring recording that.
- Fork copy: drop the renamed counter, which no caller reads.
- Repair check-block-registry, which stopped exempting migrated subblock ids
  when the migration map became an array — `in` was testing array indices.
- Drop mdx from a Tailwind content glob that emits nothing, and loosen an
  exact compiled-SQL assertion to the invariant it was pinning.

* fix(migrations): keep a ServiceNow write body off the read projection

Review findings from the first round.

- A legacy ServiceNow block can hold a Create/Update Record JSON body under
  `fields` while its stored operation is Read Records: the id served both value
  spaces before the rename, and a subblock value is not cleared when the
  operation changes. The scoped migration moved that body onto `readFields`,
  where it would reach the wire as sysparm_fields. Migration entries can now
  carry a `whenValue` predicate for the case where the stored operation alone
  cannot separate two value spaces, and the ServiceNow entry uses it to move
  only a plausible comma-separated projection.
- Type the fork copy test harness instead of using `any`, without weakening it:
  every predicate shape it does not model still throws rather than matching.
- Correct the dependent-omission comments. Omitting a parent-invalidated field
  preserves the target's stored value on Save and across an undo, where the
  parent nets out unchanged; on a Sync the written state is source-derived, so
  what it prevents there is an explicit blank reaching the fields the remap's
  clearing pass does not cover, nested tool params in particular.

Okta's migration scope is left as-is: `okta_remove_user_from_app` and the
sendEmail split shipped in the same release, so no saved block can hold legacy
state for it, and widening the scope would promote an activation-era value onto
the deactivation switch. Tests document the boundary.

* chore(forking): move the fork-sync changes to their own PR

The dependent-omission fix and the fork file-copy de-duplication are reviewed
separately in #6787. They are the only changes here that overlap #6776, and
they carry their own design tradeoff, so they should not ride along with the
unrelated audit fixes in this PR.

* fix(migrations): separate a ServiceNow write body from a projection by parsing

The guard tested for a `{` or `[` prefix, so a stored scalar body — `true`,
`"short_description"`, `42` — read as a field list and was promoted onto
`readFields`, where it would go out as sysparm_fields.

A Create/Update Record body is JSON and a projection is a bare comma-separated
field list, which is never valid JSON, so parsing is the whole test rather than
a guess at its opening character. Ambiguity still resolves to "not a
projection", leaving the value where the Create/Update control owns it.

* test(connectors,credentials): tie two assertions to what they actually prove

- Webflow: a non-array collections envelope reaching `for...of` throws, which
  is the intended loud failure. Assert the spec-mandated TypeError plus a
  single request and no write-back, rather than matching V8's wording.
- Credentials: the second guard test cannot observe "not deleted" — the proxy
  driver replays canned rows — so name it for what it does verify, that the
  reference check carries no workspace predicate and an empty RETURNING logs
  nothing. Making the driver decide the outcome would fake the database.
- Drop `vi.importActual`; a plain `drizzle-orm/pg-proxy` import works now that
  `drizzle-orm` is un-mocked.

* fix(migrations): identify a ServiceNow projection by its own shape

Recognising a write body was the wrong way round. A saved body is not always
well-formed: it can be a half-typed draft or carry an unquoted block reference,
so neither "opens with a brace" nor "fails to parse as JSON" identifies one —
and a body misread as a projection is moved to readFields with its original key
dropped, losing the draft.

Match the projection instead: a comma-separated list of ServiceNow field names,
which are word characters plus the dot of a dotted walk. A brace, quote, colon,
angle bracket or interior space fails that shape. Parsing then removes the bare
scalars that satisfy it by accident.
2026-08-17 16:02:58 -07:00

365 lines
14 KiB
TypeScript

import type { NextConfig } from 'next'
/** The shape Next expects back from `next.config.ts`'s `redirects()`. */
type DocsRedirect = Awaited<ReturnType<NonNullable<NextConfig['redirects']>>>[number]
/**
* Every redirect the docs site serves, in match order — Next applies the first
* matching rule.
*
* This lives outside `next.config.ts` so it can be read without evaluating that
* module. `createMDX()` runs at import time and bundles `source.config.ts`
* against `process.cwd()`, so importing the config from the root Vitest project
* fails with `The entry point "source.config.ts" cannot be marked as external`.
* `scripts/openapi/docs-redirects.test.ts` reads this array directly to keep the
* `/api-reference/` rules honest against the specs.
*
* The whole table lives here rather than only the `/api-reference/` block: Next
* applies the first matching rule, so splitting one ordered list across two
* modules would make match order an emergent property of two files.
*/
export const DOCS_REDIRECTS: DocsRedirect[] = [
{
source: '/',
destination: '/introduction',
permanent: true,
},
// building-agents/agents merged into the building-agents overview
{ source: '/building-agents/agents', destination: '/agents', permanent: true },
// form deployment removed
{ source: '/deployment/form', destination: '/workflows/deployment', permanent: true },
// copilot deprecated and removed
{ source: '/copilot', destination: '/chat', permanent: true },
{ source: '/copilot/:path*', destination: '/chat', permanent: true },
// mothership/* renamed to chat/* — the surface is "Chat", the agent is "Sim"
{ source: '/mothership', destination: '/chat', permanent: true },
{ source: '/mothership/:path*', destination: '/chat/:path*', permanent: true },
// connections/* and variables/* collapsed into single pages under workflows/
{ source: '/connections', destination: '/workflows/connections', permanent: true },
{ source: '/connections/:path*', destination: '/workflows/connections', permanent: true },
{ source: '/variables', destination: '/workflows/variables', permanent: true },
{ source: '/variables/:path*', destination: '/workflows/variables', permanent: true },
// capabilities/* renamed to building-agents/*
{ source: '/capabilities', destination: '/agents', permanent: true },
{ source: '/capabilities/agents', destination: '/agents', permanent: true },
{
source: '/capabilities/choosing',
destination: '/agents/choosing',
permanent: true,
},
// execution/* was broken up; redirect old URLs to their new homes
{ source: '/execution', destination: '/workflows', permanent: true },
{ source: '/execution/index', destination: '/workflows', permanent: true },
{ source: '/execution/basics', destination: '/workflows/how-it-runs', permanent: true },
{ source: '/execution/files', destination: '/files/passing-files', permanent: true },
{ source: '/execution/logging', destination: '/logs-debugging/logging', permanent: true },
{ source: '/execution/costs', destination: '/platform/costs', permanent: true },
{ source: '/execution/api', destination: '/api-reference/getting-started', permanent: true },
{
source: '/execution/api-deployment',
destination: '/workflows/deployment/api',
permanent: true,
},
{ source: '/execution/chat', destination: '/workflows/deployment/chat', permanent: true },
// Points at the final page, not at `/deployment/form` — that is itself a
// redirect source above, and Next does not chain rules internally.
{ source: '/execution/form', destination: '/workflows/deployment', permanent: true },
{
source: '/mcp/deploy-workflows',
destination: '/workflows/deployment/mcp',
permanent: true,
},
// building-agents section renamed to agents; mcp and skills folded into it
{ source: '/building-agents', destination: '/agents', permanent: true },
{ source: '/building-agents/:path*', destination: '/agents/:path*', permanent: true },
{ source: '/mcp', destination: '/agents/mcp', permanent: true },
{ source: '/skills', destination: '/agents/skills', permanent: true },
// tools/ + triggers/<service> unified into per-service integrations/ pages.
// Specific moves first (Next applies the first matching redirect):
{
source: '/tools/custom-tools',
destination: '/agents/custom-tools',
permanent: true,
},
// evernote integration page removed; without this the /tools/:slug rule below
// would permanently redirect /tools/evernote into a 404.
{ source: '/tools/evernote', destination: '/integrations', permanent: true },
{ source: '/integrations/evernote', destination: '/integrations', permanent: true },
{ source: '/tools', destination: '/integrations', permanent: true },
{ source: '/tools/:slug', destination: '/integrations/:slug', permanent: true },
// Old blocks/triggers index pages were folded into the workflows overview.
// Native trigger pages (/triggers/start|schedule|webhook|rss|table) still exist.
{ source: '/blocks', destination: '/workflows#blocks', permanent: true },
{ source: '/triggers', destination: '/workflows#triggers', permanent: true },
// Integration trigger pages: provider slug differs from the block type for a few.
{
source: '/triggers/jsm',
destination: '/integrations/jira_service_management',
permanent: true,
},
{
source: '/triggers/google-calendar',
destination: '/integrations/google_calendar',
permanent: true,
},
{
source: '/triggers/google-drive',
destination: '/integrations/google_drive',
permanent: true,
},
{
source: '/triggers/google-sheets',
destination: '/integrations/google_sheets',
permanent: true,
},
{
source: '/triggers/microsoft-teams',
destination: '/integrations/microsoft_teams',
permanent: true,
},
{
source:
'/triggers/:slug(airtable|ashby|attio|azure_devops|calcom|calendly|circleback|confluence|emailbison|fathom|fireflies|github|gmail|gong|google_forms|grain|greenhouse|hubspot|imap|intercom|jira|lemlist|linear|monday|notion|outlook|resend|salesforce|sendblue|servicenow|slack|stripe|telegram|twilio_voice|typeform|vercel|webflow|whatsapp|zoom)',
destination: '/integrations/:slug',
permanent: true,
},
// URL structure now mirrors the sidebar: sections own their pages.
{ source: '/blocks/:slug', destination: '/workflows/blocks/:slug', permanent: true },
{
source: '/triggers/:slug(start|schedule|webhook|rss|table|sim)',
destination: '/workflows/triggers/:slug',
permanent: true,
},
{ source: '/deployment', destination: '/workflows/deployment', permanent: true },
{
source: '/deployment/:path*',
destination: '/workflows/deployment/:path*',
permanent: true,
},
{ source: '/mailer', destination: '/chat/mailer', permanent: true },
{ source: '/credentials', destination: '/platform/credentials', permanent: true },
{
source: '/credentials/:path*',
destination: '/platform/credentials',
permanent: true,
},
{ source: '/permissions', destination: '/platform/permissions', permanent: true },
{
source: '/permissions/:path*',
destination: '/platform/permissions',
permanent: true,
},
{ source: '/workspaces/fundamentals', destination: '/platform/workspaces', permanent: true },
{
source: '/workspaces/:slug(organization|permissions|credentials)',
destination: '/platform/:slug',
permanent: true,
},
{ source: '/costs', destination: '/platform/costs', permanent: true },
{ source: '/self-hosting', destination: '/platform/self-hosting', permanent: true },
{
source: '/self-hosting/:path*',
destination: '/platform/self-hosting/:path*',
permanent: true,
},
{ source: '/enterprise', destination: '/platform/enterprise', permanent: true },
{
source: '/enterprise/:path*',
destination: '/platform/enterprise/:path*',
permanent: true,
},
/**
* Continuity for the `/api-reference/` operation pages retired when the docs
* moved from the single v1 `openapi.json` to the seven code-first v2 specs.
*
* Every source below was a live, sitemap-submitted page whose slug no longer
* exists in the generated set. Sources are unprefixed and have no locale
* variants because generated pages mount only under the `en` base directory;
* `scripts/openapi/docs-redirects.test.ts` documents and implements the slug
* derivation.
*
* `permanent: true` (308) is reserved for a true 1:1 successor: same operation,
* renamed. A 308 is cached indefinitely and is effectively unrecallable, so
* anything that collapses two pages into one, changes the identifier model, or
* lands on a merely adjacent operation uses `permanent: false` (307).
*
* `scripts/openapi/docs-redirects.test.ts` pins both halves of the invariant
* against the specs: no source may shadow a live slug, and every destination
* must resolve.
*/
// Pure operationId renames — same path and method, v1 -> v2.
{
source: '/api-reference/audit-logs/getAuditLogDetails',
destination: '/api-reference/audit-logs/getAuditLog',
permanent: true,
},
{
source: '/api-reference/knowledge-bases/listDocuments',
destination: '/api-reference/knowledge-bases/listKnowledgeDocuments',
permanent: true,
},
{
source: '/api-reference/knowledge-bases/getDocument',
destination: '/api-reference/knowledge-bases/getKnowledgeDocument',
permanent: true,
},
{
source: '/api-reference/knowledge-bases/deleteDocument',
destination: '/api-reference/knowledge-bases/deleteKnowledgeDocument',
permanent: true,
},
{
source: '/api-reference/knowledge-bases/uploadDocument',
destination: '/api-reference/knowledge-bases/uploadKnowledgeDocument',
permanent: true,
},
{
source: '/api-reference/knowledge-bases/searchKnowledgeBase',
destination: '/api-reference/knowledge-bases/searchKnowledge',
permanent: true,
},
{
source: '/api-reference/logs/queryLogs',
destination: '/api-reference/logs/listLogs',
permanent: true,
},
{
source: '/api-reference/tables/listRows',
destination: '/api-reference/tables/listTableRows',
permanent: true,
},
{
source: '/api-reference/tables/getRow',
destination: '/api-reference/tables/getTableRow',
permanent: true,
},
{
source: '/api-reference/tables/insertRows',
destination: '/api-reference/tables/createTableRows',
permanent: true,
},
{
source: '/api-reference/tables/updateRow',
destination: '/api-reference/tables/updateTableRow',
permanent: true,
},
// v1 `PUT /rows` and v2 `PATCH /rows` are both the filter-predicate bulk update.
{
source: '/api-reference/tables/updateRows',
destination: '/api-reference/tables/updateTableRows',
permanent: true,
},
{
source: '/api-reference/tables/deleteRow',
destination: '/api-reference/tables/deleteTableRow',
permanent: true,
},
{
source: '/api-reference/tables/deleteRows',
destination: '/api-reference/tables/deleteTableRows',
permanent: true,
},
{
source: '/api-reference/tables/upsertRow',
destination: '/api-reference/tables/upsertTableRow',
permanent: true,
},
{
source: '/api-reference/tables/addColumn',
destination: '/api-reference/tables/addTableColumn',
permanent: true,
},
{
source: '/api-reference/tables/updateColumn',
destination: '/api-reference/tables/updateTableColumn',
permanent: true,
},
{
source: '/api-reference/tables/deleteColumn',
destination: '/api-reference/tables/deleteTableColumn',
permanent: true,
},
{
source: '/api-reference/workflows/executeWorkflow',
destination: '/api-reference/workflows/executeWorkflowV2',
permanent: true,
},
{
source: '/api-reference/workflows/getWorkflowExecution',
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
permanent: true,
},
{
source: '/api-reference/workflows/cancelExecution',
destination: '/api-reference/workflow-runs/cancelRunV2',
permanent: true,
},
// Approximations — 307 so a better destination stays reachable later.
// v1 took multipart up to 100MB; v2 `createFile` takes inline UTF-8/base64
// and defers larger payloads to an upload session.
{
source: '/api-reference/files/uploadFile',
destination: '/api-reference/files/createFile',
permanent: false,
},
// The human-in-the-loop tag is retired: pause state is now a field on the
// run resource rather than its own endpoint family.
{
source: '/api-reference/human-in-the-loop/listPausedExecutions',
destination: '/api-reference/workflow-runs/listWorkflowRunsV2',
permanent: false,
},
{
source: '/api-reference/human-in-the-loop/getPausedExecution',
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
permanent: false,
},
{
source: '/api-reference/human-in-the-loop/getPausedExecutionByResumePath',
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
permanent: false,
},
{
source: '/api-reference/human-in-the-loop/getPauseContext',
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
permanent: false,
},
{
source: '/api-reference/human-in-the-loop/resumeExecution',
destination: '/api-reference/workflow-runs/resumeWorkflowRunV2',
permanent: false,
},
// Two v1 identifiers (log id, execution id) collapse onto one v2 runId.
{
source: '/api-reference/logs/getLogDetails',
destination: '/api-reference/logs/getLog',
permanent: false,
},
{
source: '/api-reference/logs/getExecutionDetails',
destination: '/api-reference/logs/getLog',
permanent: false,
},
// v1 updated a batch of rows by id; v2 has no by-id batch, only the predicate form.
{
source: '/api-reference/tables/batchUpdateRows',
destination: '/api-reference/tables/updateTableRows',
permanent: false,
},
// v1 reported rate limits alongside spend and storage; v2 billing status
// carries plan, credit allowance, and storage quota but not rate limits.
{
source: '/api-reference/usage/getUsageLimits',
destination: '/api-reference/billing/getBillingStatus',
permanent: false,
},
// v1 polled a jobId; the v2 queue receipt returns a `statusUrl` pointing at
// this run endpoint, which is the successor poll target.
{
source: '/api-reference/workflows/getJobStatus',
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
permanent: false,
},
]