| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat(audit): name api keys and environments by their uuid (#7319) Stacked on #7309. That PR moved the public environment API to UUIDs and converted the two public `api_key` audit targets with it, which left the trail using two identifiers for the same object: a key created through the dashboard was named by its internal id, one created through the public API by its uuid, and the key that *made* a call by its internal id either way. Nothing could be joined to anything. This makes the uuid the only identifier the trail uses. - **The five remaining `api_key` targets use the uuid.** The two numeric-route lookups already read the row for a display name, so the uuid costs no extra query — they only needed renaming, since `getApiKeyDisplayName` returning a uuid would be a lie. They are `getApiKeyById` / `getAccountApiKeyById` now, mirroring `getApiKeyByUuid` from #7309. - **The actor uses the uuid.** The auth query already joins `customer_keys`, so `uuid` rides along on the existing select and reaches `resolveActor` through the auth context and `res.locals`. A sandbox token resolves to the key it was minted from, which is the object its numeric id already named; only `api_secret` and `env_var` have no key row and keep the internal id. - **The event's environment is named by its uuid**, which makes `AuditEvent.environment.id` a string. The numeric environment id is dropped from the audit subject entirely — nothing in the emit path ever read it, so 25 call sites got shorter. - **Both creation endpoints and the environment key list return the uuid**, and the dashboard shows it as a read-only copyable field on environment settings and on the api key detail, in both cases directly after the name. ## Notes for review The stored contract changes shape. That is only safe because the trail is not public yet and the table can be truncated; the truncation now covers three shape changes — `scope` from #7310, the environment uuid, and the actor — so it should be a single truncate once all three land. ## Test plan - [x] Full unit suite: 4,262 tests across 317 files - [x] Full integration suite: 1,866 tests across 191 files — the CTE bug below is only reachable there - [x] `ts-build`, webapp typecheck, oxlint and prettier clean - [x] Actor uuid, both fallbacks, sandbox attribution, and all three uuid-valued `api_key` created targets break-checked - [x] Deployed to development and confirmed both dashboard fields render and copy - [ ] Confirm a dashboard-created key and a public-API-created key now record the same identifier <img width="1247" height="456" alt="Screenshot 2026-09-01 at 12 37 36" src="https://github.com/user-attachments/assets/7560a927-da27-41b8-b4c0-08fc09470e96" /> <img width="1219" height="369" alt="Screenshot 2026-09-01 at 12 37 20" src="https://github.com/user-attachments/assets/bacf47b3-150b-45f3-a156-533d51cc1a07" /> --------- Co-authored-by: Erick R <erickkrocha@gmail.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 5 天前 | |
refactor(server): use grants and scopes instead of permissions in private API (#7293) We have been running shadow authorization using the new roles described by grants (`packages/authz/lib/roles.ts`), and recording metrics to identify divergencies. This PR is the actual flip (restricted to private routes). The main thing it does is changing the `can` middleware from taking a permission, to taking a scope: ```diff -web.route('/team').put(webAuth, auditTeamUpdated, can(p.canManageTeam), putTeam); +web.route('/team').put(webAuth, auditTeamUpdated, can('account:team:update'), putTeam); -web.route('/connections').get(webAuth, can({ action: 'read', resource: 'connection', scopedBy: envScope }), getConnections); +web.route('/connections').get(webAuth, can('environment:connections:list'), getConnections); ``` `scopedBy` is not needed anymore. The scope namespace (either `account:...` or `environment:...`) is what defines the target, and the target is then evaluated against the `where` in the grant (the grant needs access to the specific target environment). Most of the conversions are simple translations of the permissions to scopes, but for 6 of them I had to make a decision, which is worth a second pair of eyes: - `/api/v1/connections` takes `:list`, `/api/v1/connections/:id` takes `:read` — the old grammar had no `list`, so one permission guarded both. - deploy and sync-toggle split into `environment:deploy` and `environment:syncs:update` - `/sync` moves to `environment:syncs:read`, matching the scope its public twin already uses - webhook-signing-key rotation moves to `environment:webhook_signing_key:rotate`, which already existed for the public route — it was guarded by the API-key permission by accident - new private scopes: `environment:api_keys:{create,delete}` and `account:billing:spend_alert:{read,update}`, because "update" was guarding creates and payment-method scopes were guarding spend alerts <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7293?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 6 天前 | |
feat(webapp): show the new pricing's plans (#7306) ## Problem From September 1 Nango sells three plans — Free, Pay-as-you-go, Enterprise. The billing page still offered Free / Starter / Growth / Enterprise, so anyone opening `#plans` after the 1st sees plans they can no longer buy. The spend tooltip promised usage "beyond your plan's included quota", which Pay-as-you-go doesn't have — Orb prices it as a $50 monthly minimum, billed in arrears. Behind the `s26-pricing` flag from NAN-6723. ## Solution - Which card set an account sees follows the pricing it's billed on: accounts already measured on the new metrics get the three new cards, everyone else keeps the four old ones. Starter and Growth become Contact us, since moving between them is no longer sold and the server rejects it. - The catalog resolves plans by code rather than by `hidden`, so `pay-as-you-go` renders while the rollout keeps it hidden. - Extends the Orb billable-metric map past the seven the old plans bill on, so a pay-as-you-go subscription stops reporting `billing_period_costs_unattributed` on every page load. - Restates the spend tooltip as a minimum rather than a base fee plus overage, matching what Orb charges. - A failed plan change points at support instead of "try again", which was only ever true for a declined card. Contact actions open the in-app chat. Fixes [NAN-6741](https://linear.app/nango/issue/NAN-6741) ## Testing With the flag on, walked `/dev/team/billing` through the plan override: **free** (three cards, primary Upgrade), **pay-as-you-go** (own card selected, Downgrade on Free with a dated confirm), **enterprise / growth-v2** (four old cards), **flag off** (unchanged). <img width="1282" height="424" alt="image" src="https://github.com/user-attachments/assets/51dab23c-0c8c-4e1f-8aef-d3279c926ea6" /> <img width="996" height="472" alt="image" src="https://github.com/user-attachments/assets/139405d0-7339-47e0-8ddc-94fe0ec33810" /> <img width="1281" height="418" alt="image" src="https://github.com/user-attachments/assets/5668c781-13a8-48c5-8d41-6c446a3cabac" /> <img width="956" height="436" alt="image" src="https://github.com/user-attachments/assets/b32abe35-6aa1-4aae-8f56-317e9687d66f" /> <img width="1268" height="489" alt="image" src="https://github.com/user-attachments/assets/3431a776-fadf-40f2-87f5-5a32bda4af75" /> <img width="1261" height="158" alt="image" src="https://github.com/user-attachments/assets/8bf779a2-6086-44bf-85eb-d45f35b7bc23" /> ## Follow-ups - [NAN-6834](https://linear.app/nango/issue/NAN-6834) — self-serve Growth add-on. The Pay-as-you-go card names it as a feature bullet meanwhile. - [NAN-6840](https://linear.app/nango/issue/NAN-6840) — the plan-change wait has no deadline. Pre-existing, left as it was. - Enterprise now sees the old cards *and* the retired usage metrics. Consistent, but nothing shows Enterprise the new ones yet. | 6 天前 | |
chore(release): 0.71.6 | 4 天前 | |
feat(audit): dedicated audit ClickHouse database + own migration (NAN-6339) (#6934) - **Audit gets a dedicated `audit` ClickHouse database with its own migration set**, created and migrated by metering at boot. `audit_trail_events` had ridden along on the usage migration runner, which targets the `usage` database — conflating the compliance audit log with billing data, and coupling audit's schema to the usage migration set. Colocation for cost only ever required the same cluster, not the same database. - **The ClickHouse migration runner is now shared, not duplicated**: extracted from `@nangohq/usage` into a new `@nangohq/clickhouse-migrations` leaf package that both `@nangohq/usage` and `@nangohq/audit` delegate to. Only the migrations *directory* stays per-package — that separation is the whole point, since one shared directory is exactly why pointing the usage runner at the audit database would apply every usage migration to it. `packages/usage/lib/clickhouse/migrate.ts` goes 83 → 16 lines with its public signature unchanged. - **An event whose `accountId` is missing or malformed is now rejected at insert.** `JSONExtractInt` cannot throw, so such events were silently materialized as `account_id = 0` — filing them under the wrong account and risking `ReplacingMergeTree` key collisions — while the other two materialized keys already reject bad input. The guard combines the key's JSON type with a check on the value, so a negative or zero `accountId` is rejected too — account ids are serials starting at 1. This means the new table is deliberately *not* identical to the existing `usage.audit_trail_events`, which keeps the old behaviour until it is dropped. - **Existing callers are untouched.** `auditClickhouseClient` gained a `database` parameter, defaulting to the database audit events are stored in today, so a call that passes nothing behaves exactly as before — the two service call sites are byte-identical to master. Pass `null` to connect without a database, which is how the migration runner reaches `CREATE DATABASE`. The cutover is then a one-line change to that default. - **This PR is machinery only — there is no cutover.** Nothing reads from or writes to the new database yet: the server's store still points at the live `usage.audit_trail_events`, which is untouched. Repointing producers/consumers and dropping the old table are follow-up PRs. ## Migration ownership Metering runs `migrateAudit()` immediately after `migrateUsage()`. It already owns all ClickHouse DDL and will host the audit consumer, so one process stays the sole ClickHouse migrator and deploy-ordering rules don't change. It skips and logs when `CLICKHOUSE_URL` is unset, so self-hosted deployments are unaffected. The runner carries over usage's existing `TODO: lock` gap — concurrent replicas are benign, since all DDL is `IF NOT EXISTS` and duplicate ledger rows collapse by name — but it now lives in one place instead of needing two fixes. Migration log lines now name the target database, which does change the message strings: anything matching on the exact `Clickhouse migration:` prefix needs updating. ## Test plan - [x] `ts-build`, `npm run lint` (exit 0), prettier all clean - [x] Audit integration tests, including new coverage for the runner: creates the database, applies only the audit migration set, does not re-apply, skips when ClickHouse is unconfigured - [x] Usage ClickHouse integration tests (34) pass through the extracted runner - [x] Audit read-API integration tests (7) - [x] `accountId` guard: rejects missing and malformed while still storing a legitimate `accountId` of `0`, verified both by integration test and against a live migrated table - [x] Verified the tests actually fail when the thing they guard is broken — the no-re-apply assertion with the applied-set filter disabled, the store suite with the migration DDL broken, and the `accountId` cases with the constraint removed - [x] Fresh ClickHouse instance: the branch creates the `audit` database and table with the intended engine/partition/order/TTL, and a blob-only insert materializes `id`, `account_id` and `occurred_at` - [x] Upgrade path on a single instance (master applied first, then this branch): the branch applies zero usage migrations and one audit migration; the schema delta is exactly the `audit` database and its two tables, with every existing usage table's DDL byte-identical and the usage migration ledger unchanged - [x] A fresh install and an upgraded install converge to identical schemas - [ ] After deploy: confirm metering creates the `audit` database in development - [ ] Follow-ups: repoint producers/consumers at the `audit` database, then drop `usage.audit_trail_events` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 1 个月前 | |
fix(connect-ui): pre-bundle react/jsx-runtime in vitest config (#7205) ## Problem `tests-connect-ui`, which is required on `master` via the merge queue, occasionally fails with `Failed to fetch dynamically imported module` for a test file. Root cause: `@vitejs/plugin-react-swc` injects the `react/jsx-runtime` import at transform time, so Vite's dep scanner never sees it and discovers it mid-run, triggering a page reload that cancels whatever module was mid-import. This happens on every run of the suite (visible as a "Vite unexpectedly reloaded a test" warning in the logs) but only fails the suite when the reload lands during a test file's import. ## Solution - Pre-bundle `react/jsx-runtime` via `optimizeDeps.include` in `packages/connect-ui/vitest.config.ts` so Vite has it up front and never needs to re-optimize mid-run Fixes [NAN-6714](https://linear.app/nango/issue/NAN-6714/fix-rare-flake-in-tests-connect-ui-ci-job) ## Testing - Ran `vitest run` locally with a cold Vite cache (`rm -rf node_modules/.vite`) repeatedly — the reload warning no longer appears and the suite passes each time, versus appearing on every run before the fix | 13 天前 | |
chore: make tsconfigs compatible with typescript-go (#6593) ## Problem Step 3 of the ESLint → oxlint migration (NAN-5769). oxlint's type-aware mode runs on the native `typescript-go` compiler (via `oxlint-tsgolint`), which has dropped several legacy tsconfig options we still use. Until they're migrated, type-aware linting errors out on the affected packages, so those files wouldn't actually be type-checked. All of these are changes we'd need for newer TypeScript regardless. ## Solution Migrate each removed option to its modern equivalent: - **root `tsconfig.json`** — drop `ignoreDeprecations: "5.0"` + `importsNotUsedAsValues: "remove"`, add `verbatimModuleSyntax: true` - **`packages/webapp` + `packages/connect-ui`** — remove `baseUrl`; the `@/*` alias still resolves via the relative `paths` entry (tsc) and the explicit Vite `alias` (build), both independent of `baseUrl` - **`scripts/one-off/*` (3 tsconfigs)** — `moduleResolution: "Node"` (node10, removed) → `"Bundler"` `verbatimModuleSyntax` surfaced one value/type export to split (`BigQueryType` in `data-ingestion`); the rest of the codebase was already compliant since `consistent-type-imports` enforces `import type`. Prereq NAN-5809 (posthog-node v5) is already on master, so `verbatimModuleSyntax` no longer trips the old posthog packaging bug. Part of NAN-5769. Sub-issue: NAN-5810. ## Testing - `npm run ts-build` passes clean (exit 0) with `verbatimModuleSyntax` enabled - `@/` alias resolution confirmed unaffected — webapp/connect-ui resolve it via an explicit Vite `alias`, not tsconfig `baseUrl` - (Follow-up) `oxlint --type-aware .` reports zero `tsconfig-error` diagnostics — verified during the migration spike | 2 个月前 | |
fix(api): use UUIDs for public environment management (#7309) Use UUIDs instead of numeric IDs throughout the public environment-management API. - Add and backfill a non-null, unique UUID on customer API keys. - Move public environment API-key creation under the environment UUID path. - Resolve environment and API-key UUIDs with account-scoped lookups for create/delete operations. - Return UUIDs from environment and API-key creation responses. - Update audit targets, API types, OpenAPI/reference/generated docs, and integration coverage. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7309?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> | 5 天前 | |
feat(webapp): rework the billing overrides dev panel (#7314) ## Problem The dev panel had grown to a dozen simulators, every one a full-width block of the same weight, so finding one meant reading all of them. Its downgrade targets came from a hardcoded plan order that each new plan has to be added to — `pay-as-you-go` was already missing. ## Solution - Overrides become grouped label-and-control rows, and the panel is renamed **Billing Overrides** — only two of its eight rows are about the plan; the rest simulate cards, invoices, spend and charges. - Downgrade targets come from each plan's own `prevPlan` rather than a hardcoded list. - Adds a simulated card on file, so card-gated flows can be exercised without configuring Stripe locally. - Fixes a design-system bug on the way: a `side="left"`/`"right"` tooltip tucked its arrow along the wrong axis and painted it over the first word. The collapsed sidebar and two other call sites had it too. Split out of [NAN-6741](https://linear.app/nango/issue/NAN-6741). Dev-only — the panel sits behind the dev tools overlay. ## Testing Walked every override on `/dev/team/billing`. Tooltip sides checked in Storybook: `arrowOverlapsText` is false on all four. <img width="3728" height="2006" alt="image" src="https://github.com/user-attachments/assets/9602035f-65cb-4cc0-afbe-1269e05dec1f" /> | 5 天前 | |
chore(release): 0.71.6 | 4 天前 | |
feat(email): add generic HTTP API email provider (#7029) Self-hosted instances can only send transactional email through Mailgun's API or SMTP. Where neither fits, `EmailClient` falls back to `NoEmailProvider`, which only logs — so verification and invite emails are silently never delivered. This adds a provider that posts to any mail API accepting JSON over HTTP (SendGrid, Resend, Postmark, ...), configured by three env vars: ``` EMAIL_HTTP_URL=https://api.sendgrid.com/v3/mail/send EMAIL_HTTP_HEADERS={"authorization":"Bearer <api-key>"} EMAIL_HTTP_BODY={"personalizations":[{"to":[{"email":"{{to}}"}]}],"from":{"email":"{{from}}"},"subject":"{{subject}}","content":[{"type":"text/html","value":"{{html}}"}]} ``` `{{to}}`, `{{from}}`, `{{subject}}` and `{{html}}` are substituted into the *parsed* JSON template rather than into raw text, so values stay escaped whatever the HTML body contains. No new dependency — `fetch` is built in on the supported Node (>= 20). Selection runs after Mailgun and SMTP, so existing setups are untouched. The sender keeps coming from `SMTP_FROM`, as it already does for Mailgun. Closes #6976 ## Testing instructions `npx vitest run --dir=packages/email` — 8 tests, the first ones in this package. They cover placeholder substitution (nested objects and arrays, non-string values, HTML containing quotes and newlines) and `send` (URL/headers/payload posted, and the error raised on a non-2xx response). End to end: set the three vars above with a real API key, then sign up locally and check the verification email is delivered. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7029?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Ross McEwan <ross@nango.dev> | 26 天前 | |
feat(webapp): show customers the usage and limits they're billed on using new pricing (#7251) ## Problem The usage view lists seven metrics and formats every figure as a count. Under the new pricing three of them are what a customer is charged for; the other five describe an invoice they no longer appear on. Data transfer has never been shown at all. The units are wrong too: compute arrives in whole started seconds and transfer in bytes, while both are priced in hours and GB — so a Free compute row reads `16.1M / 50M`. And the cap warnings roll up every capped metric, so an account keeps being warned about records and proxy requests it isn't billed for. ## Solution **Not customer-facing yet.** Everything sits behind a new S26 pricing flag that defaults off; with it off the view is unchanged. - Unleash flag keyed on the account, exposed as a boolean on `GET /api/v1/meta` (the dashboard can't read `packages/feature-flags`). An allowlist can enable it for one account first. - Metric set chosen per plan, exhaustive over `DBPlan['name']` so a new plan code fails to compile until classified. `free` and `free-uncapped` get the new three; everything else keeps the seven. No API change — `/plans/usage` already returns every metric. - Compute in decimal hours, transfer in GB/TB, at every display point: cell, hover title, progress bar, `% of limit`, chart headline, Y axis, tooltip, cap line. Counts unchanged. The conversions match Orb's new `pay-as-you-go` metrics exactly — `SUM(count) / 1000000000` for GB (decimal, not 2³⁰) and `SUM(durationSeconds) / 3600` for hours. - A figure and the cap beside it share one scale, so the pair can't mix GB with TB. - Both cap warnings — the one in the sidebar and the banner on the page — now only look at the metrics the account is billed on, so they can't say different things. The function that rolls them up takes that list as a **required** argument, so forgetting to narrow it breaks the build rather than quietly warning about a metric nobody is charged for. - Data transfer's Group/Filter controls are hidden: the server answers a breakdown with `200` and the metric absent, which zero-fills, so a control would draw a blank chart ([NAN-6752](https://linear.app/nango/issue/NAN-6752)). Fixes [NAN-6723](https://linear.app/nango/issue/NAN-6723) ## Testing 26 new unit cases: the hour and GB/TB conversions, shared-scale pairing, the `<0.01` floor, alert scoping, and the plan classification (exhaustive, so a wrong answer fails rather than a missing plan). <img width="1214" height="331" alt="image" src="https://github.com/user-attachments/assets/27ddb2fd-1f27-4b46-b12f-2ed1a73f0e24" /> ## Follow-ups - `pay-as-you-go` exists in Orb but not as a plan code here, so no paid account can reach the new set yet. [NAN-6741](https://linear.app/nango/issue/NAN-6741) adds the definition; the exhaustive map will fail the build until it's classified. - Transfer shows a figure but no limit: no `data_transfer_max` column ([NAN-6729](https://linear.app/nango/issue/NAN-6729)). - Compute shows no limit until [NAN-6734](https://linear.app/nango/issue/NAN-6734) sets the cap — worth landing as 36,000s, since 50,000 renders as `13.89h` rather than the `10h` on the pricing page. - Enterprise stays on the legacy set until its path is decided ([NAN-6726](https://linear.app/nango/issue/NAN-6726)). --------- Co-authored-by: Matej Vobornik <matej@nango.dev> | 9 天前 | |
fix: upgrade dd-trace (#6905) <!-- Describe the problem and your solution --> <!-- Issue ticket number and link (if applicable) --> <!-- Testing instructions (skip if just adding/editing providers) --> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/6905?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 1 个月前 | |
chore(release): 0.71.6 | 4 天前 | |
fix(runner): auth runner start (#7288) ## Summary - Require a jobs identity on runner `start` / `abort` / `notifyWhenIdle` when `NANGO_INTERNAL_AUTH_REQUIRED=true`. Jobs mints an EdDSA JWT (`aud: runner`); runners verify with a public key only. - Jobs never puts a minting secret on the runner. At node start it injects the Ed25519 public key, a jobs-audience node JWT, and a snapshot of `REQUIRED`. `/health` stays open. - Default is a no-op. Existing runners keep accepting dispatch (fail-open). Enforcement starts on pods created after jobs has a signing key and `REQUIRED=true`. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> | 5 天前 | |
feat(plans): cap free function runtime instead of legacy metrics (#7315) Update the Free plan’s usage caps to use function runtime seconds. - Keep the 10-connection limit - Cap function runtime at 10 hours per month - Remove caps from legacy usage metrics - Add coverage for the Free plan cap configuration <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7315?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 5 天前 | |
feat(server): mint and validate agent session tokens (NAN-6596) (#7169) NAN-6596 Agent sessions need a credential of their own until the unified authz project lands. This reuses the keystore's hashed private key minting from connect sessions, with the session's expiry as the token TTL and only the hash stored since the token is handed out once at creation. Tokens look like `nango_agent_session_` plus 64 hex chars. The new `agentSessionAuth` middleware resolves a bearer token to account, environment and session, and rejects ended or expired sessions. Nothing routes through it yet, that comes with the MCP endpoint (NAN-6600). Non-obvious decisions: - `private_keys.entity_id` was integer only and agent session ids are uuids, so the keystore gains a nullable `entity_uuid` column and `createPrivateKey` takes a discriminated entity ref. Existing callers are unchanged. - Ended and expired are checked on the session row in the middleware, not just left to the keystore TTL. Per the authz RFC these tokens will later become customer keys with grants scoped to env and session, so all mint and resolve logic sits behind `createAgentSessionToken` and `getAgentSessionByToken` to keep that switch local. Test plan: - [x] keystore mints and resolves a uuid entity key, hash only - [x] token resolves back to its session, unknown and cross entity tokens are rejected - [x] minting refuses an already expired session, resolution stops at token expiry - [x] connect session token path unaffected (getSession, postUnauthenticated suites) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7169?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 17 天前 | |
chore(kms): rename file (#7307) - Missed change from previous PR - Rename file <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7307?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 5 天前 | |
feat(kvstore): sliding window rate limiter (NAN-6609) (#7116) Adds a generic approximate sliding-window rate limiter with partial admission, weighted retry timing, Redis atomicity, and an in-memory fallback. Redis failures are logged and fail open. Idle Redis keys expire. The limiter stores only the current and previous fixed-window counts. It estimates rolling usage by weighting the previous count by the fraction of the current window that remains. This keeps memory and Lua work constant while smoothing the fixed-window boundary. This approximation fits webhook dispatch because Standard SQS buffers fanout, consumers process bounded batches, and NAN-6404 will back off rejected work. The generic limit remains the actual enforcement threshold. NAN-6404 should initially pass 80 to 85 percent of the desired ceiling and tune it with production metrics. Reference: https://blog.cloudflare.com/counting-things-a-lot-of-different-things/#sliding-windows-to-the-rescue NAN-6404 is the first consumer. Test plan: - [x] Run kvstore unit tests - [x] Run kvstore Redis integration tests - [x] Run the full TypeScript build - [x] Run lint and formatting checks | 16 天前 | |
feat(auth): internal service auth (#7167) Optional Bearer auth between internal services (orchestrator and jobs). Off by default; existing deploys are unchanged until operators set secrets and flip `NANGO_INTERNAL_AUTH_REQUIRED`. ## Summary - Orchestrator and jobs HTTP APIs accept `Authorization: Bearer`, except `GET /health`. - Control plane (server, jobs, orchestrator) uses a shared static token (`NANGO_INTERNAL_AUTH_TOKEN`). - Jobs mints HMAC JWTs for runners: task-bound for `putTask`/`heartbeat`, node-bound for register/idle. The signing key stays on jobs; runners never receive `NANGO_INTERNAL_AUTH_TOKEN` or `NANGO_INTERNAL_AUTH_SIGNING_KEY`. Rollout plan [here](https://linear.app/nango/issue/NAN-6634/add-internal-auth-middleware-to-orchestrator-and-jobs#zero-downtime-rollout-2e12b4cc) --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> | 11 天前 | |
feat(server): Connection deleted webhook (#7235) <!-- Describe the problem and your solution --> Adds connection webhook deletion so customers can take appropriate cleanup actions on a connection deletion event <!-- Issue ticket number and link (if applicable) --> [NAN-5591: feat(server): Connection deleted webhook](https://linear.app/nango/issue/NAN-5591/featserver-connection-deleted-webhook) <!-- Testing instructions (skip if just adding/editing providers) --> 1. Configure your webhooks locally to point somewhere like [webhook.site](https://webhook.site/) or a local server for testing 2. Create a new connection if required 3. Delete connection 4. Receive webhook: { "operation": "deletion", "success": true, "type": "auth", ... } 5. Check log for associated entry <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7235?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> | 6 天前 | |
feat(audit): name api keys and environments by their uuid (#7319) Stacked on #7309. That PR moved the public environment API to UUIDs and converted the two public `api_key` audit targets with it, which left the trail using two identifiers for the same object: a key created through the dashboard was named by its internal id, one created through the public API by its uuid, and the key that *made* a call by its internal id either way. Nothing could be joined to anything. This makes the uuid the only identifier the trail uses. - **The five remaining `api_key` targets use the uuid.** The two numeric-route lookups already read the row for a display name, so the uuid costs no extra query — they only needed renaming, since `getApiKeyDisplayName` returning a uuid would be a lie. They are `getApiKeyById` / `getAccountApiKeyById` now, mirroring `getApiKeyByUuid` from #7309. - **The actor uses the uuid.** The auth query already joins `customer_keys`, so `uuid` rides along on the existing select and reaches `resolveActor` through the auth context and `res.locals`. A sandbox token resolves to the key it was minted from, which is the object its numeric id already named; only `api_secret` and `env_var` have no key row and keep the internal id. - **The event's environment is named by its uuid**, which makes `AuditEvent.environment.id` a string. The numeric environment id is dropped from the audit subject entirely — nothing in the emit path ever read it, so 25 call sites got shorter. - **Both creation endpoints and the environment key list return the uuid**, and the dashboard shows it as a read-only copyable field on environment settings and on the api key detail, in both cases directly after the name. ## Notes for review The stored contract changes shape. That is only safe because the trail is not public yet and the table can be truncated; the truncation now covers three shape changes — `scope` from #7310, the environment uuid, and the actor — so it should be a single truncate once all three land. ## Test plan - [x] Full unit suite: 4,262 tests across 317 files - [x] Full integration suite: 1,866 tests across 191 files — the CTE bug below is only reachable there - [x] `ts-build`, webapp typecheck, oxlint and prettier clean - [x] Actor uuid, both fallbacks, sandbox attribution, and all three uuid-valued `api_key` created targets break-checked - [x] Deployed to development and confirmed both dashboard fields render and copy - [ ] Confirm a dashboard-created key and a public-API-created key now record the same identifier <img width="1247" height="456" alt="Screenshot 2026-09-01 at 12 37 36" src="https://github.com/user-attachments/assets/7560a927-da27-41b8-b4c0-08fc09470e96" /> <img width="1219" height="369" alt="Screenshot 2026-09-01 at 12 37 20" src="https://github.com/user-attachments/assets/bacf47b3-150b-45f3-a156-533d51cc1a07" /> --------- Co-authored-by: Erick R <erickkrocha@gmail.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 5 天前 | |
chore(release): 0.71.6 | 4 天前 | |
chore(release): 0.71.6 | 4 天前 | |
fix(syncs): batch schedule search fan-out (#7318) ## Problem The connection Syncs tab returns a 500 for any connection with more than 1000 syncs. `getSyncs` sends one schedule name per sync in a single `POST /v1/schedules/search`, and that route caps `names` at 1000, so the orchestrator answers 400 and `getSyncs` throws. Levity hit this in prod with 1581 syncs on one connection. The connections list fans out the same way but fails soft, silently dropping the "Schedule paused" badge whenever a page of 20 connections totals over 1000 syncs. ## Solution - Chunk inside `Orchestrator.searchSchedules` rather than at each call site, so `getSyncs`, `getConnections` and `manager.service` are all covered and a new caller cannot reintroduce the fan-out. - Move the cap to `maxScheduleNamesPerSearch` so the client and the route schema cannot drift. - Skip the orchestrator call entirely when a connection has no syncs. - Drop the unused `TestOrchestratorService` from the orchestrator's public exports: shared now imports the package at runtime, and the main entry was loading this dead test helper (and its scheduler test-db import) into every consumer. Raising the cap instead only moves the ceiling: the scheduler resolves these names with a single `whereIn`. Fixes [NAN-6818](https://linear.app/nango/issue/NAN-6818) ## Testing - Unit tests cover batch boundaries, merging across batches, a failing later batch, and the empty case. - Verified locally against a seeded connection with 1601 syncs: the tab now returns 200 with all 1601 rows, each carrying `schedule_status`. Probing the orchestrator directly confirms the boundary — 1000 names → 200, 1001 → 400. ## Follow-ups - The tab still fetches every sync with no pagination and re-polls every 5s — [NAN-6819](https://linear.app/nango/issue/NAN-6819). | 4 天前 | |
fix(persist): stream deleteOutdatedRecords progress to avoid client timeout on large deletes (#7192) ## Describe the problem and your solution - stream deleteOutdatedRecords progress to avoid client timeout on large deletes <!-- Issue ticket number and link (if applicable) --> <!-- Testing instructions (skip if just adding/editing providers) --> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7192?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 6 天前 | |
chore(release): 0.71.6 | 4 天前 | |
fix: upgrade dd-trace (#6905) <!-- Describe the problem and your solution --> <!-- Issue ticket number and link (if applicable) --> <!-- Testing instructions (skip if just adding/editing providers) --> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/6905?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 1 个月前 | |
fix(persist): stream deleteOutdatedRecords progress to avoid client timeout on large deletes (#7192) ## Describe the problem and your solution - stream deleteOutdatedRecords progress to avoid client timeout on large deletes <!-- Issue ticket number and link (if applicable) --> <!-- Testing instructions (skip if just adding/editing providers) --> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7192?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 6 天前 | |
chore(release): 0.71.6 | 4 天前 | |
fix(runner): auth runner start (#7288) ## Summary - Require a jobs identity on runner `start` / `abort` / `notifyWhenIdle` when `NANGO_INTERNAL_AUTH_REQUIRED=true`. Jobs mints an EdDSA JWT (`aud: runner`); runners verify with a public key only. - Jobs never puts a minting secret on the runner. At node start it injects the Ed25519 public key, a jobs-audience node JWT, and a snapshot of `REQUIRED`. `/health` stays open. - Default is a no-op. Existing runners keep accepting dispatch (fail-open). Enforcement starts on pods created after jobs has a signing key and `REQUIRED=true`. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> | 5 天前 | |
feat(mcp): add deploy function tool (#7213) Adds the deploy_function Management MCP tool for starting asynchronous code deployments. Extracts the existing code-deployment flow into a reusable service shared by HTTP and MCP callers. NAN-6315 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7213?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 10 天前 | |
feat(scheduler): support per-group task cap overrides (#7154) Allow scheduler groups to override the global queued/running task cap through `group_overrides.task_cap`. The cap is resolved on each admission, so an operator can raise it for a group that already has a backlog. Task cap and max concurrency overrides remain independent. The migration makes `max_concurrency` nullable for cap-only rows and validates that task cap overrides are positive. NAN-6528 ## Test plan - [x] `npm run ts-build` - [x] `npm run test:integration -- packages/scheduler/lib/models/tasks.integration.test.ts` - [x] `npm run lint` - [x] Prettier check <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7154?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 16 天前 | |
feat(mcp): add providers get tool (#7283) Adds providers_get to the Management MCP server for fetching provider data and optionally catalog definitions. NAN-6292: https://linear.app/nango/issue/NAN-6292/mcp-tool-providers-get <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7283?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 4 天前 | |
fix(syncs): batch schedule search fan-out (#7318) ## Problem The connection Syncs tab returns a 500 for any connection with more than 1000 syncs. `getSyncs` sends one schedule name per sync in a single `POST /v1/schedules/search`, and that route caps `names` at 1000, so the orchestrator answers 400 and `getSyncs` throws. Levity hit this in prod with 1581 syncs on one connection. The connections list fans out the same way but fails soft, silently dropping the "Schedule paused" badge whenever a page of 20 connections totals over 1000 syncs. ## Solution - Chunk inside `Orchestrator.searchSchedules` rather than at each call site, so `getSyncs`, `getConnections` and `manager.service` are all covered and a new caller cannot reintroduce the fan-out. - Move the cap to `maxScheduleNamesPerSearch` so the client and the route schema cannot drift. - Skip the orchestrator call entirely when a connection has no syncs. - Drop the unused `TestOrchestratorService` from the orchestrator's public exports: shared now imports the package at runtime, and the main entry was loading this dead test helper (and its scheduler test-db import) into every consumer. Raising the cap instead only moves the ceiling: the scheduler resolves these names with a single `whereIn`. Fixes [NAN-6818](https://linear.app/nango/issue/NAN-6818) ## Testing - Unit tests cover batch boundaries, merging across batches, a failing later batch, and the empty case. - Verified locally against a seeded connection with 1601 syncs: the tab now returns 200 with all 1601 rows, each carrying `schedule_status`. Probing the orchestrator directly confirms the boundary — 1000 names → 200, 1001 → 400. ## Follow-ups - The tab still fetches every sync with no pagination and re-polls every 5s — [NAN-6819](https://linear.app/nango/issue/NAN-6819). | 4 天前 | |
fix: upgrade dd-trace (#6905) <!-- Describe the problem and your solution --> <!-- Issue ticket number and link (if applicable) --> <!-- Testing instructions (skip if just adding/editing providers) --> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/6905?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 1 个月前 | |
chore(release): 0.71.6 | 4 天前 | |
feat(usage): read all function metrics from v2 table (#7203) Route function executions, logs, and compute GB-ms queries through daily_function_executions_v2 now that the v2 history backfill is complete. Add a regression test covering all function metrics served by the v2 table. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7203?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> | 6 天前 | |
chore(release): 0.71.6 | 4 天前 | |
chore(release): 0.71.6 | 4 天前 | |
feat(server): Connection deleted webhook (#7235) <!-- Describe the problem and your solution --> Adds connection webhook deletion so customers can take appropriate cleanup actions on a connection deletion event <!-- Issue ticket number and link (if applicable) --> [NAN-5591: feat(server): Connection deleted webhook](https://linear.app/nango/issue/NAN-5591/featserver-connection-deleted-webhook) <!-- Testing instructions (skip if just adding/editing providers) --> 1. Configure your webhooks locally to point somewhere like [webhook.site](https://webhook.site/) or a local server for testing 2. Create a new connection if required 3. Delete connection 4. Receive webhook: { "operation": "deletion", "success": true, "type": "auth", ... } 5. Check log for associated entry <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/NangoHQ/nango/pull/7235?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> | 6 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 5 天前 | ||
| 6 天前 | ||
| 6 天前 | ||
| 4 天前 | ||
| 1 个月前 | ||
| 13 天前 | ||
| 2 个月前 | ||
| 5 天前 | ||
| 5 天前 | ||
| 4 天前 | ||
| 26 天前 | ||
| 9 天前 | ||
| 1 个月前 | ||
| 4 天前 | ||
| 5 天前 | ||
| 5 天前 | ||
| 17 天前 | ||
| 5 天前 | ||
| 16 天前 | ||
| 11 天前 | ||
| 6 天前 | ||
| 5 天前 | ||
| 4 天前 | ||
| 4 天前 | ||
| 4 天前 | ||
| 6 天前 | ||
| 4 天前 | ||
| 1 个月前 | ||
| 6 天前 | ||
| 4 天前 | ||
| 5 天前 | ||
| 10 天前 | ||
| 16 天前 | ||
| 4 天前 | ||
| 4 天前 | ||
| 1 个月前 | ||
| 4 天前 | ||
| 6 天前 | ||
| 4 天前 | ||
| 4 天前 | ||
| 6 天前 |