Skip to content

release: to prod#1354

Merged
joelorzet merged 13 commits into
prodfrom
staging
May 23, 2026
Merged

release: to prod#1354
joelorzet merged 13 commits into
prodfrom
staging

Conversation

@joelorzet

Copy link
Copy Markdown

No description provided.

joelorzet added 13 commits May 22, 2026 09:17
…paid plans

Introduces an extensible feature registry under lib/features/ as the single
source of truth for plan-gated capabilities. Adding a new gated feature is
a one-entry diff to the FEATURES dictionary.

Server enforcement runs at every workflow execution surface (manual,
webhook, MCP, internal, scheduler, block, event triggers) and at workflow
save time. The executor records a failed workflow_executions row instead
of silent-dropping blocked SQS triggers so users see the blocked run in
their dashboard. Default-deny when no org context.

Client surface keeps gated actions visible in the picker with a diamond
icon and grayed style; click opens an upgrade dialog linking to the public
pricing page. A toast warns when the user opens an existing workflow that
contains gated nodes. Feature snapshot invalidates on org switch and the
toast signature includes the plan so plan changes re-evaluate without a
reload.
- Gate POST /api/workflows/create against the feature registry
- Gate POST /api/workflows/import (JSON import path)
- Gate POST /api/workflows/[id]/duplicate (copies gated nodes into new workflow)
- Gate POST /api/workflows/current (auto-save of the scratch workflow)
- Gate POST /api/workflows/[id]/claim (anonymous workflow into org)

Invalidate the client feature snapshot when the auth session user changes
via a new FeatureSessionInvalidator mounted in the root layout, so the
module-level cache no longer leaks across sign-out/sign-in transitions.

Make useFeatures fall open client-side when /api/features is unreachable
(fallback to an enterprise-equivalent snapshot) so a network blip does
not lock the entire UI behind gated diamonds. Server-side guards remain
authoritative; any actual save or execute attempt is still rejected.

Reorder declarations in action-config.tsx so isActionLocked is declared
before handleCategoryChange, removing the TDZ-only-works-at-runtime
fragility flagged in review.
The feature route guard module starts with import "server-only", which
explodes when imported in a vitest environment that does not also mock
the server-only package. Both x402-call-route.test.ts and
mcp-meta-tools.test.ts already mock every other server-only dependency
of the MCP call route handler; add a matching mock for
enforceWorkflowFeatures so the route can be imported in test context.
…d routes

Six integration tests covered routes that now invoke enforceWorkflowFeatures
from @/lib/features/route-guard. That module starts with import "server-only",
so importing the route handler in a vitest environment without a matching
mock pulls in the real server-only package and throws. Add the mock alongside
the existing route-level mocks (validateWorkflowIntegrations, getOrgContext,
enforceExecutionLimit, etc.) to keep the test surface consistent.
Hold the listener function in a useRef so the same closure registers and
unregisters across React 18 strict-mode double mounts. Without the ref,
each render created a new listener that the cleanup phase would chase,
leaving Set membership consistent but the closure identity unstable.
…l-open

Replaces the fail-open snapshot fallback with a 250ms retry loop capped
at 60 attempts. UI stays in loading state during a network outage so a
disabled or kill-switched feature can never appear unlocked in the
picker. Server-side guards remain authoritative regardless.

The previous fail-open behavior could expose features with enabled=false
in the registry whenever /api/features was unreachable, even though the
kill switch is supposed to be authoritative.
Bail out of FeatureSessionInvalidator while useSession reports isPending.
The previous logic recorded the loading-state null sentinel as the first
observed user id, so the real id arriving a tick later looked like a
sign-in transition and triggered an unnecessary snapshot invalidate.
Adds an optional errorMessage to enforceWorkflowFeatures so each route
can phrase the upgrade prompt with its own context, then uses that from
the claim route to explain the destination-org-plan situation instead
of the generic "requires a paid plan" wording.

A user moving an anonymous workflow built during a Pro trial into a
Free org would otherwise see a 402 with no hint that the gate is on the
destination org, not the workflow itself.
Adds BILLING to the ErrorCategory enum and to the workflow_executions
errorCategory TypeScript narrowing, then switches the executor to use
errorCategory: "billing", errorType: "user" when blocking an SQS
trigger because the org's plan does not include a workflow feature.

Previously these rows were tagged "configuration", which conflates a
malformed workflow with a plan-insufficient one. With a distinct
"billing" value, dashboards, oncall, and customer success can filter
plan-block events cleanly. No DB migration: the column is text with
TypeScript-level narrowing only.
feat: gate Database Query, HTTP Request, Code and Webhook actions to paid plans
@joelorzet joelorzet requested review from a team, OleksandrUA, eskp and suisuss and removed request for a team May 22, 2026 22:46
@joelorzet joelorzet merged commit cc32a6d into prod May 23, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants