v0.7.27: document tags improvements, grok 4.5, forking, custom blocks, library, typescript 7, jupyter notebook, o11y#5531
v0.7.27: document tags improvements, grok 4.5, forking, custom blocks, library, typescript 7, jupyter notebook, o11y#5531waleedlatif1 wants to merge 27 commits into
Conversation
…#5495) * feat(pii): add opt-in GLiNER NER engine (PII_ENGINE), device-agnostic Swap the 4 NER entity types (PERSON/LOCATION/NRP/DATE_TIME) to a single multilingual GLiNER zero-shot model when PII_ENGINE=gliner; spaCy stays the default and all ~36 regex/checksum recognizers are identical on both engines. Device-agnostic via PII_DEVICE / cuda auto-detect — same code on Fargate CPU now and EC2-GPU later. - engines.py: side-effect-free builders; SharedModelGLiNERRecognizer loads ONE model shared across the 5 per-language instances and restricts labels to the entities it owns; small spaCy models keep tokenization/lemmas for the regex recognizers; fail-fast on the lean image - pii.Dockerfile: multi-stage — default target unchanged (lean spaCy); --target gliner is a superset (torch CPU + gliner + baked model) where both engines work; gliner-gpu scaffold for the GPU fleet - CI publishes the gliner variant (:staging-gliner/:latest-gliner, amd64) - Helm: pii.engine / pii.device values wired to PII_ENGINE/PII_DEVICE - scripts/bench_engines.py: throughput + NER-parity diff harness - tests: unit (mocked GLiNER) + in-image integration for both engines Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Up3F97mjCH9HCj1pX4J8VJ * refactor(pii): ship both engines in one image — engine is a pure env flip Collapse the gliner build target into the single pii image: spaCy lg models, torch (CPU), gliner, and the baked GLiNER weights all ship in it, so PII_ENGINE switches engines with no image swap and no tag matrix. CI reverts to the single pii build (no -gliner tags). The GPU variant becomes the same Dockerfile built with --build-arg TORCH_INDEX_URL=.../cu128. Image grows ~6.1GB -> ~9.6GB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Up3F97mjCH9HCj1pX4J8VJ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* chore(tables): remove tables-fractional-ordering feature flag * improvement(tables): drop dead position tracking from paste undo path * improvement(tables): drop dead position from create-row undo records
…166/#167) (#5508) Patches a high-severity stored XSS in the oidc-provider and mcp plugins via javascript:/data: redirect_uri schemes. Also bumps @better-auth/sso and @better-auth/stripe to matching 1.6.13 peers. Patch-only release (1.6.11 -> 1.6.12 -> 1.6.13), no breaking changes in either changelog.
* improvement(knowledge): open document tags from row context menu The document row context menu had a Tags entry that only navigated to the document detail page, requiring another click through the breadcrumb dropdown to actually edit tags. It now opens the tag editor directly, and shows even when the document has no tags yet so a first tag can be added. * fix(knowledge): gate document tags menu item on edit permission Matches the existing disableRename/disableDelete/disableToggleEnabled pattern; the document detail breadcrumb already hides its Tags entry for non-editors the same way. * fix(knowledge): derive tags modal document data from live list cache The modal was fed a frozen document snapshot taken at right-click time. After a save, the mutation only invalidates the single-document and KB-detail queries (not the documents list query), so the modal's own sync effect rebuilt tags from the stale snapshot and could revert or drop the just-saved value. Track only the document id and look it up from the same documents array updateDocument() patches, so the modal always sees current data.
* refactor(utils): consolidate duplicated helpers onto @sim/utils Replaces ~90 hand-rolled reimplementations of error-message extraction, postgres error-code checks, sleep, Math.random, retry/backoff, object filtering/omission, noop, string truncation, date/time formatting, email normalization, and plain-object type guards with the shared @sim/utils exports. Wires check:utils into CI (test-build.yml) so these patterns don't regress. * fix(test): mock @sim/utils/random instead of Math.random in schedule-execute tests The schedules/execute route previously used Math.random() for jitter delay; this consolidation PR switched it to randomInt() from @sim/utils/random, which is backed by crypto.getRandomValues() rather than Math.random(). The route.test.ts spies on Math.random() no longer had any effect, so jitter became real random delay instead of the deterministic 0ms the tests expect, causing intermittent 10s timeouts in CI. * fix(retry): preserve uncapped Retry-After comparison in tools/index.ts parseRetryAfter() caps its return value at 30s by default. tools/index.ts compares the parsed Retry-After against a caller-configured maxDelayMs to decide whether to skip a retry entirely -- capping before that comparison silently defeats the skip check whenever maxDelayMs is configured above 30s, since a Retry-After between 30s and maxDelayMs would incorrectly look "within limits" and get retried instead of skipped (caught by Cursor Bugbot). Added an optional maxMs param (default unchanged) so tools/index.ts can request the raw, uncapped value for its own comparison while backoffWithJitter still clamps the actual sleep duration to maxDelayMs. Added a regression test covering maxDelayMs > 30s. * fix(utils): fall back to Intl-resolved abbreviation for unmapped timezones getTimezoneAbbreviation only covered 9 hardcoded IANA zones and returned the raw IANA string for everything else, degrading schedule descriptions for zones like Europe/Berlin or America/Toronto (caught by Greptile). The deleted local implementation in schedules/utils.ts resolved any valid IANA timezone generically via Intl.DateTimeFormat's short timeZoneName. Restore that as a fallback so only genuinely invalid timezone strings return themselves unchanged.
* feat(providers): add xAI grok-4.5 model * fix(providers): correct grok-4.5 release date to API-availability date
…es (#5513) * fix(comparison): correct stale and unsourced claims on comparison pages - Remove stale "Templates" references from the LangChain page and related blog posts (feature was removed platform-wide) - Re-verify every sourced claim across all comparison-page profiles (Sim's own facts and all 20 competitor profiles) against live sources; fix dead links, renamed products, outdated figures, and claims no longer supported by their citation - Fix a stale sidebar permissions reference in the enterprise access-control docs * fix(comparison): use consistent URL format for n8n AI Workflow Builder citation
…, public apis, external mcp servers/tools, special subblocks (#5505) * bad checkpoint * stash * add unlink, move UI to settings pages * fix edge cases * remove dead code * remove migration 0255 ahead of staging merge (regenerated after) * regenerate workflow_mcp_server enum migration on top of staging (0257) * fix tests * more tests * address comments * consolidate fork migrations into 0257 (enum value + activity metadata indexes) * acquire MCP server locks before reads in attachment reconcile (TOCTOU) * move into ee folder + ui perm gates * use randomInt from @sim/utils/random for chat identifier suffix --------- Co-authored-by: Siddharth Ganesan <siddharthganesan@gmail.com>
…agement (#5507) * fix(settings): let workspace admins reach the Custom blocks settings page The Custom blocks page lives in the Enterprise nav section, which the sidebar filter hides unless the user is an org admin/owner. But the server authz for managing custom blocks is source-workspace admin (hasWorkspaceAdminAccess), not org admin — so a workspace admin who could manage a block via the API couldn't even reach the page. Add an `allowNonOrgAdmin` nav flag that exempts an item from the org admin/owner requirement (the plan/hosted entitlement still applies; the page enforces its own per-resource authz), and set it on custom-blocks. Other Enterprise items (access control, audit logs, SSO, data retention/drains, whitelabeling) stay org-admin-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ddpeoz81CSqYQ73Dzf7yo * feat(custom-blocks): any org member views; create gated on workspace admin Split visibility from management to match the org-wide nature of custom blocks: - View: any org member (the nav item is now plan-gated, not org-admin). - Create: the "Create block" action shows only when the user is admin of a workspace in the current org, matching the publish route's source-workspace admin authz. - Source picker: lists only workspaces the user administers (in the current org); the default selection snaps to an eligible workspace when the current one isn't one they can publish from. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ddpeoz81CSqYQ73Dzf7yo * feat(custom-blocks): read-only detail for non-managers + fill image icon - Add the source workspaceId to the block wire type and gate the detail view on it: a viewer who isn't an admin of the block's source workspace gets a read-only view — no Save/Discard, no Delete, all fields disabled. Matches the server authz (edit/delete require source-workspace admin), so the UI no longer dangles buttons that 403. - SettingsResourceRow: add an opt-in `iconFill` so uploaded image icons fill the tile edge-to-edge instead of clamping to 20px; glyph svgs still normalize to 20px. Custom blocks list opts in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ddpeoz81CSqYQ73Dzf7yo * fix(custom-blocks): include workspaceId in publishCustomBlock return The publish path builds a CustomBlockWithInputs literal; it was missing the workspaceId field added to the interface, breaking the type check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ddpeoz81CSqYQ73Dzf7yo * fix(custom-blocks): don't flag create as dirty when source auto-snaps The create dirty-check compared selectedWorkspaceId against the URL workspace, but the source picker now auto-snaps to the first workspace the user can publish from — which may differ. Compare against that eligible default so opening/discarding the create flow doesn't show a false unsaved-changes state. (Bugbot) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ddpeoz81CSqYQ73Dzf7yo --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) * refactor(blog): split AEO/GEO content into a new /library section Blog was mixing hand-written editorial posts with listicle/comparison/how-to posts optimized for answer engines. Extract a generic content engine (lib/content) shared by both sections, move the 6 AEO/GEO posts into a new /library route tree, and keep /blog editorial-only. - lib/content: generic registry factory, MDX components, SEO builders - lib/blog + lib/library: thin per-section instantiations over the shared engine - app/(landing)/library: mirrors the blog route tree via shared Content*Page components - app/sitemap.ts, app/robots.ts, navbar: updated for the new section - next.config.ts: permanent redirects from moved /blog/<slug> URLs to /library/<slug> * fix(blog): thread route id through buildAuthorMetadata for canonical URL Author pages for unknown/unmatched ids fell back to author?.id, which is undefined when no post matches — collapsing canonical/OG URLs to {basePath}/authors/ instead of {basePath}/authors/{id}. Pass the route id explicitly so canonical always resolves correctly. * fix(blog): address Greptile review — RSS lastBuildDate, shared author cache, dead-export docs - RSS lastBuildDate now uses updated ?? date so edits after publication are reflected, matching the per-item pubDate semantics (blog + library) - Author JSON is now cached once per authorsDir at module scope instead of once per registry instantiation, since blog and library point at the same directory - Documented getNavPosts as reserved-but-unwired, matching the existing PLATFORM_MENU/SOLUTIONS_MENU convention in the navbar * fix(blog): dedupe featured posts from ContentIndexPage pagination pool A featured post older than POSTS_PER_PAGE would appear both on page 1's featured row (sorted to the front) and again on its natural date-sorted page, since only page 1 excluded featured posts from the remaining list. Carve featured posts out of the paginated pool up front so pagination stays consistent and duplicate-free across all pages. * fix(blog): RSS lastBuildDate reflects the whole feed, not just the newest item lastBuildDate only looked at items[0]'s updated/date, so revising an older post already in the feed wouldn't advance it. Extract the max-across-posts logic sitemap.ts already had into a shared lib/content/utils helper (latestModified) and use it in both RSS routes and the sitemap, so a revision to any feed item is reflected.
…stored title (#5501) * fix(workflow): render agent tool chip names from static sources, not stored title Tool entries in an agent block's inputs carry a mutable `title` in workflow state, which copilot edits (edit_workflow) could rewrite to change what the UI displays. Derive chip names from static/canonical sources instead: the block registry name for integration tools (raw type id if unregistered), the custom-tool record over the stored snapshot, the live MCP tool name, and a static literal for workflow-as-tool. The stored `title` still persists in state; the UI just no longer reads it for registry-backed tools. Covers both render paths: the expanded tool-input chips and the collapsed canvas summary (resolveToolsLabel, also used by workflow preview). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(workflow): cover workflow-as-tool and custom-tool fallbacks in static naming Address review findings: resolveToolsLabel now returns the static 'Workflow' label for workflow/workflow_input entries instead of falling through to the stored title, matching the panel chip; the panel's custom-tool name priority now matches resolveToolsLabel (record title, record schema function name, then stored title); and the display-name comment no longer overstates the guarantee. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(workflow): resolve panel chip names via unfiltered block registry The panel resolved integration tool names through toolBlocks, which is permission- and toolbar-filtered, so a stored tool whose type was filtered out of the picker fell back to the raw type id while the canvas summary (getBlock) still showed the registry name. Use getBlock directly for the display name so both paths agree; toolBlocks still drives the chip icon, params, and picker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(workflow): resolve MCP tool names from live server data in canvas summaries resolveToolsLabel preferred the stored title for mcp entries, so canvas and preview summaries could show a state-edited name while the config panel showed the live MCP tool name. Accept an optional mcpTools list (matched by toolId, same as the panel) and pass the already-fetched MCP tool data from workflow-block; the stored title remains the fallback while server data is unavailable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(workflow): single shared tool-name resolver; fix overlay, fallback, and search regressions Consolidate the per-surface name logic into one resolveStoredToolName in display.ts (canonical source -> stored title -> raw type id) used by the panel chips, the canvas summary, and the search indexer, fixing the regressions the per-renderer patches introduced: - custom-block tools (async registry overlay) no longer render raw custom_block_<uuid> ids pre-hydration or after deletion; the canvas memo and panel picker now subscribe to the overlay version so labels recompute when custom blocks hydrate - unresolvable block types fall back to the stored title before the raw type id, instead of always showing the id - the search indexer now indexes the resolved display name (the title entry was already read-only), so search text and highlights match what the chips render instead of the mutable stored title - redundant hardcoded 'Workflow' labels removed (registry names cover workflow/workflow_input); MCP lookups use a shared Map keyed by composite tool id instead of per-entry array scans; panel chip chrome falls back to the unfiltered registry for picker-hidden types Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(workflow): thread custom-tool and MCP data into the search indexer; fix blockConfigs type The search index now resolves tool names with the same data as the chip renderers: the single caller passes customTools and a live MCP name map into indexWorkflowSearchMatches, so indexed names match the panel for custom-tool references and MCP entries, not just registry-backed types. Also declare the optional name on WorkflowSearchIndexerOptions blockConfigs entries, fixing the next build type error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(workflow): recompute search index when the custom-block overlay hydrates The matches memo resolves tool names through getBlock, so it must carry the overlay version dep like the other getBlock-derived memos; without it, find-in-workflow could index stale custom-block names until an unrelated dep changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* docs(enterprise): add Custom Blocks page Document publishing a deployed workflow as a reusable org-wide block: publishing flow, common uses, using a block, managing, and self-hosted setup, with UI screenshots. Register in the enterprise sidebar and wire the settings docsLink. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvLuS5o1eFjBBhEC4PytcA * docs(enterprise): drop explicit Enterprise framing, note access control The page's Enterprise placement is implicit, so remove the "Enterprise feature" callout and plan mentions. Add that custom blocks can be allowlisted per permission group via Access Control. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvLuS5o1eFjBBhEC4PytcA * docs(enterprise): remove Self-hosted setup section from Custom Blocks Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvLuS5o1eFjBBhEC4PytcA --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tural bugs (#5517) * fix(rich-markdown-editor): don't split a list when deleting an empty item Backspacing an emptied list item in the middle of a list used ProseMirror's default lift, which pulls the item out into a top-level paragraph — splitting one list into two and stranding an empty paragraph (a visible gap). Backspace at the start of an empty list item now joins into the previous block instead, removing the item and keeping a single list. Covers bullet, ordered, and task lists. * fix(rich-markdown-editor): generalize empty-wrapped-block boundary keys Replace the joinBackward empty-list-item Backspace branch with a walk-up-and-delete that removes the whole emptied wrapper, and extend it to task items and blockquotes. joinBackward left a stray empty paragraph inside the previous item (flat lists) or no-op'd entirely on nested items (leaving them stuck); blockquotes were never handled and split in two. Add an Enter handler so an empty non-trailing list/task item is removed rather than split into two lists around a stranded, non-round-trippable empty paragraph; a trailing empty item still exits the list via the default. Covers bullet/ordered/task/blockquote at every position; non-empty items and the double-Enter list-exit are unaffected. * fix(rich-markdown-editor): isolate verbatim block nodes from boundary joins footnoteDef and rawHtmlBlock hold exact source text but were neither isolating nor atom, so a single Backspace/Delete at their boundary let ProseMirror's default join merge their raw markdown into an adjacent paragraph as HTML-escaped prose — silently destroying the node and corrupting saved markdown. Mark the block variants isolating so boundary keys can't cross their edge. Round-trip and in-place editing are unchanged. * fix(rich-markdown-editor): escape leading block markers in paragraph serialization A paragraph beginning with #, -, +, 1., 1), or a bare --- serialized unescaped, so it silently re-parsed into a heading / list / thematic break on the next load (reachable via type-a-marker then undo). The upstream serializer escapes inline delimiters (* _ ` [ ] ~, so * bullets and > quotes already round-trip) but not these block-starting markers. BlockSafeParagraph wraps the paragraph renderer with a leading-marker guard; escaping is idempotent (parsing consumes the backslash) and never over-escapes non-markers like #hashtag or -5. * fix(rich-markdown-editor): strip leading paragraph indent; keep empty paragraphs -free A paragraph beginning with a 4-space/tab indent re-parsed as an indented code block on the next load. Leading whitespace never renders in a paragraph (CommonMark strips up to three leading spaces; four or more become code), so BlockSafeParagraph now strips it — lossless and idempotent. Composes with the existing leading-marker escaping (e.g. ' # x' → '\# x'). Also locks in that consecutive empty paragraphs serialize via blank lines rather than the upstream ' ' marker: replacing StarterKit's Paragraph already dropped the path, and the round-trip preserves the empty-paragraph count without tripping the read-only safety gate (which flags as a stable-loss pattern). Added tests for both.
…hook errors (#5518) * fix(ashby): repair broken idempotency dedup and clarify duplicate-webhook errors extractIdempotencyId looked for a webhookActionId field that does not exist anywhere in Ashby's webhook payload schema (confirmed against the live OpenAPI spec), so every delivery — including Ashby's own retries — got a fresh random dedup key and could re-execute workflows. Derive the key from the affected resource's id plus its updatedAt/decidedAt instead. Also surface a clear, actionable message when Ashby's webhook.create rejects a request as a duplicate (seen repeatedly in production logs, where the outbox retried the same failing subscription for ~23 minutes before dead-lettering) instead of the generic API error passthrough. Also add the interview stage `type` field to trigger outputs (present in Ashby's schema, useful for a stage-change trigger) and fix the employmentType description to match Ashby's actual enum values. * fix(ashby): correct offer status enum values in trigger output descriptions acceptanceStatus listed a non-existent "WaitingOnResponse" value and offerStatus was missing "WaitingOnApprovalDefinition" — both caught on a second pass re-checking every enum against Ashby's live OpenAPI spec. * fix(ashby): harden idempotency key against Greptile-flagged collision risks - Return null (skip dedup) when application.updatedAt is absent instead of collapsing to an empty string, which could collide two distinct events sharing an application id onto the same key. - Drop decidedAt from the offerCreate key. It's populated only after the fact, so including it gave a retry of the same delivery a different key than the original attempt once the candidate responded, defeating dedup. offer.id alone is already stable and unique per created offer. * fix(ashby): fall back to a content fingerprint instead of skipping dedup Returning null when application.updatedAt was missing avoided false collisions between distinct events, but also disabled retry dedup entirely for that payload shape — an Ashby retry would get a random key from the idempotency service's own fallback and re-run the workflow. Extract the fallback-fingerprint helper (sha256 of a stably-serialized payload) out of salesforce.ts into the shared providers/utils.ts, and use it in ashby.ts: identical retried bytes hash identically (dedup still works), while two genuinely different events hash differently (no false collision). * chore(ashby): remove inline comments, let names carry the intent * fix(ashby): fingerprint the full data payload, not just application Hashing only data.application missed other fields (e.g. offer on candidateHire) when updatedAt is absent, so two deliveries sharing an application snapshot but differing elsewhere in data could collide onto the same idempotency key. * fix(ashby): rename output field 'type' to 'stageType' to fix build TriggerOutput reserves the 'type' key for the output's own JSON type (e.g. 'string'), so a nested field literally named 'type' inside currentInterviewStage collided with that meta-field and broke the Record<string, TriggerOutput> cast — passing local type-check (which turbo was silently serving a stale cached pass for) but failing Next.js's build-time type check in CI. Renamed to stageType, matching the existing eventType-style convention used elsewhere for API fields literally called 'type'. * fix(ashby): rename currentInterviewStage.type to stageType in delivered data too Renaming the field in the output schema alone (to fix the TriggerOutput build collision) left a mismatch: Ashby's real payload still has currentInterviewStage.type, so a picker/expression using the schema's declared stageType would resolve to undefined at runtime. formatInput now renames the field in the actual delivered payload so it matches what the schema declares. * fix(ashby): fold offer id into the idempotency key when present When application.updatedAt is present, the key ignored sibling data fields — a candidateHire delivery carries both application and offer, so two deliveries sharing an application snapshot could collide even though they concern different offers. Append offer.id to the discriminator when present; it's the only sibling object Ashby's schema ever pairs with application.
- New "customBlocks" row in the platform section: whether a platform lets a builder publish a deployed workflow as a reusable, encapsulated block for org-wide reuse (Sim's new Custom Blocks feature) - Researched and sourced an accurate answer for all 20 competitors, distinguishing real equivalents from look-alikes (sub-workflow composition, custom code nodes, connector wrappers, templates)
* docs(forking): workspace forking * update comparison links
* chore(typescript): upgrade to TypeScript 7 (native Go compiler) Bumps typescript to ^7.0.2 across every workspace package. Full bun run type-check/lint/build/test all pass; apps/sim's type-check (the one needing an 8GB heap bump) drops from ~55s to ~7s wall time. Migration fixes required by TS7's stricter defaults: - baseUrl removed: drop it from 5 tsconfigs (paths already resolved relative to tsconfig dir, so behavior is unchanged) and prefix the one bare (non-relative) paths entry each in apps/sim and apps/realtime with './' - moduleResolution=node10 removed: switch packages/cli and packages/ts-sdk to "bundler", matching the rest of the monorepo - types now defaults to [] instead of auto-including every @types/* package: add "types": ["node"] to the shared base tsconfig (this is fundamentally a Node monorepo, so this restores prior behavior in one place instead of duplicating it per-package), add explicit @types/node deps to packages that now rely on it transitively via @sim/db/@sim/logger, and add "declare module '*.css'" to the two packages with plain (non-module) CSS side-effect imports that TS7's stricter checker now flags - packages/logger's isomorphic `typeof window` check no longer needs DOM lib in every consumer: replaced with `'window' in globalThis` - packages/testing and apps/realtime's fetch/DOM mocks need DOM lib where they're compiled, since they model the browser Fetch API - the `typescript` npm package no longer exports the classic Compiler API from its main entry (moved to unstable/ast subpaths); apps/sim's Function-block route used it at runtime to strip import statements from user code, so that one call site now uses Microsoft's official transition package, @typescript/typescript6 - Next.js 16.2.6's own TypeScript-detection heuristic hardcodes a path TS7 no longer ships, and its auto-install fallback assumes npm/pnpm; added @typescript/native-preview as a devDependency to apps/sim and apps/docs so Next detects a valid native compiler instead of trying (and failing) to auto-install one Not merging yet: TS 7.0.2 published today and is still inside this repo's bunfig.toml minimumReleaseAge (7-day) supply-chain gate, so `bun install` will fail for everyone until 2026-07-15. Opening this now to get it through review; hold the actual merge until then. * fix(typescript): address Greptile review findings on TS7 upgrade - packages/logger: 'window' in globalThis treats a shim that leaves globalThis.window explicitly undefined as browser-only, silently dropping production server logs. Restore the original typeof !== 'undefined' semantics via an inline cast instead, so it stays correct without requiring DOM lib in every consumer. - packages/ts-sdk, packages/cli: both are tsc-built, published as Node ESM (package.json "type": "module" with an "exports" map). "moduleResolution": "bundler" is too permissive for that target - it accepts import patterns (e.g. extensionless relative imports) that Node's actual ESM resolver rejects at runtime. Switch both to "module"/"moduleResolution": "nodenext", the correct pairing for a published Node ESM package. Verified real tsc builds (not just --noEmit) still succeed for both. * chore(bunfig): temporarily disable minimumReleaseAge gate for TS7 install TS 7.0.2 published today, still inside the 7-day gate. Lowering to 0 to unblock this merge; will restore to 604800 in an immediate follow-up commit right after merging.
…ng triggers (#5524) * fix(instantly): add idempotency dedup for webhook deliveries Instantly webhook deliveries had no extractIdempotencyId, so retried deliveries were never deduped. Instantly doesn't send a delivery-id header, so key on email_id when present (unique per email/reply) and fall back to a content-based key (event_type + campaign_id + lead_email + event timestamp) otherwise. Added instantly.test.ts covering auth, event matching, formatInput/output alignment, idempotency, and the subscription lifecycle. * fix(gitlab): add delivery idempotency and fix reserved type-field collision risk extractIdempotencyId was missing entirely, so GitLab's automatic webhook retries (triggered after timeouts/5xx, up to 4 consecutive failures before temporary disable) would re-execute the workflow as a brand-new event. Added a content-derived fallback key (checkout_sha for push, object id + updated_at otherwise) since GitLab's X-Gitlab-Event-UUID delivery header isn't available to extractIdempotencyId (body-only). Also renamed the Issue Hook payload's object_attributes.type (GitLab 17.2+ work item type) to work_item_type in formatInput and exposed it in buildGitLabIssueOutputs, since 'type' collides with the TriggerOutput meta-key. Added detailed_merge_status to the merge request outputs to match the deprecation of merge_status. Added apps/sim/lib/webhooks/providers/gitlab.test.ts covering verifyAuth, matchEvent, formatInput, and extractIdempotencyId. * fix(gmail): correct labels output type and add formatInput/schema alignment test The gmail_poller trigger declared its `labels` output as type: 'string' while the description and actual runtime value (email.labelIds) are a string array. Changed to type: 'json' to match the shape actually delivered, consistent with how other triggers type array-of-string fields. Added apps/sim/lib/webhooks/providers/gmail.test.ts covering gmailHandler.formatInput passthrough behavior and a regression check that every key formatInput can deliver on `email` matches a key declared in the trigger's output schema. * fix(linear): tighten replay window, dedupe HMAC verify, add idempotency fallback - Reuse the shared createHmacVerifier helper for Linear's signature check instead of hand-rolling header/secret validation. - Tighten the webhookTimestamp replay-protection skew from 5 minutes to 60 seconds, matching Linear's documented recommendation. - Add extractIdempotencyId as a content-based fallback (type:action:id: updatedAt) for when the Linear-Delivery header is unavailable. - Remove unused teamOutputs/stateOutputs dead code from triggers/linear/utils.ts. * fix(gitlab): preserve raw object_attributes.type alongside work_item_type formatInput previously spread the entire raw body, so object_attributes.type (GitLab 17.2+ work item type) was already reachable via a manual expression even though undocumented. The earlier fix renamed it to work_item_type in the delivered data too, which would silently break any pre-existing workflow referencing the raw path. Keep both keys in the delivered data — this is plain passthrough data, not schema-constrained, so there's no reserved-key collision risk in doing so; only the declared *schema* needs the work_item_type name. * fix(instantly): differentiate idempotency key by timestamp when email_id repeats Instantly's is_first field ("whether this is the first event of this type for the lead") only makes sense if the same event_type can fire more than once for the same email_id: every open of the same email, every click, and every reply in a thread all share one email_id (Instantly's reply_to_uuid). Keying idempotency on email_id alone collapsed all of those distinct, legitimate deliveries into a single dedup slot, so every open/click/reply after the first was silently dropped for the 7-day idempotency TTL. Append timestamp (fixed per event occurrence, confirmed not to regenerate on retry) to keep the key both retry-stable and unique per occurrence. * fix(linear): revert replay-window tightening — retry timestamp semantics unverified The prior pass tightened LINEAR_WEBHOOK_TIMESTAMP_SKEW_MS from 5 minutes to Linear's suggested 60 seconds, justified by the claim that webhookTimestamp is re-stamped fresh on every delivery/retry attempt. That claim is not stated anywhere in Linear's webhook docs (verified against the raw docs page directly, including the exact wording around retries and the webhookTimestamp field) — the docs only say it is "the time when the webhook was sent" with no mention of retry behavior. A third-party implementation guide explicitly recommends a 5-minute window instead of Linear's literal 60s suggestion, citing this same ambiguity. If webhookTimestamp is actually fixed to the original event time (not refreshed per attempt), a strict 60s window would silently and permanently reject every one of Linear's own 1hr/6hr retry deliveries following any transient outage on our side, since Linear gives up after 3 failed attempts. Idempotency dedup (Linear-Delivery header / extractIdempotencyId fallback) already prevents double-processing of replayed/retried deliveries within a wider window, so the extra replay-protection from matching the literal 60s suggestion is marginal next to the risk of dropping real business events. Reverting to the 5-minute window pending explicit confirmation from Linear on retry timestamp semantics. * fix(gitlab): include ref in push idempotency key to avoid branch-deletion collisions GitLab sets checkout_sha to null on branch/tag deletion, so the extractIdempotencyId fallback (checkout_sha || after) degenerated to the all-zeros SHA for every deletion. Without ref in the key, two unrelated branch deletions in the same project produced the same idempotency key, silently dropping the second (and any subsequent) legitimate trigger execution as a false duplicate. * fix(gmail): use 'array' not 'json' for poller labels output type The prior pass changed the gmail_poller trigger's `labels` output from 'string' to 'json', which is directionally correct (labelIds is a string array, not a string) but not the precise type. The codebase's own PrimitiveValueType union has a dedicated 'array' variant, used by sibling triggers with identical semantics (github issue/PR triggers, Jira triggers all declare their `labels` field as type: 'array'). Concretely, 'json' vs 'array' diverges in generateMockValue (apps/sim/lib/workflows/triggers/trigger-utils.ts), which backs the auto-generated "Event Payload Example" sample payload shown in the trigger config UI (apps/sim/triggers/index.ts, gated on trigger.id.includes('poller') — gmail_poller qualifies). 'json' mocks as a single generic object ({id, name, status}), actively misleading for a field that is always an array. 'array' mocks as a list, matching the true shape (Gmail API docs confirm labelIds is a string array on the Message resource). No behavioral difference in output-path validation/resolution (isPathInSchema, generateOutputPaths) since the field declares no items/properties either way — this is purely a type-declaration correctness fix, verified against Gmail API docs and cross-checked against every other output field in poller.ts (all match runtime shape). * docs(triggers): correct GitLab retry claim, convert inline comments to TSDoc GitLab does not automatically retry failed webhook deliveries — a failed request only counts toward auto-disabling the webhook (4 consecutive failures = temporary disable, 40 = permanent), and re-delivery only happens via a manual "Resend Request" (UI or API), which carries the same webhook-id/Idempotency-Key/X-Gitlab-Event-UUID headers as the original delivery. Those headers are already in the shared idempotency service's allowlist and checked ahead of extractIdempotencyId, so the body-based fallback matters for the rare case those headers are stripped in transit — not for "automatic retries" as previously stated. Verified directly against docs.gitlab.com. Also replaced inline `//` comments across the gitlab/gmail/linear/ instantly provider and trigger files with TSDoc blocks on the relevant declaration where the reasoning was non-obvious, or removed them outright where they only restated adjacent code. * fix(triggers): close two Greptile-flagged crash/collision gaps instantly: the timestamp-present branch was fixed for the email_id collision risk, but the no-timestamp fallback still keyed on bare email_id — same collision risk, unfixed. Return null instead so dedup is skipped rather than risking a false collision between repeat opens/ clicks/replies on the same email. linear: extractIdempotencyId cast `body` straight to a Record without checking it was actually an object first, so a JSON `null` body (no Linear-Delivery header available) would throw on `b.type` and turn a malformed payload into a webhook 500 instead of a clean skip. * fix(linear): guard matchEvent and formatInput against a null body Found during a final backwards-compat pass, same class of bug Greptile just caught in extractIdempotencyId: both cast body straight to a Record without checking it was actually an object, so a genuinely null/malformed body would throw instead of degrading gracefully. gitlab.ts (asRecord's `|| {}`) and instantly.ts (isRecordLike checks) already guard this everywhere; linear.ts was the one inconsistent provider. Uses the same isRecordLike helper as instantly.ts. * fix(gitlab): distinguish pipeline lifecycle transitions in idempotency key Pipeline Hook payloads have no updated_at field (confirmed against docs.gitlab.com — only Issue/Merge Request/Note hooks reliably include it), so every lifecycle transition of the same pipeline (pending -> running -> success/failed) collapsed onto the identical fallback key and later real transitions were skipped as duplicates. Falls back to status + finished_at/created_at when updated_at is absent.
… pages (#5522) * fix(landing): fix Core Web Vitals regressions across public marketing pages - root layout unconditionally rendered next-runtime-env's PublicEnvScript, which calls unstable_noStore() and silently forced every route in the app dynamic - marketing pages never got static/ISR caching despite their own revalidate. Gated it to self-hosted only; hosted now uses a static, build-time equivalent (app/_shell/public-env-script.tsx) - removed real pointer-drag handlers from the hero's decorative workflow animation (was draggable despite being aria-hidden) - disabled dragging/panning on the (currently unmounted) landing-preview ReactFlow canvas so it's static-by-default if it's ever wired in - lazy-mount the Product Demo section's duplicate HeroVisual instance via next/dynamic + IntersectionObserver instead of loading it eagerly below the fold - disabled Next.js Link prefetch on always-in-viewport /signup and /login CTAs (navbar, hero, mobile nav) so their JS isn't fetched on every pageview regardless of whether the visitor clicks - removed `unoptimized` from local blog/integration images (including the priority LCP image on every blog post), letting next/image serve resized AVIF/WebP instead of full-size originals * fix(landing): address review findings on the CWV PR - revert prefetch={false} on below-fold CTAs (cta.tsx, enterprise.tsx) - contradicts the prefetch-on-approach rule this PR itself documents - restore unoptimized on avatarUrl and the MDX body-image renderer, both of which can legitimately hold external URLs outside next.config.ts's image remotePatterns allow-list - simplify handleAnchors to a single block argument now that positions are static (the second "live position" argument was always identical to the first after the drag-handler removal) - extract the near-duplicate IntersectionObserver lazy-mount logic shared by landing-preview-mount.tsx and product-demo-visual-mount.tsx into a single apps/sim/app/(landing)/hooks/use-lazy-mount.ts hook - import next-runtime-env's own exported PUBLIC_ENV_KEY constant instead of a hardcoded string literal, and match its case-insensitive NEXT_PUBLIC_ filter exactly, removing any drift risk between the two implementations - drop plain inline comments with no TSDoc home in favor of relying on the existing TSDoc/CLAUDE.md documentation * fix(landing): carry the unoptimized-image fix onto the shared content-*-page components The blog/library split (#5516) moved the blog post/index/author JSX into shared ContentPostPage/ContentIndexPage/ContentAuthorPage components while this branch was in flight, so the original unoptimized removal (verified local-only ogImage paths for both blog and library content) needs to land on those shared components instead of the old per-route JSX. * fix(landing): document ogImage's local-path expectation, fix CLAUDE.md structure doc - add a one-line comment on ContentFrontmatterSchema.ogImage documenting that it's rendered without unoptimized and expects a local path, matching the existing avatarUrl comment convention (a reviewer noted ogImage's schema is technically unconstrained and seo.ts has an http-prefix branch, though all current content is local) - add the new hooks/ folder to the (landing) CLAUDE.md structure diagram and name use-lazy-mount.ts directly in the lazy-mount rule * fix(landing): escape </script> breakout in the static public-env script Greptile P1: a NEXT_PUBLIC_* value containing "</script>" would close the inline script early and could inject markup/script into every hosted page. Escape "<" in the serialized JSON before interpolating it, matching the standard JSON-in-script-tag safeguard.
* feat(chat): replace thinking indicator with gooey loader Promotes ThinkingLoader from the landing route group to the shared components/ui barrel and swaps it in for the 4-square color loader in the chat's PendingTagIndicator. Removes the now-dead animate-thinking-block keyframe/animation. * fix(chat): keep a stable Thinking… label instead of cycling phase text Greptile flagged that phase text repeatedly re-announces to assistive tech during a single pending stream; a static label avoids the repeated live-region updates while keeping the gooey shape morph.
…nstead (#5528) PR #5522 removed `unoptimized` from local blog/library/integration cover images, expecting next/image's runtime optimizer to serve resized AVIF/WebP. On staging, images broke entirely on /blog - the runtime optimizer is failing there (root cause still under investigation: _next/image requests aren't captured by the app's structured logger, so the underlying error wasn't visible in application logs). Reverting `unoptimized` immediately restores working images. In its place, this compresses the actual source files (mozjpeg quality 82 for JPEGs, palette PNG for PNGs) at their EXACT existing pixel dimensions - verified programmatically per-file (dimension mismatch aborts the write) and spot-checked visually. This gets the same bandwidth/LCP win the runtime optimizer was meant to provide, without depending on it: - 13 blog/library cover images + 3 author avatars + 1 brand logo - ~1.72MB -> ~1.02MB combined (~41% smaller), zero resolution change
… API docs (#5530) * fix(slack): guard webhook handlers against null/non-object bodies handleSlackChallenge, extractIdempotencyId, and formatInput cast the webhook body to Record<string, unknown> without checking for null first. handleProviderChallenges runs Slack's handleChallenge unconditionally on every webhook path before the webhook row is looked up, so a POST with a literal JSON `null` body crashed with a TypeError instead of degrading gracefully, unlike sibling providers (e.g. monday.ts) that already guard this case. Switch all three to isRecordLike, matching the pattern used by other providers. * fix(stripe): guard extractIdempotencyId against non-object bodies Bring the Stripe webhook provider in line with the isRecordLike convention used by other providers (linear, sendblue, instantly) so a null/non-object body degrades gracefully instead of throwing on property access. In practice the only caller already guards against non-object bodies, so this is defense-in-depth, not a live crash fix. Adds a colocated stripe.test.ts covering signature verification (valid/invalid/wrong-secret), event-type filtering, formatInput pass-through, and extractIdempotencyId including the null-body case. * fix(github): fix workflow_run event matching and formatInput/output-schema mismatch isGitHubEventMatch's eventMap was missing an entry for github_workflow_run, so unknown-trigger fallthrough caused that trigger to fire on every GitHub event type, not just workflow_run. The provider's formatInput did a raw passthrough of the webhook body, but the trigger output schemas rename GitHub's reserved `type` field (a TriggerOutput meta-key) to `user_type`/`owner_type`, and `description` to `repo_description` for the repository object. Since formatInput never performed those renames, the declared output fields never matched real delivered data. Added alias renaming (keeping the raw keys for back-compat, matching the existing GitLab work_item_type precedent) plus null-body guards in formatInput/matchEvent and a content-based extractIdempotencyId fallback. * fix(jira): dedupe trigger-type dropdown, guard against null webhook bodies - All 15 Jira trigger files hand-rolled their own selectedTriggerId dropdown subBlock instead of using the shared buildTriggerSubBlocks helper. Since blocks/blocks/jira.ts merges every trigger's subBlocks into one array, the Jira block ended up with 15 duplicate, unconditional selectedTriggerId dropdowns. Refactored every trigger to use buildTriggerSubBlocks, with includeDropdown only on the primary jira_issue_created trigger (matches the jsm sibling module's pattern). - Removed the dead, unused fieldFilters subBlock on issue_updated (never read by matchEvent/formatInput and has no analog in Jira's own webhook admin UI, unlike jqlFilter). - Guarded all `body as Record<string, unknown>` casts in the provider handler (matchEvent, formatInput, extractIdempotencyId) and in triggers/jira/utils.ts's extract* helpers with isRecordLike, so a malformed/scalar JSON body (e.g. literal `null`) degrades gracefully instead of throwing. - jira_webhook (generic, all-events) trigger's output schema was missing sprint/project/version keys that its formatInput branch already returns, and duplicated comment/worklog shapes that drifted from the shared builders (e.g. comment.body typed as plain string instead of ADF json). Now composed from the same buildXOutputs() helpers used by the dedicated triggers. Verified against live Atlassian webhook docs: X-Hub-Signature HMAC signing and the X-Atlassian-Webhook-Identifier header (stable across retries) are both real and already correctly implemented/allowlisted; no changes needed there. * fix(salesforce): correct setup instructions for Flow HTTP Callout auth Salesforce Flow's HTTP Callout action requires a Named Credential (and an External Credential for auth headers) pointing at the target URL — it cannot call an arbitrary URL with inline headers as the previous instructions implied. Update both the generic and per-event setup instructions to walk through creating the External/Named Credential first, and drop the inaccurate "connectivity checks" claim. * fix(hubspot): cap advanced filters to stay within HubSpot's per-group limit HubSpot's Search API rejects any filterGroup with more than 6 filters. buildUserFilters combines pipeline/stage/owner shortcuts with user-supplied JSON filters and spread them into both filter groups uncapped; Group B reserves 2 slots for the timestamp/id tie-break, so as few as 3 shortcuts plus 2 advanced filters silently broke every poll with an opaque 400 from HubSpot. Cap combined filters at 4 and warn when truncating. Added hubspot.test.ts covering buildUserFilters (shortcuts, JSON parsing, invalid-operator drop, malformed JSON, and the new cap) since the file had no prior test coverage. * test(zendesk): add handler tests for webhook trigger signature/idempotency/format-input Audited the Zendesk webhook trigger (signature verification, event matching, output-schema mapping, idempotency) against live Zendesk webhook docs and the repo's trigger conventions. No bugs found — the existing implementation already correctly uses base64 HMAC-SHA256 over timestamp+body (not hex), safeCompare, fail-closed auth, the native event-subscription payload shape (not the admin-configurable Trigger/Automation payload), and null-safe body handling. Added colocated tests to lock in this behavior, matching the gitlab/linear test pattern. * fix(microsoft-teams): persist subscription expiration, close auth/idempotency gaps - createSubscription never wrote subscriptionExpiration into providerConfig, so the renewal cron's `if (!expirationStr) continue` guard permanently skipped every Teams chat subscription — they silently expired after ~3 days (Graph's chatMessage max lifetime) and were never renewed. Persist it on both initial creation and the existing-subscription reuse path. - verifyAuth only checked HMAC when providerConfig.hmacSecret happened to be present, silently accepting unauthenticated requests for outgoing webhook triggers if it was ever missing. Fail closed instead. - extractIdempotencyId/enrichHeaders only handled Graph notification payloads (the `value` array shape), so outgoing webhook channel messages never got a stable idempotency key and fell back to a random one on every delivery. Extend to key off the Bot Framework Activity `id`, and guard the parsing with isRecordLike instead of an unchecked cast. - formatInput declared channelData.teamsTeamId/teamsChannelId in the trigger's output schema but never populated them (Teams doesn't send those as literal wire keys). Compute them from channelData.team.id / channelData.channel.id. - The block's triggers.available list only listed microsoftteams_webhook, hiding the chat subscription trigger from Copilot's block metadata tool and other consumers of that list. Add it, matching the Jira/Linear pattern for multi-trigger blocks. * fix(sentry): guard webhook handler against null/non-object body matchEvent cast body to Record<string, unknown> without a null check, so a validly-signed webhook delivery with a null (or non-object) JSON body threw inside the shared webhook loop, aborting processing for any other webhooks sharing the same path. formatInput/extractIdempotencyId already tolerated null via `|| {}`/optional chaining but didn't match the isRecordLike convention used by sibling providers (linear, sendblue, instantly). Aligned all three methods on isRecordLike and added regression tests for a null body. * fix(twilio): guard SMS and Voice webhook handlers against non-object body matchEvent, extractIdempotencyId, and formatInput cast body directly to Record<string, unknown> without checking it was actually an object, so a malformed or non-form-encoded request (e.g. JSON body "null") would throw instead of degrading gracefully. Use isRecordLike, matching the pattern already used in linear.ts/sendblue.ts/instantly.ts. * chore(slack): convert inline rationale comments to TSDoc Adversarial re-audit of the null-body-guard fix: no correctness issues found, backwards compatibility confirmed strictly additive. Per repo convention, converts non-TSDoc inline // comments in slack.ts to TSDoc blocks on the relevant declarations (formatSlackInteractive, extractIdempotencyId, formatInput) and drops two low-value inline comments in slack.test.ts that just restated adjacent assertions. * test(github): add negative alias test, remove extraneous inline comments Adversarial re-audit of the workflow_run/formatInput fix: eventMap now covers all 11 GitHub trigger IDs (verified exhaustively against every file in triggers/github/), and withGitHubUserTypeAliases only augments objects shaped like a GitHub user (login+type strings) rather than renaming every `type` key in the tree. Added a test proving an unrelated nested `type` field (e.g. an issue label) is left untouched. Removed inline // comments that only restated the code; folded the two genuinely non-obvious GitHub semantics (issue_comment firing for both issues and PRs, closed vs merged pull requests) into the function's TSDoc. * chore(jira): drop extraneous inline comments from second-pass re-audit Removed self-evident line comments in isJiraEventMatch() and the jira_webhook output-schema composition — the code (variable/function names) already says what these restated. * fix(salesforce): add missing permission-set and async-path steps to Flow setup instructions Re-verified the Named Credential setup instructions against live Salesforce docs. Two required steps were still missing: (1) the Flow's running user (Automated Process user for record-triggered flows) needs a permission set granting External Credential Principal Access, or the callout fails auth even with a correctly configured Named Credential; (2) record-triggered flows can only perform callouts on the Run Asynchronously path. * fix(hubspot): fail loudly instead of silently truncating over-limit filters Filters within a HubSpot Search API filterGroup are AND-combined, so silently dropping the last N filters when the combined shortcut + advanced-filter count exceeded MAX_USER_FILTERS widened the match set instead of narrowing it — a poll could start matching records the user's config meant to exclude, firing the workflow on unintended records with no visible error. That's worse than the original hard-400 bug, which was at least loud. buildUserFilters now throws when the combined count exceeds the limit, which pollWebhook's existing catch turns into a visible markWebhookFailed, matching how every other misconfiguration in this file (missing objectType, invalid eventType, corrupt watermark) is already handled. Re-derived the cap arithmetic against HubSpot's live docs (developers.hubspot.com/docs/api/crm/search): max 6 filters per filterGroup, 5 groups, 18 total. Group B reserves 2 slots (filterProperty EQ + hs_object_id GT tie-break), so MAX_USER_FILTERS = 4 is exact — not off by one in either direction. * fix(zendesk): validate subdomain as a bare hostname label createSubscription/deleteSubscription interpolate the user-supplied subdomain directly into the Zendesk API URL (`https://${subdomain}.zendesk.com`). An unvalidated value containing a '/' (e.g. "evil.example.com/x") escapes the host portion of the URL, redirecting the request — and its Basic-auth admin credentials — to an attacker-controlled host. Unlike the equivalent pattern in apps/sim/tools/zendesk (invoked only when a user explicitly runs a block with their own credentials), this fires automatically on deploy and undeploy using admin-scoped API tokens, making it the more acute instance of the pattern. Reject anything but letters, digits, and internal hyphens. Also drops a few restated-in-code inline comments per repo convention. * fix(triggers): restore jira fieldFilters as a real feature, fix twilio-voice status collision jira: the second-pass audit removed the `fieldFilters` subBlock on issue_updated as "dead code, never read" — true, but that meant the feature was already non-functional before removal (a UI control promising field-level filtering that did nothing), and removing it silently dropped an already-visible control from the merged block UI (flagged independently by both Greptile and Cursor). Rather than either reinstating a broken control or leaving it removed, implement the feature for real: matchEvent now checks the comma-separated field list against Jira's changelog.items[].field on issue_updated deliveries, matching Jira's actual webhook payload shape. twilio-voice: extractIdempotencyId keyed on CallSid alone, so every status callback for a call (ringing/in-progress/completed/etc) shared one idempotency key and only the first was ever processed — later CallStatus transitions were silently dropped as duplicates. Fixed to include CallStatus in the key, matching the SMS handler's existing SID:status pattern. Also converted an inline rationale comment in the SMS handler to TSDoc for consistency with the rest of this cleanup. * fix(twilio-voice): key idempotency on all callback discriminators, not just status CallStatus/RecordingStatus/TranscriptionStatus share overlapping values (e.g. 'completed'), and Gather turns / recording events fan out multiple distinct deliveries under one CallSid while CallStatus itself stays unchanged. Build the key from field=value pairs across every known discriminator (CallStatus, Digits, SpeechResult, RecordingSid, RecordingStatus, TranscriptionSid, TranscriptionStatus) so callback kinds can never collide on a shared value, while identical retries still dedupe. * chore(github-trigger): remove extraneous inline comment Round 3 re-audit: verified push event handling (commits[].author/ committer, head_commit, pusher) is correctly excluded from the GitHub-user type-alias walk since those objects lack login+type, so no output-schema drift. No functional issues found. Removed a leftover inline // comment in the generic webhook trigger's output schema per repo comment conventions. * fix(stripe-trigger): add missing 2025 event types to eventTypes allowlist invoice.payment_attempt_required and balance_settings.updated shipped in Stripe's 2025-10-29 API update but were missing from the trigger's curated eventTypes dropdown despite their categories (Invoices, Balance) already being represented. * fix(salesforce-trigger): document required Allow Formulas in HTTP Header checkbox Setup instructions told admins to add a Named Credential custom header using a $Credential formula, but never mentioned that the "Allow Formulas in HTTP Header" checkbox must be checked when adding that header. Left unchecked, Salesforce sends the literal "{!\$Credential...}" text instead of evaluating it, so the shared-secret auth silently fails. * fix(slack): skip block_suggestion payloads instead of wastefully executing workflows block_suggestion (external select option loading) requires Slack to receive a synchronous JSON options response within 3 seconds, which this trigger's async fire-and-forget webhook execution model can never provide. It was previously routed through the generic interactivity handler like block_actions/shortcut/view_submission, meaning every keystroke in an external-select typeahead would silently trigger a full (useless) workflow execution. Now explicitly skipped via the existing skip mechanism. * fix(microsoftteams): recreate Teams subscription when renewal PATCH 404s If every renewal attempt in a subscription's 48h renewal window failed (revoked consent, prolonged Graph outage), the subscription actually expired on Microsoft's side and the cron kept PATCHing a deleted subscription forever, always 404ing, with the webhook silently dead. Now a 404/410 from the renewal PATCH triggers a POST to recreate the subscription from the stored chatId, closing the same "never comes back" failure mode the original bug had, for the narrower case where renewal itself keeps failing. Also dedupes getCredentialOwner onto the shared provider-subscription-utils helper instead of a local copy, and drops a couple of restated-in-code comments.
…5527) * feat(jupyter): add Jupyter integration (contents, kernels, sessions) - 16 tools covering Contents, Kernels, Kernelspecs, and Sessions REST APIs - File upload/download via UserFile, following the Box upload pattern - Block with operation dropdown, token auth, and 8 catalog templates - Registered tools + block, generated docs, bumped API validation baseline * fix(jupyter): address Greptile review — SSRF guard, upload path ambiguity, silent notebook fallback - Route uploads through validateUrlWithDNS + secureFetchWithPinnedIP (matches Grafana/1Password pattern) instead of a raw fetch to the user-supplied server URL - Replace the upload path/trailing-slash heuristic with an unambiguous directory + filename split - create_file no longer silently writes an empty notebook when notebook content is malformed JSON — it now errors clearly * fix(jupyter): request content=1 when listing directory contents Without it, Jupyter Server returns directory metadata with content: null, so jupyter_list_contents always reported an empty items array. * fix(jupyter): reject path-traversal segments in Jupyter content paths encodeJupyterPath now rejects '.'/'..' segments across the whole path (shared by all 16 tools, not just upload); the upload route returns a clean 400 when it's hit. * fix(jupyter): close remaining path-traversal and redirect-credential gaps - extract the traversal check out of encodeJupyterPath into a shared assertion, and apply it to body-only path fields (rename newPath, copy copyFromPath, session path) that never flowed through URL encoding and so skipped the check - pass stripAuthOnRedirect to the upload route's secureFetchWithPinnedIP call so a malicious Jupyter server can't redirect the PUT to another origin and receive the caller's token * fix(jupyter): also reject percent-encoded traversal segments A segment like %2e%2e wouldn't match the literal '..' check. Now decodes each segment before comparing, in addition to the literal check, so an already-encoded traversal attempt is caught too. * fix(jupyter): route all 15 remaining tools through an internal proxy for HTTP/private-host support and no redirects The generic external tool executor blocks plain-HTTP and non-localhost private-IP hosts by default, so every non-upload Jupyter operation could fail against typical self-hosted setups (LAN IP, docker hostname, or even literal localhost on a hosted deployment) even though the upload route worked via its own internal route. Added /api/tools/jupyter/proxy (DNS-pinned, allowHttp, maxRedirects: 0) that mirrors the upstream Jupyter response verbatim, matching the established pattern for self-hosted-arbitrary-host integrations (Grafana, 1Password) instead of the generic executor path. Each tool's request block now posts to the proxy instead of building a direct external URL; transformResponse and outputs are unchanged since the proxy response mirrors upstream status/body exactly. Also switches the upload route from stripAuthOnRedirect to maxRedirects: 0 — stronger, since it stops the uploaded file body (not just the token) from ever reaching a redirect target. * fix(jupyter): validate proxy path at the trust boundary, reject path separators in upload filename - The proxy route now independently validates the incoming path field for traversal segments instead of only relying on tool-side validation before the request reaches it — the route is a shared internal boundary, not something only our own tool code can call - The upload route's fileName can come from an advanced override or the legacy fileContent path and could itself contain '/' or '\', silently nesting the upload deeper than the directory param specified. Now rejected outright before joining. * fix(jupyter): decode the whole path before splitting, not per-already-split segment A segment like foo%2f..%2fsecret has no literal slash, so splitting on literal '/' first and decoding each piece in isolation treats it as one opaque segment and never notices the '..' hiding behind the encoded slash. Decode the full path once, then split and check every segment the target server's own single URL-decode pass would see.
…ng (#5529) * chore(ship-skill): check branch is synced with origin/staging before pushing Prevents PRs from silently picking up extraneous commits when a branch/worktree is cut from a stale or diverged local staging/main. * feat(babysit): add skill to drive a PR through review to a clean 5/5 Ships (via /ship's sync check), waits for Greptile/Cursor Bugbot, triages every open thread (fix real findings, push back on false positives), replies + resolves each thread, re-triggers review, and loops until Greptile is 5/5 with zero open threads. * chore(cursor-commands): mirror sync-check into ship.md, add babysit.md Keeps the Cursor /commands mirrors of ship and babysit in sync with the Claude Code .agents/skills versions so they don't drift. * fix(ship-skill): compare commit content not count, handle WIP before rebase, avoid fixed temp branch name - Final verify now diffs actual commit subjects between git log and the PR, not just a count — a corrupted branch's inflated count could coincidentally match a later count check - The pre-commit count in step 2 was being compared against the post-commit PR in step 9, which would mismatch even on success; now compares content at the point it's actually available - Recovery flow stashes uncommitted work before rebasing so it isn't blocked by a dirty tree, and checks for an existing tmp branch before reusing the name * fix(ship-skill,babysit): clean rebase can still hide drift, fixed temp-branch name blocks recovery - A clean git rebase can replay stray commits with zero conflicts, so 'rebase succeeded' was being treated as sufficient — now the log is always re-checked after rebasing, clean or not, before trusting it - The recovery text told the agent to 'pick another name' if ship-sync-tmp was taken but then hardcoded that literal name anyway; now it just deletes the leftover (disposable, single-purpose) branch instead of introducing a naming scheme - babysit's step 5 pointed at re-running the log check alone; it now points at the full /ship step 2 recovery flow, since a review loop spanning a long session is exactly where drift compounds silently * fix(ship-skill,babysit): guard tmp-branch delete, fetch before final verify, paginate threads, verify after every push - ship-sync-tmp deletion is now conditional on the branch existing — a bare 'git branch -D' on a first recovery attempt with no leftover would fail and block the rebuild before it started - step 9's final commit-content verify now fetches origin/staging first, matching step 2; a stale local ref could mask real drift - babysit's reviewThreads query now checks pageInfo.hasNextPage and pages through all threads instead of assuming 50 is always enough - babysit now re-runs the sync-content verify after every push, not just before it, so a bad push or a PR that drifted mid-loop doesn't go unnoticed across review rounds * fix(ship-skill,babysit): fix reversed commit-list order, tail-1 grabbing wrong line, stale step reference - git log --oneline is newest-first, gh pr view commits is oldest-first — the two content-verify commands now use --reverse so a positional comparison doesn't spuriously fail on any multi-commit branch - 'select(...) | .body | tail -1' pipes every matching comment's full body through the pipeline and keeps only the last LINE of the combined output (the review-count footer), not the last COMMENT — demonstrated this myself this session reading the footer instead of the actual score. Now uses '[.comments[]] | last | .body' - Cursor's babysit.md pointed at '/ship step 9', but Cursor's ship.md only has 7 steps (no cleanup/migration steps) — now points at step 7 and cross-references the 9-step Claude Code skill copy * fix(ship-skill): checkout before deleting ship-sync-tmp, cherry-pick oldest-first - git refuses to delete the branch currently checked out, so if an earlier interrupted recovery left the agent sitting on ship-sync-tmp, the conditional delete would find the branch and then fail to remove it, blocking the rest of the rebuild. Now checks out the original branch first (a no-op if already there). - git log's default order is newest-first; cherry-picking session SHAs in that displayed order applies the newest commit before older ones, which can fail or produce the wrong history with 2+ session commits. Now explicit: cherry-pick oldest-first, using --reverse to get them in that order directly. * fix(ship-skill): capture cherry-pick SHAs before switching to the temp branch, make tmp delete idempotent - Step 4 of the rebuild checks out ship-sync-tmp at origin/staging, so HEAD no longer contains the session's commits by the time the old step 4 (now step 5) ran 'origin/staging..HEAD' to find them — that range was always empty by then, following the steps literally would cherry-pick nothing. Now captures the SHA list first, against the original branch name explicitly, before any branch switch. - The show-ref-guarded delete still exits 1 (git show-ref's own failure) on the common first-attempt case where there's no leftover branch, which could halt a caller that stops on nonzero exit before ever reaching the rebuild. Replaced with a plain delete + || true, which always succeeds whether or not there was anything to delete. * fix(ship-skill,babysit): rebuild was replaying the exact stray commits it was meant to leave behind - The 'capture the range' step from the previous round's fix captured origin/staging..<original-branch> wholesale — but in exactly the scenario that triggers this rebuild, that range also contains the unrecognized/stray commits, so cherry-picking 'all of it' recreated the same polluted branch. Now explicit: read the log, write down only the SHA(s) you recognize as your own, never the whole range. - babysit's pre-push gate list named lint/typecheck/boundary-validation but dropped ship's conditional /cleanup and /db-migrate steps, which a review-fix round can trip just as easily as the original commit. - babysit's force-with-lease condition only covered the cherry-pick rebuild path, but a plain 'git rebase origin/staging' that completes with no conflicts also rewrites already-published history and needs the same force push. * fix(ship-skill): push step needs force-with-lease too, not just babysit's Step 2's sync recovery rewrites already-pushed commits on a branch that's been pushed before, but the push step only said 'push using the current branch name' with no force-with-lease — a plain push would be rejected in exactly the polluted-remote case step 2 exists to fix. babysit already got this fix; ship's own push step didn't.
|
Too many files changed for review. ( |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview Enterprise & product: Workspace forking (linked child workspaces, push/pull of deployed workflows, mappings for credentials/secrets/resources) and custom blocks (publish deployed workflows as org-wide reusable blocks) ship with new docs and comparison-page facts. Landing blog routes are refactored onto shared Integrations & providers: New Jupyter workflow integration (contents, kernels, sessions) with docs/icons and DNS-pinned server-side proxy/upload routes for user-supplied Jupyter URLs. xAI Grok 4.5 is added as a provider; knowledge document tags open from the row context menu. PII service: Presidio NER is split into Quality & DX: TypeScript 7 (including docs/realtime tsconfig tweaks). Reviewed by Cursor Bugbot for commit 472457b. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 472457b. Configure here.
* improvement(forking): minor ui improvements * fix warning
…coverage (#5269) * feat(observability): extend audit log, PostHog, and storage metering coverage Instruments previously-uncaptured resources and actions across the three observability layers (audit log, PostHog product analytics, usage metering): - Exfiltration audit: file downloads (workspace/public-share/v1), table, workflow, and workspace exports. - Credential access audited at the token-issuance boundary (success-only). - Full revenue trail: invoice paid/failed, overage billed, disputes, credit fulfillment, subscription lifecycle, plan/seat changes. - Session/login lifecycle audit via Better Auth hooks (login, blocked sign-in, logout, session revoke, account delete). - v1/admin programmatic surface and copilot tool handlers instrumented. - Dead audit/PostHog constants wired (lock/unlock, table, custom tool, etc.). - Storage metering extended to KB documents and copilot files. - PostHog person identify + workspace/organization group hygiene. Audit package hardened to null the actor FK for system actors (admin-api) and adds an awaitable recordAuditNow for pre-delete hooks. All instrumentation is fire-and-forget and never blocks or breaks the primary operation. * fix(observability): audit file/credential egress only on success Address Cursor Bugbot review: - GET OAuth token: emit CREDENTIAL_ACCESSED/credential_used after refreshTokenIfNeeded succeeds (matches POST), not before. - File export: audit on each actual success exit via a shared helper — including the non-markdown serve redirect (previously unaudited) and after the zip is generated (previously before asset fetch). * fix(audit): record null actor for anonymous public-share downloads Address Greptile P1: setting actorId to the file owner made every anonymous external download read as a self-download, undermining the exfiltration trail. recordAudit now accepts a null actor; the public content/inline routes record actorId=null with the owner in metadata.sharedByUserId and rely on ip/user-agent for the forensic trail. The misleading owner-attributed file_downloaded PostHog event is dropped on these anonymous paths. * fix(observability): audit admin exports after zip; tidy comments - Admin workflow/workspace ZIP exports now audit only after the archive is built (via a local helper), so a zip/build failure no longer logs an export. - Remove redundant 'success-only placement' inline comments and tighten the remaining design-rationale notes (export helper now TSDoc). * fix(billing): complete the chargeback + overage financial trail Address Cursor review: - handleDisputeClosed now records CHARGE_DISPUTE_CLOSED for every closed dispute (won/lost/warning_closed), unblocking only on favorable outcomes. dispute.status in the metadata distinguishes the outcome, so lost chargebacks are no longer missing from the trail. - Threshold overage now emits OVERAGE_BILLED + overage_billed even when credits fully cover the overage (settledVia: 'credits' vs 'stripe'), so credit-settled overages are audited instead of silently returning null. * fix(storage): decrement copilot quota before deleting metadata Address Greptile P1: deleting the metadata row before the decrement meant a decrement failure left the quota permanently inflated with no record to retry from. Decrement first; only remove the metadata row once it succeeds. * fix(observability): atomic copilot storage release; signup-blocked action - Copilot file delete now releases storage via a single transaction (releaseDeletedFileStorage): the soft-delete is the idempotency claim and the decrement shares the transaction, so neither a partial failure (inflated counter) nor a retry (double-decrement) can desync the quota. Resolves the conflicting Greptile/Cursor ordering findings. - Policy-blocked sign-ups now record USER_SIGNUP_BLOCKED instead of USER_SIGNIN_BLOCKED, so account-lifecycle events aren't mislabeled. * fix(storage): meter copilot ingest centrally for path symmetry Address Cursor review: only uploadCopilotFile incremented storage, but copilot files also enter via the generic upload route and presigned uploads — all of which persist metadata through insertFileMetadata. Move the increment into insertFileMetadata (scoped to context='copilot', on genuine insert/restore) so every ingest path is symmetric with the delete-time decrement, and drop the now redundant per-path increment in uploadCopilotFile. Other contexts are metered by their own managers and remain unaffected. * fix(audit): skip WORKFLOW_EXPORTED when admin export is empty Address Cursor review: when every requested workflow fails to load, the admin export still returned 200 but recorded a successful export of zero workflows. Guard auditExport so an empty result records nothing. * fix(storage): settle copilot accounting before deleting the blob Address Cursor review: the blob was removed before releaseDeletedFileStorage, so a release failure left the counter inflated and the metadata active with the blob gone. Now the atomic soft-delete + decrement runs first and the blob is deleted only if it succeeds, so a failure leaves the file fully intact and retryable. * fix(storage): decrement KB document storage atomically with deletion Address Cursor review: hardDeleteDocuments deleted the rows then decremented best-effort, so a decrement failure left billed storage inflated with no row to reconcile. Resolve each owner's subscription up front, then decrement inside the same transaction that deletes the embeddings/documents (decrementStorageUsageInTx, also now shared by releaseDeletedFileStorage), so the counter and the content commit or roll back together. Connector docs remain excluded. * fix(audit): don't treat email verification as a login Address Cursor review: /verify-email could emit USER_LOGIN when verification mints or refreshes a session, mislabeling (or double-counting) a non-sign-in as a login. Restrict isLoginPath to genuine sign-in entrypoints. * fix(audit): null actor FK when the user lookup throws Address Greptile P1: the catch branch left the original actorId, so a system actor like 'admin-api' (or a since-deleted user) would FK-violate the insert and lose the row when the existence lookup errored. Mirror the not-found branch — null the FK with a readable label — so the audit row always persists. * revert(audit): drop session/account-lifecycle auth instrumentation Remove the Better Auth login/logout/session-revoke/account-delete/blocked-signin audit hooks from auth.ts — they touch sensitive auth paths and are noisy. Also removes the now-unused taxonomy (USER_LOGIN/_FAILED, USER_SIGNIN_BLOCKED, USER_SIGNUP_BLOCKED, USER_LOGOUT, SESSION_REVOKED, ACCOUNT_DELETED, ACCOUNT_EMAIL_CHANGED actions; SESSION/USER resource types) and the awaitable recordAuditNow helper that only the pre-delete hook used. auth.ts is back to the staging baseline. * fix(storage): harden copilot+KB delete accounting against read errors and concurrency Final-audit follow-ups: - deleteCopilotFile: a failed metadata *read* (vs a genuine missing row) now blocks the blob delete too, so a transient read error can't leave an active row un-decremented with the blob gone. - hardDeleteDocuments: drive the per-user decrement from the delete's returning() (the rows this tx actually removed), so two concurrent deletes of the same ids can't both decrement. * chore(observability): drop two unused definitions Final-audit cleanup: remove the orphaned knowledge_base_searched PostHog event (never wired; KB search analytics already flow through the OpenTelemetry channel) and the redundant AuditAction.SUBSCRIPTION_UPDATED (subscription/plan changes are audited via ORG_PLAN_CONVERTED). Every remaining new action/event has a real emit site. * fix(knowledge): key hard-delete result off rows actually deleted Address Cursor review: hardDeleteDocuments returned existingIds.length (the requested count) and cleaned storage for the full pre-tx set, even though the decrement was driven by the rows the transaction actually deleted. Under a concurrent delete that claimed some ids first, that overstated the result and re-touched storage for rows this call didn't delete. Drive the storage cleanup, log, and return value from deletedDocs (the returning() rows) so all four are consistent. * fix(storage): gate copilot quota on all ingest paths; tidy inline comments - Copilot uploads via the generic /api/files/upload route and presigned URLs now run checkStorageQuota before writing (matching uploadCopilotFile), so no copilot ingest path can grow usage past the plan limit. The central increment stays in insertFileMetadata. - Trim/remove redundant inline comments across the diff; keep only concise notes on non-obvious decisions (left pre-existing comments untouched). * fix(analytics): omit empty workspace_id on workspace-less file downloads Address Greptile P1: the generic key-based /api/files/download route and the no-workspace markdown-export path emitted file_downloaded with workspace_id:'' creating a phantom '' bucket in PostHog. Make workspace_id optional on the event and omit it when there is no workspace (workspace-scoped routes still pass it). * fix(analytics): omit empty workspace_id/workflow_id on copilot_chat_sent Final-sweep nit: copilot_chat_sent sent '' for workspace_id/workflow_id in the agent (workspace-less) branch, same phantom-bucket issue as file_downloaded. Make both properties optional and omit them when absent. * fix(analytics): clear stale org PostHog group on personal workspaces Address Cursor review: switching from a team workspace to a personal one left the previous organization group set, so later events kept rolling up under it. When the active workspace has no organizationId, resetGroups() to drop the stale org group, then re-apply the workspace group. * fix: address review — export audit timing, copilot delete signal, group reset - Table export (Cursor MED): audit fires before streaming begins, not after controller.close(), so a mid-stream failure still records the partial export. - deleteCopilotFile (Greptile P1): throw when storage accounting can't be settled instead of silently returning, so callers can detect the file was not deleted. - workspace-scope-sync (Cursor MED): only resetGroups() once workspace metadata is loaded (activeWorkspace present), so a team workspace doesn't transiently lose its org group while organizationId is still null during load. * refactor(observability): final line-justification cleanup Address final audit flags: - Table import: move the 'columns added' audit OUT of the import transaction into a post-commit auditTableColumnsAdded() helper called by the three tx-owning callers, so a mid-import row-batch rollback no longer logs a false 'added N columns' (matches the PR's success-only discipline). - Omit empty-string analytics dimensions on workflow_lock_toggled (workspace_id) and organization_created (name), consistent with file_downloaded/copilot_chat_sent. - Restore an unrelated capacity-check comment removed incidentally. - Move copilot_chat_sent emit above the traceparent comment so the comment sits with the code it documents. * fix(table): attribute import column audit to the importing user Address Cursor review: addImportColumns (async createColumns import path) now threads the importing userId into auditTableColumnsAdded instead of falling back to table.createdBy, so column additions are attributed to the actual member who ran the import rather than the table creator. * feat(observability): drop copilot from storage accounting Per design decision: copilot files are working/conversational artifacts, so gating their materialization on storage quota would fail an agent operation mid-flow when a user is over limit, and metering them would inflate usage enough to block KB/workspace uploads indirectly. Remove copilot quota gates + copilot ingest metering entirely (revert copilot-file-manager, metadata, and the upload route's copilot branch to baseline) and drop the now-unused releaseDeletedFileStorage. KB document metering + quota enforcement (the deliberate, persistent storage path) is unchanged. * fix(analytics): switch workspace + org PostHog groups together Address Cursor review: gate the group-sync effect on workspace metadata being loaded (activeWorkspace present) so the workspace and organization groups always update atomically. Acting during the load window paired the new workspace group with the previous workspace's org group; until metadata loads, events stay consistently attributed to the previous workspace. * fix(table): audit async export at authorization, not job completion Address Cursor review: async exports only emitted TABLE_EXPORTED when the background job reached 'ready', so an authorized export whose job later failed or was abandoned left no audit trail — inconsistent with the sync export route, which audits before streaming. Move the audit + analytics to the async route's authorization point (after the job is claimed/dispatched) and remove it from the runner. Drop the now-unused userId from TableExportPayload. * fix(billing): never let payment_failed instrumentation skip user blocking Address Greptile P1: the payment_failed audit hoisted an unguarded isSubscriptionOrgScoped DB read (for entity_type) directly before the attempt-count user-blocking block. A transient failure of that read would throw out of the handler and skip blocking. Wrap the whole audit/analytics block in try/catch (best-effort), and let the blocking compute its own isSubscriptionOrgScoped as it did originally — instrumentation can no longer abort payment processing. * chore(observability): trim verbose inline comments to concise notes * fix(billing): wrap new dispute/enterprise/subscription instrumentation in Stripe webhook idempotency recordAudit/captureServerEvent calls added for charge disputes, enterprise subscription provisioning, and free->paid subscription creation ran unconditionally with no idempotency guard, unlike their sibling handlers in the same files. Stripe redelivers webhooks at-least-once, so a retry would double-record the audit row and PostHog event even though the underlying DB writes were already idempotent. * fix(observability): tag org-scoped audit metadata with organizationId, guard concurrent table delete The org-scoped self-service audit-log endpoint matches org-level rows via metadata.organizationId (or resourceType=organization). Six recordAudit call sites (subscription create/cancel, admin credit issuance, threshold overage billing, charge disputes, credit purchase, invoice payment succeeded/failed) tagged org-scoped events with a differently-named key (referenceId/entityId/targetOrgId), making them invisible to org admins querying their own audit trail despite being stored in the DB. Add the missing organizationId key everywhere the pattern was missed. Also guard deleteTable's archive UPDATE with isNull(archivedAt) so a concurrent duplicate delete request is a no-op instead of re-archiving and re-firing a duplicate TABLE_DELETED audit row. Adds test coverage for the ORG_MEMBER_ADDED audit/analytics emission in acceptInvitation, which previously had none. * fix(test): queue resolveBillingActorId's owner lookup in payment-failure email test handleInvoicePaymentFailed's new payment_failed audit instrumentation resolves the billing actor via an extra db.select before sendPaymentFailureEmails runs. The test's fixed select-response queue didn't account for it, so the org-admin lookup consumed the wrong queued row and the assertion saw zero email sends. Production behavior is unaffected — each query is independent; this was a mock-queue ordering issue only. * fix(billing): don't let a post-commit usage-limit sync failure suppress the seat audit Cursor Bugbot flagged: reconcileOrganizationSeats committed the seat change and Stripe outbox enqueue in a transaction, then called syncSubscriptionUsageLimits outside it before recording the audit/ PostHog events. A thrown sync left a genuinely-changed seat count with no ORG_SEAT_PROVISIONED/DEPROVISIONED trail. Wrap the sync in its own try/catch (log + continue) so the events always fire for a committed change, matching the fire-and-forget instrumentation pattern used elsewhere in this PR. * fix(billing): guard the new subscription-created instrumentation block Greptile P1: the org-scope check I added for the audit-metadata fix (isSubscriptionOrgScoped) was a raw DB call with no error guard, unlike resolveSubscriptionActorId next to it. Since it ran inside the idempotency lambda with an unconditional rethrow, a transient DB error would abort and retry the whole webhook after the free -> paid usage reset had already committed. Wrap the actor/org-scope resolution + audit + analytics block in try/catch, matching the same guarded instrumentation pattern already used in handleInvoicePaymentFailed.

Uh oh!
There was an error while loading. Please reload this page.