Conversation
…her modal When minting or revoking an org API key fails a role/MFA guard, the client popped the dialog and either opened the Settings overlay or routed to the step-up page. That drops the user onto an unrelated modal mid-action. Surface the guard error as a toast and leave the user on the current screen (the create dialog, or the key list after the confirm dialog closes).
Manual and webhook runs already stamped executedWorkflowHash so a run resolves to its workflow_history version by content hash. MCP calls and all executor-driven triggers (scheduled, block, event) did not, leaving those executions unlinked to any version. Stamp the hash at every execution insert: - MCP call route: hash the definition it loads to run. - internal executions route: hash at phantom/direct-run creation. - executor: hash the definition it actually loads, on both phantom upgrade and the no-phantom fallback insert (the run-time authority). Update the phantom-upgrade test for the new helper argument and assert the hash lands on the upgraded row.
The catch-all direct-execution route /api/execute/[...slug] (backing the agent-callable execute_protocol_action MCP tool) sent protocol write actions straight to writeContractCore, which signs and broadcasts a real tx from the org wallet. Unlike every other direct write path (transfer, contract-call, check-and-execute), it never enforced the plan execution limit, never checked for a wallet, never reserved against the daily spend cap, and created no directExecutions audit row -- so an API key could run unlimited, unmetered, unlogged on-chain writes. Wrap the write branch with enforceExecutionLimit + requireWallet + checkAndReserveExecution and record the execution (markRunning + complete/ fail), mirroring contract-call. Reads stay ungated.
The session-minting path in /api/user/verify-ip compared the request IP to the one pinned in the pending_ip_verify cookie with a !currentRawIp short-circuit, so a null IP made ipMatch true. In production a null IP means the request reached the origin without CF-Connecting-IP (direct-origin / Cloudflare bypass), which let an attacker holding the cookie plus valid factors finish the session mint on a foreign network, defeating the IP binding this endpoint exists to enforce. Extract the decision into isVerifyIpMatch and fail closed in production when the IP cannot be resolved; local dev (no CF header) still passes.
The authorize page resolved the token's org from the cookie-derived active
session at click time and never showed which org the token would scope to. A
multi-org user who switched their active org in another tab between viewing the
page and clicking Approve would silently mint a token for an unintended org.
Render the resolved org on the consent screen ("Authorizing access to ..."),
bind its id into the approve form, and reject in handleApprove when the org
resolved at submit time no longer matches the one the user consented to
(isConsentOrgMismatch). A missing bound id stays backward compatible.
…/sign On the Tempo (MPP) charge path the fund-moving amount and recipient live inside the server-signed serialized challenge, while the outer caller-supplied paymentChallenge fields are 0/empty. verifyWorkflowBinding only equality-checks the outer fields (base-only), so an HMAC-compromised caller could (a) pair a cheap cover-workflow slug with an expensive challenge for a different workflow to under-reserve the daily cap (F-011), and (b) keep the outer amount at 0 so the risk classifier saw 0 and never escalated a >=$50 charge to the ask/human tier (F-012). Deserialize the challenge up front, cross-check the inner amount + recipient against the workflow binding (same equality the base/x402 path enforces), and feed the verified inner amount to classifyRisk. The ask-tier approval binding + stored payload are enriched with the inner recipient/amount so the /approve re-derivation stays consistent. Proof-intent challenges are unchanged. Also stubs server-only in the sign-route integration suite so it loads under the node test env (matches the unit-test convention).
…ir item markOverageRecordsPaid stamped providerInvoiceId on EVERY billed, unattributed overage record for an org whenever any invoice was paid -- so an unrelated standalone or manual invoice could claim all pending overage, and the follow-up scanAndCreateDebt early-return (invoice set + paid) then silently waived it. An org could run large overage and clear it against a tiny payment. Resolve each candidate record's invoice via the same getInvoiceForItem path scanAndCreateDebt uses and stamp only the records whose item belongs to this exact invoice; records with no item link are left unattributed for the debt scan to re-resolve. The billing provider is threaded through from handleBillingEvent rather than re-fetched.
…cles Both POST /api/auth/finish-credential-signup and POST /api/auth/strict-signin/start verify a credential password for any caller and return a distinguishable 401 vs 200 (strict-signin/start signs non-TOTP users straight in on a correct password). Neither sits under the Better Auth catch-all, so neither inherited rate limiting, leaving two unauthenticated password oracles. Add a shared in-memory limiter (per-email 5/15m + per-IP 20/15m, mirroring the forgot-password limiter) consulted before any password comparison on BOTH routes. Buckets are shared so an attacker cannot get a fresh allowance by hopping between the two endpoints for the same target.
…s-on-screen fix(ui): keep API key dialog on guard failure instead of opening another modal
The version timeline (GET /history) and historical-snapshot fetch (GET ?version=N) were gated on isOrgAdmin, so a regular member got 403 on the right-sidebar History tab of a workflow they already have full access to. Version history is the edit history of that workflow, not the org-wide security audit, so any current org member with full access can read it. The org activity/audit feed stays admin/owner only. Drop the isOrgAdmin requirement from both routes, keeping the existing org-membership (hasFullAccess) gate.
…ures Pre-broadcast gating returns (plan limit, wallet, spend cap) were recorded as success or failed via protocolActionDisposition, freezing the error on the idempotency key so a retry replayed it even after the cause was resolved. Thread the idempotency outcome into executeProtocolAction and record each return explicitly: gating failures release, broadcast results finalize as success or failed. Mirrors the contract-call route.
…n-linkage fix(executor): stamp workflow version hash on every execution trigger
…ccess fix(api): let org members read a workflow's version history
feat: allow using API key for Blockscout (KEEP-806)
…p-fails-open-when-cf-connecting-ip-absent-ip fix(security): fail closed in verify-ip when client IP is unresolved
The Activity nav item opened the org-wide security audit in an overlay. Point it at a real route (/activity) so it deep-links and behaves like Analytics/Earnings. The page reuses the existing audit hook, filter bar, and feed; the ActivityOverlay component stays in place for any other callers. Admin/owner only, with a gate for non-admins who hit the URL directly.
…separators - ActivityFeed gains opt-in fillHeight (flex column, list scrolls, pager pinned to the bottom) and syncPageToUrl (?page=N seed + reflect), both off by default so the overlay and per-resource feeds are unchanged. - usePaginatedResource accepts initialPage and skips the first reset so a URL-seeded page survives mount. - Activity page chrome is a bounded full-height flex column so the pager sits at the very bottom. - Date groups are the separators (underlined section headers); rows no longer carry per-row divider lines.
- usePaginatedResource only resets to page 1 on an actual resetKey change (tracked via a prev-key ref), not on mount or StrictMode's double-invoked effect, so a URL-seeded initialPage survives the first render. - useAuditActivity accepts initialPageSize; the page seeds it from ?size (validated against the allowed options). - ActivityFeed's URL writer now syncs both ?page and ?size in one effect, so the two never race; size is omitted at the default to keep the URL clean. A shared/refreshed /activity?page=4&size=25 now loads page 4 at size 25.
Page size was part of the feed's reset key, so changing it snapped back to page 1. Drop limit from the reset key and instead refetch the current page when the size changes, so the page number is preserved (items shift, which is acceptable).
…redential-signup-is-an-unrate-limited-password fix(security): rate-limit finish-credential-signup password oracle
…ists When a fetch returns a server-clamped page (the requested page is past the end -- e.g. page 7 at size 5 becomes out of range at size 25, or rows were deleted), adopt meta.page and refetch so we land on the last page with real items instead of an empty page. In-range pages are unchanged.
Non-openable rows (no chevron) let their label run into the chevron's slot, so their right edge didn't line up with openable rows. Reserve the chevron width with an invisible placeholder when there's no chevron.
Search: - /api/security/audit accepts ?q and filters across actor label, action, resource id, the actor's current name/email, matching workflow names, and the request IP. Every comparison is a parameterized ILIKE (Drizzle binds the value) with the user's LIKE wildcards escaped, capped at 200 chars -- no SQL injection or pattern abuse. IP is matched through the metadata JSONB ->> accessor so it runs in Postgres, not by scanning rows in JS. - useAuditActivity debounces the input 500ms before it drives the query; a new search resets to page 1. - Search box added to the activity page header. Access: - The /activity page now redirects anyone who is not an org admin/owner to home once auth + membership resolve, instead of rendering a gate.
The feed's URL writer now reflects the debounced search as ?q (dropped when empty), alongside page/size, and the page seeds the search box from ?q on load. A shared/refreshed /activity?q=... restores the search.
…write-actions-bypass-execution-guards fix(security): gate protocol write actions through execution guards
Search only resolved workflow names among resource names, so 'discord' missed integration events and key changes. Resolve matching integration names (and provider type) plus org and personal API key names to id sets and match them via resourceId, alongside the existing workflow-name match.
…arge-amount-validation fix(security): validate Tempo MPP inner charge amount and recipient
feat(activity): move org activity to a dedicated page with search
…thorize-org-binding fix(security): show and bind the consent org on the OAuth authorize page
…records-paid-attribution fix(security): attribute overage records to the invoice that owns their item
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.