diff --git a/.agents/skills/add-block/SKILL.md b/.agents/skills/add-block/SKILL.md index 3f8c86f5fc7..d063e16b485 100644 --- a/.agents/skills/add-block/SKILL.md +++ b/.agents/skills/add-block/SKILL.md @@ -83,7 +83,8 @@ export const {ServiceName}Block: BlockConfig = { // Multi-line input { id: 'field', title: 'Label', type: 'long-input', placeholder: '...', rows: 6 } -// Password input +// Password input: only for legacy blocks or a secret with no saved-credential fit +// (model-provider keys, hosted-key pairs); a service token uses a saved credential { id: 'apiKey', title: 'API Key', type: 'short-input', password: true } ``` @@ -136,6 +137,32 @@ export const {ServiceName}Block: BlockConfig = { } ``` +A service-account-only picker (a saved token or key credential) instead sets +`credentialKind: 'service-account'` and `mode: 'basic'` (no scopes), and pairs with an advanced +twin; `isSavedCredentialPicker` matches only this shape: +```typescript +{ + id: 'credential', + title: '{Service} Account', + type: 'oauth-input', + serviceId: '{service}', + credentialKind: 'service-account', + canonicalParamId: 'oauthCredential', + mode: 'basic', + placeholder: 'Select {Service} credential', + required: true, +}, +{ + id: 'manualCredential', + title: '{Service} Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, +} +``` + **Scopes:** Always use `getScopesForService(serviceId)` from `@/lib/oauth/utils` for `requiredScopes`. Never hardcode scope arrays — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`. **Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`. @@ -159,6 +186,40 @@ export const {ServiceName}Block: BlockConfig = { Optional companions: `credentialLabels` (override the picker's section/connect-row copy) and `allowServiceAccounts: true` (trigger-mode only — list service accounts, which triggers otherwise exclude; set only when the trigger's polling path can resolve a service-account token). The connect modal, provider families (Google JSON key, Atlassian token, token-paste, client-credential, Slack bot), and the preview gate are all resolved from `serviceAccountProviderId` — you don't wire them per block. +### Credential-only successor (replacing inline secrets) + +A block that takes a secret inline keeps running for existing workflows; the saved-credential +version is a new block type that spreads it. Keep the successor an object literal with the picker +written inline, because the docs generator reads the source text: + +```typescript +export const ServiceV2Block: BlockConfig = { + ...ServiceBlock, + type: 'service_v2', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + sunset: undefined, + ...withSavedCredential(ServiceBlock, { + id: 'credential', + type: 'oauth-input', + serviceId: 'service', + title: 'Service Credential', + }), +} +``` + +- The legacy block changes only by `hideFromToolbar: true` and `sunset: { status: 'legacy', + replacedBy: 'service_v2' }` (an older `_v2` points at the new `_v3`). Register both types. +- `withSavedCredential` (`blocks/utils.ts`) swaps exactly the fields its policy in + `SAVED_CREDENTIAL_OAUTH` supplies for the picker, places it directly under the operation + dropdown, inherits the replaced fields' `condition` and `required`, and leaves trigger-mode + fields alone. Pass `required` when the replaced fields disagree (an optional host), and `params` + only when the legacy params builder validates the secret: extract that builder so the legacy + block calls it unchanged (`blocks/legacy-builders.test.ts`) and the successor skips the check. +- Add the pair to `serializer/saved-credentials.test.ts` (`optionalIds` for replaced fields the + legacy block never required), and grep for code that hard-codes the legacy type (`getBlock`, + `integration.type ===`, availability lookups), which now resolves to the successor. + ### OAuth deployment availability (required for integration blocks) A visible tools-category block with OAuth is deployment-gated. Its `oauth-input.serviceId` is @@ -661,7 +722,8 @@ outputs: { When creating V2 blocks (alongside legacy V1): ```typescript -// V1 Block - mark as legacy +// V1 Block - mark as legacy. A credential-only successor is the exception: the +// successor keeps the legacy block's name, so neither block is renamed. export const ServiceBlock: BlockConfig = { type: 'service', name: 'Service (Legacy)', diff --git a/.agents/skills/add-integration/SKILL.md b/.agents/skills/add-integration/SKILL.md index 71aa71c8b2c..ec33d091e2e 100644 --- a/.agents/skills/add-integration/SKILL.md +++ b/.agents/skills/add-integration/SKILL.md @@ -43,6 +43,12 @@ supported paths and choose the simplest supported setup for the intended user: | Customer-owned client credentials | Saved `service_account` credential with a client ID and secret | Internal-app eligibility, grant activation, scope syntax, token lifetime and revocation behavior | | API token or personal access token | Saved token service account when reusable connections are useful | Token permissions, identity verification, expiry and rotation | | Private key, certificate, or service-account JSON | Existing key-based service-account framework | Signing algorithm, audience, subject, tenant binding and key rotation | +| Cloud workload identity (AWS role, Databricks/Snowflake/Entra/GCP federation) | Keyless saved credential: Sim signs a short-lived OIDC token the customer's cloud trusts. Add the provider to every table in `lib/credentials/workload-identity/audiences.ts` (`WORKLOAD_IDENTITY_PROVIDERS`, its service-account provider ID map, `WORKLOAD_IDENTITY_KEYLESS_CONFIGS`, `WORKLOAD_IDENTITY_AUDIENCES`) and to `MIXED_MODE_PROVIDERS` in `lib/credentials/application/prepare-workload-identity.ts` | Issuer and audience the provider accepts, how the trust pins the subject, token lifetime | + +API, CLI, and MCP callers set up keyless in two steps: `POST /api/v2/credentials/workload-identity/subjects` +(or `sim credentials keyless-subject --output json`) returns the subject to trust and a +`keylessConfig`; they add its `requiredConfigFields` and create the credential with it. The provider +catalog advertises `keyless` only when the deployment can sign tokens. Do not present customer-owned credentials as a way around a provider's approval requirements for a shared public integration. Explain the supported account/app type in the setup docs. Keep @@ -66,6 +72,50 @@ Use explicit permission choices when the supported operations have different acc region and permission selections on reconnect unless the user changes them. Verify documented identity endpoints rather than guessing which person a client-credentials token represents. +### Token credentials (API key, personal access token, token + host) + +A service authenticated by a pasted key or token takes it through a saved token credential, never an +inline `password` field, so one shared, verified credential serves every block. Wire it by the +service id: + +1. `apps/sim/lib/credentials/token-service-accounts/descriptors.ts`: a provider id and descriptor. + Fields are `apiToken`, plus `domain` for a host, instance URL, or region and + `providerWorkspaceId` for an account or org id; every listed field is required. Set `url: true` + on a `domain` that is a URL whose scheme or path matters, so the connect form sends it as typed. +2. `validators/token-checks.ts`: one `tokenCheckValidator` declaration (register it in + `server.ts`). Make one cheap, documented read on the tools' own host with the tools' own auth, + reusing their header and URL builders. `domain` and `workspaceId` normalize to exactly the form + the tools build URLs from and throw for anything that could steer the host or path + (`normalizeHttpBaseUrl`, `allowedDomain`, `refuseUnsafeTypedUrl`); the normalized domain is what + the credential stores. Set `egress` when the user chooses the host. Add `forbiddenMeansScoped`, + `rejectsKey`, or `provesKey` only for documented replies (probe a fake key to see how a bad one + answers), and `errorBodyEchoesToken` when an error body echoes the token. +3. `apps/sim/lib/oauth/oauth.ts` and `types.ts`: the service entry (`authType: 'service_account'`, + `serviceAccountProviderId`). +4. The tools' credential policy. A NEW integration's tools take a hidden `accessToken` and declare + `oauth: { required: true, provider: '', credentialKind: 'service-account' }`, pinning + any host or account param with `authoritativeParams` (Flow's and Snowflake's tools are the + pattern). `tokenCredentialOAuth('', { : 'accessToken' | 'domain' | + 'providerWorkspaceId' })` in `apps/sim/tools/shared/saved-credentials.ts`, listed in + `SAVED_CREDENTIAL_OAUTH`, is for existing tools that already take the token inline (successors). + Either way, the executor overwrites mapped and pinned params after authorizing the credential, + and `check-block-registry` probes the real URL builders of every block with a saved-credential + picker and fails on a host-steering param left unmapped or unpinned. +5. A credential-only block (including a successor) uses `authMode: AuthMode.OAuth`, so the + generated catalog maps the service; a token-only block that keeps `AuthMode.ApiKey`, as Coda + does, registers in `tokenCredentialIntegrationTypes` instead (see below). The credential picker + sits directly under the operation dropdown (`add-block` → OAuth/Credentials). A new block + writes the picker inline, as Higgsfield does: `{ id: 'credential', type: 'oauth-input', serviceId, + credentialKind: 'service-account', canonicalParamId: 'oauthCredential', mode: 'basic', + required: true }`, plus a `manualCredential` `short-input` with the same `canonicalParamId` in + `mode: 'advanced'`. An existing block that already takes the secret inline is never edited in + place: add a credential-only successor (`add-block` → Credential-only successor). + +Then run `generate-docs`, `generate:block-successors`, `generate:deployment-config`, and +`generate:tool-metadata`, and `check-block-registry.ts origin/staging`. This pattern does not fit +hosted-key tools, model-provider keys, two-secret or username/password logins, or a host chosen +per call. + ### Pair saved credentials with block and resource selectors `oauth-input` is the shared saved-credential picker, including for service accounts that never @@ -425,7 +475,8 @@ If creating V2 versions (API-aligned outputs): 1. **V2 Tools** - Add `_v2` suffix, version `2.0.0`, flat outputs 2. **V2 Block** - Add `_v2` type, use `createVersionedToolSelector` -3. **V1 Block** - Add `(Legacy)` to name, set `hideFromToolbar: true`, and add +3. **V1 Block** - Add `(Legacy)` to name (a credential-only successor is the exception: it keeps + the legacy block's name), set `hideFromToolbar: true`, and add `sunset: { status: 'legacy', replacedBy: '{service}_v2' }` — `check-block-registry` fails a legacy block with no `replacedBy`, and the amber legacy badge plus its click-to-upgrade action read from that field. diff --git a/.agents/skills/add-tools/SKILL.md b/.agents/skills/add-tools/SKILL.md index ddcb7a625bd..5261ea8169d 100644 --- a/.agents/skills/add-tools/SKILL.md +++ b/.agents/skills/add-tools/SKILL.md @@ -89,6 +89,10 @@ export const {serviceName}{Action}Tool: ToolConfig< provider: '{service}', // Must match the service id in OAUTH_PROVIDERS // credentialKind: 'service-account', // service-account-only integrations }, + // A token credential (API key, PAT, token + host) instead uses the service's shared policy from + // tools/shared/saved-credentials.ts, built with tokenCredentialOAuth('{service}', { apiKey: + // 'accessToken' }). Its keys are the tool's own token/host/account params, which the executor + // fills after authorizing the credential. params: { // Hidden params (system-injected, e.g. the OAuth accessToken) diff --git a/.agents/skills/validate-integration/SKILL.md b/.agents/skills/validate-integration/SKILL.md index afc070e00b5..ee0b092cddf 100644 --- a/.agents/skills/validate-integration/SKILL.md +++ b/.agents/skills/validate-integration/SKILL.md @@ -218,6 +218,24 @@ For **each tool** in `tools.access`: - True/false → `switch` (a Yes/No `dropdown` only when the tool needs a third "unset" state) - Credentials → `oauth-input` with correct `serviceId` - Every tool with a `credential`/`oauthCredential` param declares `oauth: { required: true, provider }` matching that `serviceId`, plus `credentialKind: 'service-account'` when only service accounts are supported +- [ ] In a new block or credential-only successor, a pasted key, token, or token + host comes from a + saved token credential (add-integration → Token credentials), not an inline `password` field + (legacy blocks keep theirs); a successor changes its legacy block only by `hideFromToolbar` + and `sunset`, and its picker sits under the operation +- [ ] For a token-credential integration, every tool in `tools.access` declares the service's + `tokenCredentialOAuth` policy, every + param that decides the request host is mapped, and the stored domain is in the form the tools + build URLs from (`check-block-registry.ts origin/staging` passes) +- [ ] A non-successor token block's tools declare `oauth: { required: true, provider, + credentialKind: 'service-account' }` and pin every host or account param in + `authoritativeParams` (`check-block-registry` enforces this for every block with a + saved-credential picker) +- [ ] A keyless provider appears in every table in `lib/credentials/workload-identity/audiences.ts` + and in `MIXED_MODE_PROVIDERS` (`prepare-workload-identity.ts`), and the provider catalog + shows `keyless` only when the deployment can sign tokens +- [ ] The `tokenCheckValidator` request uses the tools' host and auth, its normalizers refuse + anything that could steer the host or path, and a fake key is rejected without a valid + scoped key being refused - [ ] Dropdown `value: () => 'default'` is set for dropdowns with a sensible default - [ ] Every `short-input`, `long-input`, `code`, and selector subBlock has a `placeholder` — including password fields (`Enter your API key`). Formatted values show the shape diff --git a/.github/CI.md b/.github/CI.md index 1831aefbb53..314b375df05 100644 --- a/.github/CI.md +++ b/.github/CI.md @@ -4,13 +4,13 @@ The `changes` job runs Turborepo 2.9.14's `affectedTasks` query before installin A PR compares the checked-out GitHub merge commit with its first parent: the actual target revision tested by that merge. The PR event's base SHA can become stale when the target advances. Scoping requires a matching GitHub merge ref and SHA, exactly two merge parents, and a usable target parent; otherwise every check runs. This works with the existing depth-two checkout without fetching older ancestry. Main/staging pushes execute all checks; the parent `ci` workflow skips checks on `dev` pushes, while PRs targeting `dev` still run checks. A manual `checks` run executes all checks unless supplied a full base commit SHA to reproduce a scoped plan. Missing history, an empty diff, unknown files outside the workspace graph, root script/CI changes, or a planner error run every check. Missing selection outputs fail the aggregate gate. The gate also rejects an omitted dependency, any failed/cancelled job, and any skip that its explicit selection flag did not authorize. -Dedicated docs verification uses the same planner, comparing pushes with their previous commit and PRs with the tested merge's first parent. Site acceptance follows `docs#build` and its transitive package dependencies. The app's docs-consumer task depends on server runtime and that docs build: backend-only changes run the platform consumer against the docs app, while site changes also run corpus and browser checks. Independent SDK and unit-only edits skip those production jobs. The required `docs HTTP` gate verifies explicit site/platform selections before accepting any skip. +Dedicated docs verification uses the same planner, comparing pushes with their previous commit and PRs with the tested merge's first parent. Site acceptance follows `docs#build` and its transitive package dependencies. The app's docs-consumer task depends on server runtime and that docs build: backend-only changes run the platform consumer against the docs app, while site changes also run corpus and browser checks. Standalone package and unit-only edits skip those production jobs. The required `docs HTTP` gate verifies explicit site/platform selections before accepting any skip. Docs production builds run once and publish an artifact to isolated HTTP, corpus, Chromium, and WebKit jobs. Source checks keep their own checkout because generator verification mutates inputs. Content caches use separate lint/source/build namespaces; generation validates every restore, repairs missing declarations, and removes obsolete modules. The Next build cache keeps its supported incremental behavior. Content and Next caches have size limits, and Next cache files expire after seven days; successful content eviction happens after all consumers finish. | Change | Expensive checks selected | | --- | --- | -| Independent SDK | That workspace's tests and type-check; no app build, services, or browsers | +| Standalone package | That workspace's tests and type-check; no app build, services, or browsers | | App unit test | App test shards and test type check | | Integration test | Both provision paths; no HTTP/browser job merely for the test edit | | Workspace UI | Database integration, mobile, SEO and live Electron, plus app tests/build | diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2e8af715a65..454529f0dfd 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -140,23 +140,19 @@ Using clear and consistent commit messages makes it easier for everyone to under To set up your local development environment: -### Option 1: Using NPM Package (Simplest) +### Option 1: Using the Setup Wizard (Simplest) -The easiest way to run Sim locally is using our NPM package: +The easiest way to run Sim locally is the interactive setup wizard: ```bash -npx simstudio +npx sim-setup ``` -After running this command, open [http://localhost:3000/](http://localhost:3000/) in your browser. - -#### Options - -- `-p, --port `: Specify the port to run Sim on (default: 3000) -- `--no-pull`: Skip pulling the latest Docker images +When it finishes, open [http://localhost:3000/](http://localhost:3000/) in your browser. Inside a cloned Sim repository, run `bun run sim-setup --mode dev` for the local development mode. #### Requirements +- Node.js 20+ - Docker must be installed and running on your machine ### Option 2: Using Docker Compose diff --git a/.github/actions/image/action.yml b/.github/actions/image/action.yml index 6ebf4468556..ff32a025bdb 100644 --- a/.github/actions/image/action.yml +++ b/.github/actions/image/action.yml @@ -63,7 +63,7 @@ runs: # under 300 MB, sat at 249 GB. Sticky disks bill at ~$0.51/GB-month. - name: Set up Blacksmith builder if: inputs.provider == '' || inputs.provider == 'blacksmith' - uses: useblacksmith/setup-docker-builder@19215110ab936351210feebdfa5b440b4493e184 # v2 + uses: useblacksmith/setup-docker-builder@06dd9c83a5d541274d2e3ac67625560979275267 # v2 with: cache-key: ${{ steps.cache-key.outputs.value }} diff --git a/.github/scripts/http-e2e.sh b/.github/scripts/http-e2e.sh index 8c5f185e7cd..83ff385d768 100755 --- a/.github/scripts/http-e2e.sh +++ b/.github/scripts/http-e2e.sh @@ -259,6 +259,9 @@ case "$group" in bun --no-env-file scripts/test-assistant-actions-e2e.ts ;; organization-search) + GITLAB_CSV_E2E_BASE_URL="$local_app_url" \ + GITLAB_CSV_E2E_REPORT_PATH="$report_dir/gitlab-csv-permissions-http-report.json" \ + bun --no-env-file scripts/test-gitlab-csv-permissions-e2e.ts ORGANIZATION_SEARCH_E2E_BASE_URL="$local_app_url" \ ORGANIZATION_SEARCH_E2E_DATABASE_URL="$DATABASE_URL" \ ORGANIZATION_SEARCH_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dff08f6ecfd..8986a857d27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -239,7 +239,7 @@ jobs: uses: aws-actions/amazon-ecr-login@03f1aad4c6c7ffd436567f42f9384779290529bd # v2 - name: Login to Docker Hub - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -404,14 +404,14 @@ jobs: uses: aws-actions/amazon-ecr-login@03f1aad4c6c7ffd436567f42f9384779290529bd # v2 - name: Login to Docker Hub - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR if: github.ref == 'refs/heads/main' - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -793,7 +793,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 - name: Login to GHCR - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -847,7 +847,7 @@ jobs: steps: - name: Login to GHCR - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -931,7 +931,7 @@ jobs: count: ${{ steps.resolve.outputs.count }} steps: - name: Login to GHCR - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -1112,7 +1112,7 @@ jobs: steps: - name: Login to GHCR - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -1161,7 +1161,7 @@ jobs: push-to-registry: true - name: Install Cosign - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + uses: sigstore/cosign-installer@756fe2d284c7292c4e9b62c8a28f1e5629c59532 # v4.1.3 # The attestations above prove how the image was built; this is the plain # signature that admission controllers (Kyverno, the Sigstore policy diff --git a/.github/workflows/docs-negotiation.yml b/.github/workflows/docs-negotiation.yml index 3cce1c8c815..f0ffc918a23 100644 --- a/.github/workflows/docs-negotiation.yml +++ b/.github/workflows/docs-negotiation.yml @@ -358,13 +358,13 @@ jobs: run: tar -I zstd -xf docs-build.tar.zst -C apps/docs - name: Set up Java for copied request examples if: matrix.group == 'http' - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 + uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0 with: distribution: temurin java-version: '17' - name: Set up .NET for copied request examples if: matrix.group == 'http' - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: dotnet-version: '8.0.x' - name: Set up Go for copied request examples diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index 2988995c3c0..f3e3fd7e9eb 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -176,7 +176,7 @@ jobs: version: v3.16.4 - name: Create kind cluster - uses: helm/kind-action@1544676c07bb3570fcb70166beaa07c4528fd4aa # v1.15.1 + uses: helm/kind-action@ea822c7bf1874f08e5a491df991c228edffc3376 # v1.15.2 with: version: v0.24.0 @@ -282,7 +282,7 @@ jobs: # oras also reads ~/.docker/config.json, so this one login covers both the # chart push and the Artifact Hub metadata push below. - name: Login to GHCR - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -372,7 +372,7 @@ jobs: - name: Install Cosign if: steps.exists.outputs.already == 'false' - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + uses: sigstore/cosign-installer@756fe2d284c7292c4e9b62c8a28f1e5629c59532 # v4.1.3 # Signed by digest, never by tag: a tag is a mutable pointer, so signing # one would attest to whatever it happens to reference later. The verify diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml deleted file mode 100644 index 5eef3cc7e1a..00000000000 --- a/.github/workflows/publish-cli.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: publish-cli - -on: - push: - branches: [main] - paths: - - 'packages/cli/**' - -permissions: - contents: read - -# A newer push waits for the running publish rather than racing its version check. -concurrency: - group: publish-cli-${{ github.ref }} - cancel-in-progress: false - -jobs: - build: - name: build - runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} - timeout-minutes: 15 - outputs: - version: ${{ steps.package_version.outputs.version }} - exists: ${{ steps.version_check.outputs.exists }} - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 - with: - persist-credentials: false - - - name: Setup workspace - uses: ./.github/actions/setup - with: - provider: ${{ vars.CI_PROVIDER }} - node-version: '22' - - - name: Build package - working-directory: packages/cli - run: bun run build - - - name: Get package version - id: package_version - working-directory: packages/cli - run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" - - - name: Check if version already exists - id: version_check - env: - VERSION: ${{ steps.package_version.outputs.version }} - run: | - if npm view "simstudio@$VERSION" version &> /dev/null; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - # Bun packs because it rewrites `workspace:` ranges to real versions, - # which `npm pack` would publish verbatim. - - name: Pack package - if: steps.version_check.outputs.exists == 'false' - working-directory: packages/cli - run: bun pm pack --ignore-scripts --filename "$RUNNER_TEMP/package.tgz" --quiet - - - name: Upload package - if: steps.version_check.outputs.exists == 'false' - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: package - path: ${{ runner.temp }}/package.tgz - if-no-files-found: error - retention-days: 1 - - - name: Log skipped publish - if: steps.version_check.outputs.exists == 'true' - env: - VERSION: ${{ steps.package_version.outputs.version }} - run: echo "Skipped publishing because version $VERSION already exists on npm" - - # npm trusted publishing trades this job's OIDC token for a short-lived npm - # credential, so no npm token is stored anywhere. npm accepts the token only - # from GitHub-hosted runners, and only this job can mint one: it runs no - # repository code, it publishes the tarball the build job packed. npm runs no - # lifecycle scripts for a tarball, so nothing is rebuilt here, and provenance - # is attached automatically. - publish: - name: publish - needs: build - if: needs.build.outputs.exists == 'false' - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - id-token: write - steps: - # Node 24 bundles npm 11.5.1+, which trusted publishing requires. - - name: Setup Node - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 - with: - node-version: '24' - - - name: Download package - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - name: package - path: ${{ runner.temp }} - - - name: Publish to npm - run: npm publish "$RUNNER_TEMP/package.tgz" --access public diff --git a/.github/workflows/publish-python-sdk.yml b/.github/workflows/publish-python-sdk.yml deleted file mode 100644 index 71e89894f5f..00000000000 --- a/.github/workflows/publish-python-sdk.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: publish-python-sdk - -on: - push: - branches: [main] - paths: - - 'packages/python-sdk/**' - -permissions: - contents: write - -# A newer push waits for the running publish rather than racing its version check. -concurrency: - group: publish-python-sdk-${{ github.ref }} - cancel-in-progress: false - -jobs: - publish: - name: publish - runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} - timeout-minutes: 15 - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 - with: - persist-credentials: false - - - name: Setup Python - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 - with: - python-version: '3.12' - cache: 'pip' - - - name: Install build dependencies - run: | - python -m pip install --upgrade pip - pip install build twine pytest requests tomli - - - name: Run tests - working-directory: packages/python-sdk - run: | - PYTHONPATH=. pytest tests/ -v - - - name: Get package version - id: package_version - working-directory: packages/python-sdk - run: echo "version=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")" >> $GITHUB_OUTPUT - - - name: Check if version already exists - id: version_check - # Exact lookup: PyPI answers 404 only for a version it has never published. Anything other - # than 200 or 404 stops the job rather than guessing. - env: - VERSION: ${{ steps.package_version.outputs.version }} - run: | - status="$(curl -sS -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/simstudio-sdk/$VERSION/json")" - case "$status" in - 200) echo "exists=true" >> "$GITHUB_OUTPUT" ;; - 404) echo "exists=false" >> "$GITHUB_OUTPUT" ;; - *) echo "::error::PyPI returned HTTP $status for simstudio-sdk $VERSION" >&2; exit 1 ;; - esac - - - name: Build package - if: steps.version_check.outputs.exists == 'false' - working-directory: packages/python-sdk - run: python -m build - - - name: Check package - if: steps.version_check.outputs.exists == 'false' - working-directory: packages/python-sdk - run: twine check dist/* - - - name: Publish to PyPI - if: steps.version_check.outputs.exists == 'false' - working-directory: packages/python-sdk - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: twine upload dist/* - - - name: Log skipped publish - if: steps.version_check.outputs.exists == 'true' - env: - VERSION: ${{ steps.package_version.outputs.version }} - run: echo "Skipped publishing because version $VERSION already exists on PyPI" - - - name: Create GitHub Release - if: steps.version_check.outputs.exists == 'false' - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: python-sdk-v${{ steps.package_version.outputs.version }} - name: Python SDK v${{ steps.package_version.outputs.version }} - body: | - ## Python SDK v${{ steps.package_version.outputs.version }} - - Published simstudio-sdk==${{ steps.package_version.outputs.version }} to PyPI. - - ### Installation - ```bash - pip install simstudio-sdk==${{ steps.package_version.outputs.version }} - ``` - - ### Documentation - See the [README](https://github.com/simstudioai/sim/tree/main/packages/python-sdk) or the [docs](https://docs.sim.ai/sdks/python) for more information. - draft: false - prerelease: false \ No newline at end of file diff --git a/.github/workflows/publish-sim-cli.yml b/.github/workflows/publish-sim-cli.yml index 2d87aa6bd92..8146da78d87 100644 --- a/.github/workflows/publish-sim-cli.yml +++ b/.github/workflows/publish-sim-cli.yml @@ -106,6 +106,16 @@ jobs: echo "tag=$TAG" } >> "$GITHUB_OUTPUT" + # `./embed` and the `#sim-cli/*` imports point at TypeScript source, which + # only the monorepo's own Next.js app consumes (it transpiles the package). + # The tarball ships `dist` alone, so published they would be dead entry + # points; the smoke test below fails on any export that does not exist. + - name: Drop source-only entry points + working-directory: packages/sim-cli + run: | + jq 'del(.imports, .exports["./embed"])' package.json > "$RUNNER_TEMP/package.json" + mv "$RUNNER_TEMP/package.json" package.json + # Bun packs because it rewrites `workspace:` ranges to real versions, # which `npm pack` would publish verbatim. The smoke test below and the # publish job both use this one tarball, so what was tested is what ships. @@ -126,6 +136,20 @@ jobs: SMOKE_DIR="$(mktemp -d "$RUNNER_TEMP/sim-cli-smoke.XXXXXX")" tar -xzf "$RUNNER_TEMP/package.tgz" -C "$SMOKE_DIR" test "$("$SMOKE_DIR/package/dist/index.js" --version)" = "$VERSION" + node -e ' + const fs = require("node:fs") + const path = require("node:path") + const root = path.join(process.argv[1], "package") + const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) + const targets = Object.values(pkg.exports ?? {}).flatMap((entry) => + typeof entry === "string" ? [entry] : Object.values(entry) + ) + const missing = targets.filter((target) => !fs.existsSync(path.join(root, target))) + if (missing.length > 0) { + console.error("Exports point at files the tarball does not contain: " + missing.join(", ")) + process.exit(1) + } + ' "$SMOKE_DIR" - name: Verify version is unpublished env: diff --git a/.github/workflows/publish-ts-sdk.yml b/.github/workflows/publish-ts-sdk.yml deleted file mode 100644 index 8a3c9c15b5e..00000000000 --- a/.github/workflows/publish-ts-sdk.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: publish-ts-sdk - -on: - push: - branches: [main] - paths: - - 'packages/ts-sdk/**' - -permissions: - contents: read - -# A newer push waits for the running publish rather than racing its version check. -concurrency: - group: publish-ts-sdk-${{ github.ref }} - cancel-in-progress: false - -jobs: - build: - name: build - runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} - timeout-minutes: 15 - outputs: - version: ${{ steps.package_version.outputs.version }} - exists: ${{ steps.version_check.outputs.exists }} - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 - with: - persist-credentials: false - - - name: Setup workspace - uses: ./.github/actions/setup - with: - provider: ${{ vars.CI_PROVIDER }} - node-version: '22' - - - name: Run tests - working-directory: packages/ts-sdk - run: bun run test - - - name: Build package - working-directory: packages/ts-sdk - run: bun run build - - - name: Get package version - id: package_version - working-directory: packages/ts-sdk - run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" - - - name: Check if version already exists - id: version_check - env: - VERSION: ${{ steps.package_version.outputs.version }} - run: | - if npm view "simstudio-ts-sdk@$VERSION" version &> /dev/null; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - # Bun packs because it rewrites `workspace:` ranges to real versions, - # which `npm pack` would publish verbatim. - - name: Pack package - if: steps.version_check.outputs.exists == 'false' - working-directory: packages/ts-sdk - run: bun pm pack --ignore-scripts --filename "$RUNNER_TEMP/package.tgz" --quiet - - - name: Upload package - if: steps.version_check.outputs.exists == 'false' - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: package - path: ${{ runner.temp }}/package.tgz - if-no-files-found: error - retention-days: 1 - - - name: Log skipped publish - if: steps.version_check.outputs.exists == 'true' - env: - VERSION: ${{ steps.package_version.outputs.version }} - run: echo "Skipped publishing because version $VERSION already exists on npm" - - # npm trusted publishing trades this job's OIDC token for a short-lived npm - # credential, so no npm token is stored anywhere. npm accepts the token only - # from GitHub-hosted runners, and only this job can mint one: it runs no - # repository code, it publishes the tarball the build job tested. npm runs no - # lifecycle scripts for a tarball, so nothing is rebuilt here, and provenance - # is attached automatically. - publish: - name: publish - needs: build - if: needs.build.outputs.exists == 'false' - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - id-token: write - steps: - # Node 24 bundles npm 11.5.1+, which trusted publishing requires. - - name: Setup Node - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 - with: - node-version: '24' - - - name: Download package - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - name: package - path: ${{ runner.temp }} - - - name: Publish to npm - run: npm publish "$RUNNER_TEMP/package.tgz" --access public - - release: - name: release - needs: [build, publish] - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: write - steps: - - name: Create GitHub Release - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: typescript-sdk-v${{ needs.build.outputs.version }} - name: TypeScript SDK v${{ needs.build.outputs.version }} - body: | - ## TypeScript SDK v${{ needs.build.outputs.version }} - - Published simstudio-ts-sdk@${{ needs.build.outputs.version }} to npm. - - ### Installation - ```bash - npm install simstudio-ts-sdk@${{ needs.build.outputs.version }} - ``` - - ### Documentation - See the [README](https://github.com/simstudioai/sim/tree/main/packages/ts-sdk) or the [docs](https://docs.sim.ai/sdks/typescript) for more information. - draft: false - prerelease: false diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 45232483d1c..3f8e6924c29 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -67,6 +67,6 @@ "react": "19.2.4", "react-dom": "19.2.4", "typescript": "^7.0.2", - "vitest": "^5.0.4" + "vitest": "^5.1.0" } } diff --git a/apps/docs/COPY_AUDIT.md b/apps/docs/COPY_AUDIT.md index 2691adeb91a..1cf3934e02d 100644 --- a/apps/docs/COPY_AUDIT.md +++ b/apps/docs/COPY_AUDIT.md @@ -637,8 +637,6 @@ Every row below represents a fully reviewed inventory entry. “Rewritten” inc | --- | --- | --- | --- | | [api-reference/authentication.mdx](content/docs/api-reference/authentication.mdx) | rewritten | Separated credential identity/current access from workspace-key management and operation policies; retained workspace payer/member attribution and personal/OAuth restrictions. Retained verified one-time key display/create/revoke instructions and exact Sim API keys label; qualified encryption and modern/legacy prefixes by API_ENCRYPTION_KEY. Kept API-key versus OAuth headers, scope table, method-independent scopes, account grant versus login revocation and API-key logout caveat. Replaced duplicate three-language request with curl and canonical Getting started/SDK links; folded repeated credential-safety text. | Verified improvement. Distinguishes acting identity, current role/policy, operation-specific workspace-key access, and payer instead of the baseline vague permissions table. Corrects API-key Bearer misuse, optional encryption/legacy prefix, OAuth scope/grant and logout behavior; removes duplicate HTTP language samples while linking complete versions. Preserved: Retains key creation/revocation/one-time display, X-API-Key, server-side credentials, modern prefix and OAuth scopes. Complete HTTP examples still exist in Getting started; OAuth/profile detail remains in CLI authentication. | | [api-reference/getting-started.mdx](content/docs/api-reference/getting-started.mdx) | rewritten | Separated Cloud/self-host origins, active deployment prerequisites, manual draft access, HTTP result versus in-band run failures, and request versus execution rate policies. Added checked env-backed direct requests, all current wait/terminal/pause states, async mode compatibility and client/server timeout distinction. Preserved one-shot X-Run-Id409 behavior and uncertain-run reconciliation; fixed pagination with required workspaceId and URL-encoded returned cursor. Preserved resource/list/error shapes, strict undeclared-field behavior and validation details while distinguishing OAuth401, usage402, conflict409 and USD/credits. Kept substantial first-call Steps/language Tabs; removed routine status/billing callouts. API navigation moves advanced workflow-sync after SDKs under API guides. Used shared authored tabs with padded mixed content, wrapped prose, visible horizontal scrolling, and mobile touch targets. | Verified improvement. Examples now define environment prerequisites and handle HTTP and in-band run failures; manual draft execution is distinguished from the active deployment. Explains actual status/pause/redaction, async option compatibility, one-shot run-ID recovery, endpoint-specific units, and bound cursor queries. Preserved: Retains origin/OpenAPI URL, API-key creation, ID lookup, deployment, three HTTP languages, async run/status URL, envelopes, strict unknown-field validation, pagination and Retry-After. | -| [api-reference/python.mdx](content/docs/api-reference/python.mdx) | rewritten | Retained every public method signature/default and complete return-field reference, added forced-sync method and missing retry timeout parameter. Replaced repeated Quick Start/Basic/Steps/env tabs with env-backed context manager; retained sequential multiple-workflow, file-like upload, async polling, retry/status/usage examples. Explained Python failed result versus exception, completed/paused/cancelled states, metadata units and unset logs/traces; preserved 0.2.x migration and mixed legacy/v2 helpers. Fixed usage requestsPerMinute/maxBurst fields and account/key-owner USD scope; Python storage omission explicit; corrected v2 reset milliseconds with legacy-unit caveat. Replaced broken SDK streaming example and done-event parser with raw HTTP SSE inspection and canonical framing/forwarding instructions; preserved Flask backend authentication, event preservation, status/timeout/cleanup concerns. Added redacting to polling and local wait deadline, distinguished client/server timeouts, output selectors, run fields/files/permissions and opaque large-value references. | Verified improvement. Replaces the misleading async/streaming/readiness claims with blocking HTTP, SDK-vs-HTTP streaming limits and failed/paused/cancelled result behavior. Keeps full method and return reference while consolidating repeated examples; corrects retry/reset units and usage shape. Preserved: Retains Python/requests requirements, context management/close, method options/defaults, types, baseURL, file inputs, async polling, retry, usage helpers and Apache-2.0 link. Legacy status/jobs and SDK migration remain explicit; runnable examples replace duplicate/incorrect samples. | -| [api-reference/typescript.mdx](content/docs/api-reference/typescript.mdx) | rewritten | Retained all public method signatures/options and complete declared/runtime return fields, added forced-sync convenience and LargeValueRef/legacyJob status distinctions. Used Bun install and Node18+ ESM requirement, scoped automatic globalFile conversion to Node20+, fixed Buffer-to-BlobPart typing using Uint8Array. Explained failed-run rejection versus cancelled/paused resolution and exact-state run lookup; async polling waits through runtime redacting without widening SDK types. Fixed legacy USD usage fields/storage, v2 reset-millisecond example, client wait versus server cap, rate-only retries and unsafe-retry recovery. Replaced broken SDK streaming and stale done-frame example with raw HTTP stream, terminal/reset handling, chunk boundaries and canonical protocol client links. Folded duplicate initialization/Steps/environment tabs into one setup, preserved backend helper and Express/Next.js POST/auth/input-size/status responsibilities; retained file formats and actual upload example. | Verified improvement. Corrects Node minimum/global File availability, failed-run rejection, paused success, absent synchronous status, unsupported SDK streaming, and timeout-without-abort. Preserves complete declarations and options while reducing repeated implementations; corrects retries/usage/reset units and shows HTTP framing. Preserved: Retains constructor and methods, options/defaults, types, setter/helpers, file and URL objects, server/backend authorization, async polling, retry, usage, legacy compatibility and license link. | | [api-reference/workflow-sync.mdx](content/docs/api-reference/workflow-sync.mdx) | rewritten | Reorganized prerequisites/import/fork/sync/verification/recovery and moved advanced navigation; retained every command/endpoint relationship and scope/role distinction. Made billing-enabled Enterprise versus billing-disabled deployment gate explicit; qualified export sanitization and kept unresolved env values/provenance non-authority. Retained source kind mappings, flat occurrence bindings/encodings/positions, null/required binding rules, dependentValues and selector scope/context/partial/restart bounds. Preserved fork drafts/explicit copies, push/pull direction/direct edge, mapping read/write projection, omitted/replaced/cleared saved choices and trigger URL adoption. Preserved request-ID/fingerprint apply commands, atomic import/idMap/plain-import exception, receipt workspace ownership, committed-follow-up failure and exact deployment readiness. Kept uncertainty reconciliation/stale preview recovery, rollback draft/copy limitations, unlink/exclusions; moved contributor harness details to existing canonical repository README link. | Verified improvement. Puts permissions and deployment gates before mutation steps; separates export/import, fork, direction-relative sync, receipt/recovery, and rollback consequences. Corrects confidentiality guarantees and adds selector bounds, fingerprint/retry safeguards, copy identity differences and exact deployment readiness. Preserved: Retains CLI↔HTTP map, all import/sync payload shapes, source IDs, nested occurrences/encodings, destination selectors, trigger adoption, request IDs, receipts and retry recovery. Contributor harness detail moved to a linked canonical README that contains Bun/Docker/PostgreSQL17/fixtures/migration boundary. | ### Knowledgebase (7) diff --git a/apps/docs/MACHINE_ACCESS.md b/apps/docs/MACHINE_ACCESS.md index 96b62913da9..2e2ea1f2b7f 100644 --- a/apps/docs/MACHINE_ACCESS.md +++ b/apps/docs/MACHINE_ACCESS.md @@ -33,7 +33,7 @@ Continue until `nextCursor` is `null`. Unknown cursors and invalid parameters re Configure a remote Streamable HTTP server at `https://docs.sim.ai/api/mcp`. This endpoint is separate from Sim's workspace MCP server and `/mcp/...` documentation pages. It requires no API key. -The official stable MCP TypeScript SDK 2.3.1 serves the 2026-07-28 protocol and stateless compatibility for 2025-era clients. Tools: +The official stable MCP TypeScript SDK 2.4.0 serves the 2026-07-28 protocol and stateless compatibility for 2025-era clients. Tools: | Tool | Input | Result | | --- | --- | --- | diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts index 6cbf1c069a0..b5b552cfde8 100644 --- a/apps/docs/app/llms.txt/route.ts +++ b/apps/docs/app/llms.txt/route.ts @@ -41,7 +41,7 @@ Sim is the open-source AI workspace where teams build, deploy, and manage AI age - Run a deployed workflow: \`POST ${SIM_SITE_URL}/api/v2/workflows/{workflowId}/execute\` with a JSON body like \`{ "input": { "fieldName": "value" } }\`. - Authenticate API keys with the \`X-API-Key\` header. \`Authorization: Bearer\` accepts only Sim OAuth access tokens, never API keys. - Responses wrap the payload in \`data\`. A synchronous run returns \`200 { "data": { "runId", "status", "output", ... } }\`; with \`"async": true\` in the body it returns \`202 { "data": { "runId", "statusUrl" } }\` and you poll \`statusUrl\`. -- SDKs: \`simstudio-ts-sdk\` (npm) and \`simstudio-sdk\` (PyPI). Full reference: [API getting started](${baseUrl}/api-reference/getting-started). +- Full reference: [API getting started](${baseUrl}/api-reference/getting-started). ## Documentation Overview diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index af90c3f47e6..39e82ed79c6 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -332,69 +332,100 @@ type IconComponent = ComponentType> export const blockTypeToIconMap: Record = { a2a: A2AIcon, affinity: AffinityIcon, + affinity_v2: AffinityIcon, agent: AgentIcon, agentmail: AgentMailIcon, + agentmail_v2: AgentMailIcon, agentphone: AgentPhoneIcon, + agentphone_v2: AgentPhoneIcon, agiloft: AgiloftIcon, ahrefs: AhrefsIcon, + ahrefs_v2: AhrefsIcon, airtable: AirtableIcon, algolia: AlgoliaIcon, + algolia_v2: AlgoliaIcon, amplitude: AmplitudeIcon, api: ApiIcon, api_trigger: ApiIcon, apify: ApifyIcon, + apify_v2: ApifyIcon, apollo: ApolloIcon, + apollo_v2: ApolloIcon, appconfig: AppConfigIcon, + appconfig_v2: AppConfigIcon, arxiv: ArxivIcon, asana: AsanaIcon, ashby: AshbyIcon, + ashby_v2: AshbyIcon, athena: AthenaIcon, + athena_v2: AthenaIcon, attio: AttioIcon, azure_data_explorer: AzureDataExplorerIcon, azure_devops: AzureIcon, + azure_devops_v2: AzureIcon, bitbucket: BitbucketIcon, box: BoxCompanyIcon, box_v2: BoxCompanyIcon, brandfetch: BrandfetchIcon, brex: BrexIcon, + brex_v2: BrexIcon, brightdata: BrightDataIcon, + brightdata_v2: BrightDataIcon, browser_use: BrowserUseIcon, + browser_use_v2: BrowserUseIcon, buffer: BufferIcon, + buffer_v2: BufferIcon, calcom: CalComIcon, calendly: CalendlyIcon, + calendly_v2: CalendlyIcon, cbinsights: CbInsightsIcon, chat_trigger: Task, checkr: CheckrIcon, + checkr_v2: CheckrIcon, circleback: CirclebackIcon, + circleback_v2: CirclebackIcon, clay: ClayIcon, clerk: ClerkIcon, + clerk_v2: ClerkIcon, clickhouse: ClickHouseIcon, clickup: ClickUpIcon, cloudflare: CloudflareIcon, + cloudflare_v2: CloudflareIcon, cloudformation: CloudFormationIcon, + cloudformation_v2: CloudFormationIcon, cloudtrail: CloudTrailIcon, + cloudtrail_v2: CloudTrailIcon, cloudwatch: CloudWatchIcon, + cloudwatch_v2: CloudWatchIcon, coda: CodaIcon, codepipeline: CodePipelineIcon, + codepipeline_v2: CodePipelineIcon, condition: ConditionalIcon, confluence: ConfluenceIcon, confluence_v2: ConfluenceIcon, context_dev: ContextDevIcon, convex: ConvexIcon, + convex_v2: ConvexIcon, credential: CredentialIcon, credential_group: GridOffset, crowdstrike: CrowdStrikeIcon, crunchbase: CrunchbaseIcon, + crunchbase_v2: CrunchbaseIcon, cursor: CursorIcon, cursor_v2: CursorIcon, + cursor_v3: CursorIcon, dagster: DagsterIcon, databricks: DatabricksIcon, + databricks_v2: DatabricksIcon, datadog: DatadogIcon, datagma: DatagmaIcon, daytona: DaytonaIcon, + daytona_v2: DaytonaIcon, deployments: Rocket, devin: DevinIcon, + devin_v2: DevinIcon, discord: DiscordIcon, + discord_v2: DiscordIcon, docusign: DocuSignIcon, downdetector: DowndetectorIcon, dropbox: DropboxIcon, @@ -403,22 +434,31 @@ export const blockTypeToIconMap: Record = { dspy: DsPyIcon, dub: DubIcon, dub_v2: DubIcon, + dub_v3: DubIcon, duckduckgo: DuckDuckGoIcon, dynamodb: DynamoDBIcon, + dynamodb_v2: DynamoDBIcon, dynatrace: DynatraceIcon, + dynatrace_v2: DynatraceIcon, ec2: EC2Icon, + ec2_v2: EC2Icon, elasticsearch: ElasticsearchIcon, elevenlabs: ElevenLabsIcon, + elevenlabs_v2: ElevenLabsIcon, emailbison: EmailBisonIcon, + emailbison_v2: EmailBisonIcon, embeddings: EmbeddingsIcon, enrich: EnrichSoIcon, + enrich_v2: EnrichSoIcon, enrichment: EnrichmentIcon, enrow: EnrowIcon, evaluator: ChartBarIcon, exa: ExaAIIcon, extend: ExtendIcon, extend_v2: ExtendIcon, + extend_v3: ExtendIcon, fathom: FathomIcon, + fathom_v2: FathomIcon, file: DocumentIcon, file_v2: DocumentIcon, file_v3: DocumentIcon, @@ -428,17 +468,22 @@ export const blockTypeToIconMap: Record = { firecrawl: FirecrawlIcon, fireflies: FirefliesIcon, fireflies_v2: FirefliesIcon, + fireflies_v3: FirefliesIcon, fivetran: FivetranIcon, flint: FlintIcon, + flint_v2: FlintIcon, flow: FlowIcon, function: CodeIcon, gamma: GammaIcon, + gamma_v2: GammaIcon, gauge: GaugeIcon, generic: WebhookIcon, generic_webhook: Webhook, github: GithubIcon, github_v2: GithubIcon, + github_v3: GithubIcon, gitlab: GitLabIcon, + gitlab_v2: GitLabIcon, gmail: GmailIcon, gmail_v2: GmailIcon, gong: GongIcon, @@ -468,9 +513,12 @@ export const blockTypeToIconMap: Record = { 'google-drive': GoogleDriveIcon, 'google-sheets': GoogleSheetsIcon, grafana: GrafanaIcon, + grafana_v2: GrafanaIcon, grain: GrainIcon, grain_v2: GrainIcon, + grain_v3: GrainIcon, granola: GranolaIcon, + granola_v2: GranolaIcon, greenhouse: GreenhouseIcon, greenhouse_v2: GreenhouseIcon, greptile: GreptileIcon, @@ -478,61 +526,82 @@ export const blockTypeToIconMap: Record = { harmonic: HarmonicIcon, helpscout: HelpScoutIcon, hex: HexIcon, + hex_v2: HexIcon, higgsfield: HiggsfieldIcon, hubspot: HubspotIcon, huggingface: HuggingFaceIcon, + huggingface_v2: HuggingFaceIcon, human_in_the_loop: HumanInTheLoopIcon, human_in_the_loop_v2: HumanInTheLoopIcon, hunter: HunterIOIcon, iam: IAMIcon, + iam_v2: IAMIcon, icypeas: IcypeasIcon, identity_center: IdentityCenterIcon, + identity_center_v2: IdentityCenterIcon, image_generator: ImageIcon, image_generator_v2: ImageIcon, imap: MailServerIcon, incidentio: IncidentioIcon, + incidentio_v2: IncidentioIcon, infisical: InfisicalIcon, + infisical_v2: InfisicalIcon, input_trigger: FormInput, instantly: InstantlyIcon, + instantly_v2: InstantlyIcon, intercom: IntercomIcon, intercom_v2: IntercomIcon, + intercom_v3: IntercomIcon, intune: IntuneIcon, jama: JamaIcon, jina: JinaAIIcon, jira: JiraIcon, jira_service_management: JiraServiceManagementIcon, jotform: JotformIcon, + jotform_v2: JotformIcon, jsm: JiraServiceManagementIcon, jupyter: JupyterIcon, jupyter_v2: JupyterIcon, + jupyter_v3: JupyterIcon, kalshi: KalshiIcon, kalshi_v2: KalshiIcon, ketch: KetchIcon, knowledge: PackageSearchIcon, lambda: LambdaIcon, + lambda_v2: LambdaIcon, langsmith: LangsmithIcon, + langsmith_v2: LangsmithIcon, latex: LatexIcon, launchdarkly: LaunchDarklyIcon, + launchdarkly_v2: LaunchDarklyIcon, leadmagic: LeadMagicIcon, lemlist: LemlistIcon, + lemlist_v2: LemlistIcon, linear: LinearIcon, linear_v2: LinearIcon, linkedin: LinkedInIcon, linkup: LinkupIcon, linq: LinqIcon, + linq_v2: LinqIcon, logfire: LogfireIcon, + logfire_v2: LogfireIcon, logrocket: LogRocketIcon, logs: Library, logs_v2: Library, loops: LoopsIcon, + loops_v2: LoopsIcon, luma: LumaIcon, + luma_v2: LumaIcon, mailchimp: MailchimpIcon, + mailchimp_v2: MailchimpIcon, mailgun: MailgunIcon, + mailgun_v2: MailgunIcon, managed_agent: ClaudeIcon, manageengine_sdp: ManageEngineIcon, manual_trigger: Play, mcp: McpIcon, mem0: Mem0Icon, + mem0_v2: Mem0Icon, memory: BrainIcon, microsoft_ad: AzureIcon, microsoft_dataverse: MicrosoftDataverseIcon, @@ -559,62 +628,80 @@ export const blockTypeToIconMap: Record = { netsuite: NetSuiteIcon, neverbounce: NeverBounceIcon, new_relic: NewRelicIcon, + new_relic_v2: NewRelicIcon, note: NoteIcon, notion: NotionIcon, notion_v2: NotionIcon, obsidian: ObsidianIcon, oci: OracleIcon, okta: OktaIcon, + okta_v2: OktaIcon, onedrive: MicrosoftOneDriveIcon, onepassword: OnePasswordIcon, openai: OpenAIIcon, oracle_fusion_financials: OracleIcon, oracledb: OracleIcon, otter: OtterIcon, + otter_v2: OtterIcon, outlook: OutlookIcon, pagerduty: PagerDutyIcon, + pagerduty_v2: PagerDutyIcon, parallel_ai: ParallelIcon, peopledatalabs: PeopleDataLabsIcon, perplexity: PerplexityIcon, persona: PersonaIcon, + persona_v2: PersonaIcon, pi: PiIcon, pinecone: PineconeIcon, pipedrive: PipedriveIcon, pitchbook: PitchBookIcon, plane: PlaneIcon, + plane_v2: PlaneIcon, planetscale: PlanetScaleIcon, polymarket: PolymarketIcon, postgresql: PostgresIcon, posthog: PosthogIcon, powerbi: PowerBIIcon, profound: ProfoundIcon, + profound_v2: ProfoundIcon, prospeo: ProspeoIcon, pulse: PulseIcon, pulse_v2: PulseIcon, + pulse_v3: PulseIcon, qdrant: QdrantIcon, quartr: QuartrIcon, + quartr_v2: QuartrIcon, quickbooks: QuickBooksIcon, quiver: QuiverIcon, quiver_v2: QuiverIcon, + quiver_v3: QuiverIcon, rabbitmq: RabbitmqIcon, railway: RailwayIcon, ramp: RampIcon, rb2b: RB2BIcon, + rb2b_v2: RB2BIcon, rds: RDSIcon, + rds_v2: RDSIcon, reddit: RedditIcon, redis: RedisIcon, reducto: ReductoIcon, reducto_v2: ReductoIcon, + reducto_v3: ReductoIcon, resend: ResendIcon, + resend_v2: ResendIcon, response: ResponseIcon, revenuecat: RevenueCatIcon, rippling: RipplingIcon, + rippling_v2: RipplingIcon, rocketlane: RocketlaneIcon, + rocketlane_v2: RocketlaneIcon, rootly: RootlyIcon, + rootly_v2: RootlyIcon, router: ConnectIcon, router_v2: ConnectIcon, rss: RssIcon, s3: S3Icon, + s3_v2: S3Icon, sailpoint: SailPointIcon, salesforce: SalesforceIcon, sap_concur: SapConcurIcon, @@ -622,14 +709,18 @@ export const blockTypeToIconMap: Record = { schedule: Clock, search: SearchIcon, secrets_manager: SecretsManagerIcon, + secrets_manager_v2: SecretsManagerIcon, semrush: SemrushIcon, sendblue: SendblueIcon, sendgrid: SendgridIcon, + sendgrid_v2: SendgridIcon, sentry: SentryIcon, + sentry_v2: SentryIcon, serper: SerperIcon, servicenow: ServiceNowIcon, servicenow_v2: ServiceNowIcon, ses: SESIcon, + ses_v2: SESIcon, sftp: SftpIcon, sftp_v2: SftpIcon, sharepoint: MicrosoftSharepointIcon, @@ -637,54 +728,77 @@ export const blockTypeToIconMap: Record = { shopify: ShopifyIcon, sim_workspace_event: SimTriggerIcon, similarweb: SimilarwebIcon, + similarweb_v2: SimilarwebIcon, sixtyfour: SixtyfourIcon, + sixtyfour_v2: SixtyfourIcon, slack: SlackIcon, slack_app: SlackIcon, slack_v2: SlackIcon, smartlead: SmartleadIcon, + smartlead_v2: SmartleadIcon, smtp: SmtpIcon, snowflake: SnowflakeIcon, splunk: SplunkIcon, sportmonks: SportmonksIcon, + sportmonks_v2: SportmonksIcon, sqs: SQSIcon, + sqs_v2: SQSIcon, square: SquareIcon, + square_v2: SquareIcon, ssh: SshIcon, ssh_v2: SshIcon, ssm: SSMIcon, + ssm_v2: SSMIcon, stagehand: StagehandIcon, start_trigger: StartIcon, starter: StartIcon, stripe: StripeIcon, + stripe_v2: StripeIcon, sts: STSIcon, + sts_v2: STSIcon, stt: STTIcon, stt_v2: STTIcon, supabase: SupabaseIcon, + supabase_v2: SupabaseIcon, table: Table, table_v2: Table, tailscale: TailscaleIcon, + tailscale_v2: TailscaleIcon, tavily: TavilyIcon, + tavily_v2: TavilyIcon, telegram: TelegramIcon, + telegram_v2: TelegramIcon, temporal: TemporalIcon, textract: TextractIcon, textract_v2: TextractIcon, + textract_v3: TextractIcon, thinking: BrainIcon, thrive: ThriveIcon, + thrive_v2: ThriveIcon, tiktok: TikTokIcon, tinybird: TinybirdIcon, + tinybird_v2: TinybirdIcon, tinyfish: TinyFishIcon, translate: TranslateIcon, trello: TrelloIcon, trigger_dev: TriggerDevIcon, + trigger_dev_v2: TriggerDevIcon, tts: TTSIcon, twilio: TwilioIcon, twilio_sms: TwilioIcon, + twilio_sms_v2: TwilioIcon, twilio_voice: TwilioIcon, + twilio_voice_v2: TwilioIcon, typeform: TypeformIcon, + typeform_v2: TypeformIcon, upstash: UpstashIcon, + upstash_v2: UpstashIcon, uptimerobot: UptimeRobotIcon, + uptimerobot_v2: UptimeRobotIcon, vanta: VantaIcon, variables: VariableIcon, vercel: VercelIcon, + vercel_v2: VercelIcon, video_generator: VideoIcon, video_generator_v2: VideoIcon, video_generator_v3: VideoIcon, @@ -695,6 +809,7 @@ export const blockTypeToIconMap: Record = { webflow: WebflowIcon, webhook_request: WebhookIcon, whatsapp: WhatsAppIcon, + whatsapp_v2: WhatsAppIcon, wikipedia: WikipediaIcon, windchill: WindchillIcon, wiza: WizaIcon, @@ -704,9 +819,12 @@ export const blockTypeToIconMap: Record = { workflow_input: WorkflowIcon, x: xIcon, youcom: YouComIcon, + youcom_v2: YouComIcon, youtube: YouTubeIcon, + youtube_v2: YouTubeIcon, zendesk: ZendeskIcon, zep: ZepIcon, + zep_v2: ZepIcon, zerobounce: ZeroBounceIcon, zoho_desk: ZohoDeskIcon, zoom: ZoomIcon, diff --git a/apps/docs/content/docs/api-reference/(generated)/workflows/meta.json b/apps/docs/content/docs/api-reference/(generated)/workflows/meta.json index 0416e441abe..4c6c32150a3 100644 --- a/apps/docs/content/docs/api-reference/(generated)/workflows/meta.json +++ b/apps/docs/content/docs/api-reference/(generated)/workflows/meta.json @@ -22,6 +22,8 @@ "importWorkflow", "getWorkflowDeployment", "updateWorkflowPublicApi", + "listWorkflowTriggers", + "updateWorkflowTrigger", "deployWorkflow", "undeployWorkflow", "rollbackWorkflow", diff --git a/apps/docs/content/docs/api-reference/authentication.mdx b/apps/docs/content/docs/api-reference/authentication.mdx index ea3279bf00a..58b962765fe 100644 --- a/apps/docs/content/docs/api-reference/authentication.mdx +++ b/apps/docs/content/docs/api-reference/authentication.mdx @@ -5,7 +5,7 @@ description: Choose an API key or delegated OAuth token and understand its acces import { Callout } from '@/components/ui/callout' -Authenticate API requests with a Sim API key, or an OAuth access token when your deployment enables OAuth. The Python and TypeScript SDKs use API keys. The CLI and registered OAuth applications can act with the permissions you approve. +Authenticate API requests with a Sim API key, or an OAuth access token when your deployment enables OAuth. The CLI and registered OAuth applications can act with the permissions you approve. ## Choose a key [#key-types] @@ -24,7 +24,7 @@ Workspace Admins can turn off **Allow personal API keys**. The workspace policy ## Create and store a key [#generating-api-keys] 1. Open **Account settings → Sim API keys** to create a personal key, or **Workspace settings → Sim API keys** to create a workspace key. -2. Select **Create API key**, enter a name, and create it. Workspace-key creation requires workspace **Admin** access and the applicable key-management permission. +2. Select **Create API key** and enter a name. Choose its **Access** and when it **Expires**, then create it. Workspace-key creation requires workspace **Admin** access and the applicable key-management permission. 3. Copy the key and store it in a server-side environment variable such as `SIM_API_KEY`. @@ -33,6 +33,23 @@ Sim shows the complete key once, immediately after creation. Copy it before clos Delete a key from the same settings page to revoke it. Update applications that used it with a replacement key. +### Access [#key-access] + +| Access | Allows | +| --- | --- | +| **Full access** | Everything the key's identity can do: reads, changes, deploys, and workflow runs | +| **Read only** | The operations the [`api:read` scope](#oauth-access-tokens) allows, including searches sent as POST requests and search chat | + +A read-only key cannot change workspace resources or run, resume, or cancel workflows. Reads can still cost credits: searches use embedding credits, and search chat uses model credits and keeps a private conversation history. Selector lookups call third-party services with your connected accounts. + +A refused request fails with `403` and the message `This API key is read-only and cannot perform this operation`. Where the surface carries a code, it is `READ_ONLY_API_KEY`: in `error.details.code` on the v2 API and the Sim MCP server, in `details.code` on the v1 API and workflow execution, and in `error.data.code` on a workflow MCP server. + +Access narrows a key; it never grants more than the key's identity already has. Access is fixed at creation, so create a new key to change it. Keys created before access levels existed have full access. Keys created from the deploy modal or for a workflow MCP server always have full access, because they exist to run workflows. If **Access** isn't shown when you create a key, read-only keys aren't available on your deployment yet. + +### Expiration [#key-expiration] + +A key expires 30 days, 90 days, or 1 year after creation, or never. The settings page defaults to never. After it expires, every request with the key fails with `401`. The v1 and v2 APIs, both MCP servers, and workflow execution answer with the message `API key has expired`. Create a replacement before then. The key list shows each key's expiration date. `GET /api/v2/meta` reports the calling key's `expiresAt` and `scopes`. + ## Send an API key [#using-api-keys] Send the key in `X-API-Key`. The v2 API accepts `Authorization: Bearer` only for [OAuth access tokens](#oauth-access-tokens), not API keys. For example, with `SIM_API_KEY` and `WORKFLOW_ID` set: @@ -45,7 +62,7 @@ curl --fail-with-body -X POST \ -d '{"input": {}}' ``` -This runs the active deployment. Use your deployment's origin for self-hosted requests. [Getting started](/api-reference/getting-started) includes complete HTTP examples and response handling; [Python](/api-reference/python) and [TypeScript](/api-reference/typescript) show SDK calls. +This runs the active deployment. Use your deployment's origin for self-hosted requests. [Getting started](/api-reference/getting-started) includes complete HTTP examples and response handling. Keys authenticate workflow execution, logs, deployed MCP servers, and other API resources that accept their key type. See each endpoint for its required access. @@ -72,8 +89,6 @@ Scopes limit the application's actions; your current workspace membership, role, Manage grants in **Settings → General → Authorized apps**. Revoking an application revokes all its logins for your account. For an OAuth login, `sim logout` revokes the current login and removes it locally; the CLI reports if server revocation fails. Removing a locally stored API key does not revoke that key on the server. -The Python and TypeScript SDKs do not manage OAuth sign-in or token refresh. - ## Protect credentials [#security] Keep API keys and OAuth tokens on your server. A browser application should call an authenticated backend that checks which resources the caller may use before sending a Sim request. Do not put keys in client-side code or source control. @@ -85,4 +100,3 @@ Sim supports modern `sk-sim-` and legacy `sim_` API keys. On self-hosted deploym - [API getting started](/api-reference/getting-started) - [CLI authentication](/cli/authentication) - [Permissions](/platform/permissions) -- [Python SDK](/api-reference/python) and [TypeScript SDK](/api-reference/typescript) diff --git a/apps/docs/content/docs/api-reference/getting-started.mdx b/apps/docs/content/docs/api-reference/getting-started.mdx index ad8a2acf426..9d0b352682e 100644 --- a/apps/docs/content/docs/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/api-reference/getting-started.mdx @@ -14,7 +14,7 @@ Sim Cloud uses `https://www.sim.ai`, with API paths under `/api/v2`. There is no Download the [OpenAPI 3.1 specification](/openapi.json) for client generation, request validation, and API tooling. -Official SDKs wrap these calls: [TypeScript](/api-reference/typescript) (`bun add simstudio-ts-sdk`) and [Python](/api-reference/python) (`pip install simstudio-sdk`). The [CLI](/cli) (`bun add -g sim`) runs workflows and manages resources from a terminal. +The [CLI](/cli) (`bun add -g sim`) runs workflows and manages resources from a terminal. ## Run a deployed workflow [#quick-start] @@ -243,6 +243,5 @@ A rate-limited request returns `429`; `Retry-After` gives seconds to wait. Inspe ## Related docs - [Authentication](/api-reference/authentication) -- [Python SDK](/api-reference/python) and [TypeScript SDK](/api-reference/typescript) - [Execute workflow](/api-reference/workflows/executeWorkflowV2) - [Workflow imports and workspace sync](/api-reference/workflow-sync) diff --git a/apps/docs/content/docs/api-reference/meta.json b/apps/docs/content/docs/api-reference/meta.json index 3f5de5114b6..413272d21ab 100644 --- a/apps/docs/content/docs/api-reference/meta.json +++ b/apps/docs/content/docs/api-reference/meta.json @@ -5,9 +5,6 @@ "---Getting started---", "getting-started", "authentication", - "---SDKs---", - "python", - "typescript", "---API guides---", "workflow-sync", "---Endpoints---", diff --git a/apps/docs/content/docs/api-reference/python.mdx b/apps/docs/content/docs/api-reference/python.mdx deleted file mode 100644 index e137b1e4689..00000000000 --- a/apps/docs/content/docs/api-reference/python.mdx +++ /dev/null @@ -1,402 +0,0 @@ ---- -title: Python -description: Execute deployed workflows from Python, poll runs, and handle SDK results. ---- - -import { Callout } from '@/components/ui/callout' - -Use `SimStudioClient` to execute deployed workflows from a Python application. It supports synchronous requests, server-queued asynchronous runs, rate-limit retries, and file-like inputs. Its HTTP methods are blocking; asynchronous execution does not make it an `asyncio` client. - -## Install and configure [#installation] - -The package requires Python `3.8+` and `requests>=2.25.0`: - -```bash -pip install simstudio-sdk -``` - -Create a key in **Account settings → Sim API keys**, or use a workspace key if your administrator requires one. Keep it on your server as `SIM_API_KEY`. Set `WORKFLOW_ID` to a deployed workflow you can access. See [Authentication](/api-reference/authentication) for key types and permissions. - -### Server compatibility - -SDK `0.2.x` executes and polls through `/api/v2`. Your deployment must serve `POST /api/v2/workflows/{workflowId}/execute`; the SDK does not fall back when that route is unavailable. Upgrade an older self-hosted server, or intentionally retain `simstudio-sdk<0.2` for its legacy execution endpoints. - -The default origin is `https://www.sim.ai`. Set `base_url` to your self-hosted deployment's origin. Versions before `0.2.1` defaulted to `https://sim.ai`; set the `www` origin explicitly when using an older SDK against Sim Cloud to avoid redirects. - -## Run a workflow [#quick-start] - -Use a context manager to close the HTTP session automatically: - -```python -import os -from simstudio import SimStudioClient, SimStudioError - -with SimStudioClient( - api_key=os.environ["SIM_API_KEY"], - base_url=os.getenv("SIM_BASE_URL", "https://www.sim.ai"), -) as client: - try: - result = client.execute_workflow_sync( - os.environ["WORKFLOW_ID"], - input={"message": "Process this data"}, - timeout=30.0, - ) - if result.status == "completed": - print(result.output) - print("Duration in ms:", result.total_duration) - elif result.status == "paused": - print("The workflow is waiting to resume.") - else: - print(result.status, result.error) - except SimStudioError as error: - print(error.code, error.status, str(error)) - raise -``` - -Replace `message` with an input field defined by your deployed trigger. The SDK runs the active deployment; it does not select manual draft execution. - -A failed synchronous workflow returns normally with `status="failed"`, `success=False`, and `error` set. A cancelled run also returns `success=False`, often without an error. `success=True` means `completed` **or** `paused`; a pause is unfinished work. HTTP, authentication, and client timeout errors raise `SimStudioError`. This differs from the TypeScript SDK, which throws for a failed synchronous run. - -## Start an asynchronous run [#async-workflow-execution] - -Queue a deployed run and poll its `run_id`. The initial `success=True` confirms acceptance, not completion: - -```python -import os -import time -from simstudio import AsyncExecutionResult, SimStudioClient - -waiting = {"queued", "pending", "running", "redacting"} -workflow_id = os.environ["WORKFLOW_ID"] - -with SimStudioClient(api_key=os.environ["SIM_API_KEY"]) as client: - result = client.execute_workflow( - workflow_id, - input={"message": "Process this dataset"}, - async_execution=True, - execution_timeout_seconds=3600, - ) - if not isinstance(result, AsyncExecutionResult): - raise RuntimeError("Expected an asynchronous run receipt") - - deadline = time.monotonic() + 300 - while True: - run = client.get_workflow_run(workflow_id, result.run_id, include_output=True) - if run["status"] not in waiting: - break - if time.monotonic() >= deadline: - raise TimeoutError(f"Stop waiting locally; run {result.run_id} may still be active") - time.sleep(2) - - if run["status"] == "completed": - print(run["output"]) - elif run["status"] == "paused": - print("Resume condition:", run["paused"]) - else: - print(run["status"], run["error"]) -``` - -The five-minute polling deadline is an application choice; it does not cancel the run. A time pause can resume automatically, while a human pause requires input. Resume can create another run attempt. Use [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) and the [resume endpoint](/api-reference/workflow-runs/resumeWorkflowRunV2) for the current state and resume behavior. - -Async submission cannot include `stream=True`, `selected_outputs`, or other output-shaping options. `execution_timeout_seconds` accepts an integer from `1` to `604800`, requires async execution, and only shortens the account policy. The HTTP `timeout` remains separate. - -## Handle errors and rate limits [#error-handling] - -`SimStudioError(message, code=None, status=None)` exposes `code` and the HTTP `status` when available. A workflow failure returned in `WorkflowExecutionResult` must be handled separately from exceptions. - -| Code | Meaning | -| --- | --- | -| `UNAUTHORIZED` | Missing, invalid, or expired credential | -| `BAD_REQUEST` | V2 request validation failed | -| `TIMEOUT` | The client stopped waiting | -| `RATE_LIMIT_EXCEEDED` | SDK wrapper for an HTTP `429` | -| `USAGE_LIMIT_EXCEEDED` | Usage policy rejected the request or run | -| `INVALID_EXECUTION_TIMEOUT` | An unsupported or invalid async timeout option | -| `EXECUTION_ERROR` | Transport, parsing, or execution-request failure | -| `STATUS_ERROR`, `USAGE_ERROR` | A status or usage helper failed | - -Other server codes can pass through. V2 uses `BAD_REQUEST` for invalid JSON/request input, rather than the legacy `INVALID_JSON` branch. See [API errors](/api-reference/getting-started#error-handling). - -`execute_with_retry` retries only rate-limit exceptions. Its defaults allow three retries after the initial attempt, with exponential delay and ±25% jitter. A supplied `Retry-After` takes precedence over the calculated delay; jitter applies to that wait too. - -```python -import os -from simstudio import SimStudioClient - -with SimStudioClient(api_key=os.environ["SIM_API_KEY"]) as client: - result = client.execute_with_retry( - os.environ["WORKFLOW_ID"], - input={"message": "Process this"}, - timeout=30.0, - max_retries=3, - initial_delay=1.0, - max_delay=30.0, - backoff_multiplier=2.0, - ) - print(result) -``` - -`initial_delay` and `max_delay` are **seconds**. `RateLimitInfo.retry_after` is **milliseconds**. `max_delay` caps the calculated backoff, not a server-supplied wait. - - -A client timeout or lost response does not prove the workflow never started. Avoid blindly repeating an execution that may have effects. For caller-chosen run IDs and reconciliation, use the raw v2 API described in [Getting started](/api-reference/getting-started#sync-vs-async-execution). - - -## Method reference [#api-reference] - -### Constructor - -```text -SimStudioClient(api_key: str, base_url: str = "https://www.sim.ai") -``` - -`api_key` is required. `base_url` is an origin, without an `/api` suffix; trailing slashes are removed. The client sends the key in `X-API-Key`. - -### Execute - -```text -execute_workflow(workflow_id: str, input=None, *, timeout=30.0, - stream=None, selected_outputs=None, async_execution=None, - execution_timeout_seconds=None) -> WorkflowExecutionResult | AsyncExecutionResult - -execute_workflow_sync(workflow_id: str, input=None, *, timeout=30.0, - stream=None, selected_outputs=None) -> WorkflowExecutionResult - -execute_with_retry(workflow_id: str, input=None, *, timeout=30.0, - stream=None, selected_outputs=None, async_execution=None, - execution_timeout_seconds=None, max_retries=3, initial_delay=1.0, - max_delay=30.0, backoff_multiplier=2.0) - -> WorkflowExecutionResult | AsyncExecutionResult -``` - -All options after `*` are keyword-only. `execute_workflow_sync` forces `async_execution=False`; the retry method accepts the same execution options as `execute_workflow`. - -| Parameter | Behavior and default | -| --- | --- | -| `workflow_id` | Deployed workflow ID | -| `input` | Optional value; a dictionary becomes the v2 `input` object, while a primitive or list becomes `{"input": value}` inside it; omitted/`None` becomes `{}` | -| `timeout` | Client HTTP timeout in seconds; default `30.0` | -| `async_execution` | Default omitted, so server execution is synchronous; `True` returns an async receipt | -| `execution_timeout_seconds` | Async-only integer cap, `1`–`604800`; cannot extend account policy | -| `selected_outputs` | Optional list of normalized block-name/path selectors such as `agent.content`; incompatible with async submission | -| `stream` | Defaults omitted/false; the current SDK parser cannot consume the SSE response when `True`; use raw HTTP below | -| `max_retries` | Retry count after the initial attempt; default `3` | -| `initial_delay`, `max_delay` | Calculated retry delay in seconds; defaults `1.0`, `30.0` | -| `backoff_multiplier` | Delay multiplier; default `2.0` | - -File-like objects nested in input are converted to base64. The current synchronous adapter does not expose v2 `blockOutputs`, even when `selected_outputs` is sent. Use a run read or raw HTTP when you need that map. - -### Read workflow deployment and runs - -```text -get_workflow_status(workflow_id: str) -> WorkflowStatus -validate_workflow(workflow_id: str) -> bool -get_workflow_run(workflow_id: str, run_id: str, *, - include_output=None, selected_outputs=None) -> Dict[str, Any] -get_job_status(job_id: str) -> Dict[str, Any] -``` - -`get_workflow_status` reads the legacy `/api/workflows/{id}/status` route. `validate_workflow` returns only its `is_deployed` flag; it returns `False` if the status request raises `SimStudioError`. It does not validate credentials, input, quota, or deployment readiness. Use the status method directly to distinguish a failed lookup from an undeployed workflow: - -```python -status = client.get_workflow_status("workflow-id") -print(status.is_deployed, status.needs_redeployment) -``` - -`get_workflow_run` reads and unwraps the v2 run resource. `include_output=True` includes final output for a completed run; default omitted/false. Run-read `selected_outputs` uses **block IDs**, as `blockId` or `blockId.path`, rather than block names. Selected block outputs are independent of `include_output`. - -`get_job_status` is for IDs created by legacy `0.1.x` async execution at `/api/jobs/{jobId}`. Use `get_workflow_run` for runs started by `0.2.x`. - -### Read limits and update the client - -```text -get_rate_limit_info() -> RateLimitInfo | None -get_usage_limits() -> UsageLimits -set_api_key(api_key: str) -> None -set_base_url(base_url: str) -> None -close() -> None -``` - -`get_rate_limit_info` reads cached headers from the most recent response that supplied rate information. It does not make a request. `get_usage_limits` calls the legacy account helper; see its shape below. Setters change subsequent requests. `close` closes the HTTP session; a context manager calls it automatically. - -## Return objects [#data-classes] - -### WorkflowExecutionResult - -| Field | Python type/default | Current behavior | -| --- | --- | --- | -| `success` | `bool`, required | `True` only for completed or paused | -| `status` | `Optional[str] = None` | Server status: completed, failed, paused, or cancelled | -| `output` | `Optional[Any] = None` | Workflow output, including any returned partial result | -| `error` | `Optional[str] = None` | Failure message, when present | -| `metadata` | `Optional[Dict[str, Any]] = None` | SDK-built `duration`, `runId`, `startTime`, `endTime`; duration is milliseconds | -| `total_duration` | `Optional[float] = None` | Duration in milliseconds | -| `logs`, `trace_spans` | `Optional[list] = None` | Compatibility fields; not populated by the v2 adapter | - -Nested oversized values may be `LargeValueRef` objects with `__simLargeValueRef`, `version`, `id`, `kind`, `size`, optional `key`, `executionId`, and `preview`. Their keys are opaque server pointers, not download URLs. See [large outputs](/workflows/deployment/api#oversized-outputs). - -### AsyncExecutionResult - -| Field | Python type/default | -| --- | --- | -| `success` | `bool`, required; current accepted receipts set `True` | -| `run_id` | `str`, required | -| `status_url` | `str`, required | -| `message` | `str = ""`; current adapter sets `"Workflow execution queued"` | -| `async_execution` | `bool = True` | - -### WorkflowStatus - -| Field | Python type/default | -| --- | --- | -| `is_deployed` | `bool`, required | -| `deployed_at` | `Optional[str] = None` | -| `needs_redeployment` | `bool = False` | - -### Run dictionary - -`get_workflow_run` returns the server's `data` dictionary without changing camelCase keys: - -| Field | Shape | -| --- | --- | -| `runId`, `workflowId` | String identifiers | -| `status` | `queued`, `pending`, `running`, `redacting`, `paused`, `completed`, `failed`, or `cancelled` | -| `trigger` | String or `None` | -| `startedAt`, `endedAt` | ISO timestamps or `None`; queued runs report enqueue time as startedAt | -| `durationMs` | Number or `None` | -| `paused` | Pause details or `None`; use `pauseKind` and `resumeAt` to identify the resume condition | -| `cost` | `{"total": number}` in credits, or `None` when unavailable/withheld | -| `error` | Structured `code`/`message` failure, or `None` | -| `output` | Final output when requested for a completed run, otherwise `None` | -| `blockOutputs` | Selected block-ID/path outputs, or `None` | -| `files` | Produced-file metadata when output is requested/available, otherwise `None` | - -Each file includes `id`, `name`, byte `size`, MIME `type`, `downloadPath`, and nullable `base64`. The SDK does not expose file-inlining options; download at the returned path with an authenticated raw HTTP request. Permission policies can withhold cost or recorded output fields. See [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) for the full contract, functional-output permission conflict, and file limits. - -### RateLimitInfo - -Fields are `limit: int`, `remaining: int`, `reset: int`, and `retry_after: Optional[int] = None`. V2 ISO reset headers become epoch **milliseconds**. Integer reset headers from older endpoints retain their original epoch-seconds value. For a v2 response: - -```python -from datetime import datetime, timezone - -rate = client.get_rate_limit_info() -if rate: - print(rate.remaining) - print(datetime.fromtimestamp(rate.reset / 1000, tz=timezone.utc)) -``` - -Use a known response surface when converting `reset`; use `resetAt` ISO strings from the usage helper when you need a format that is unambiguous across endpoints. - -### UsageLimits - -The dataclass contains `success: bool`, `rate_limit: Dict[str, Any]`, and `usage: Dict[str, Any]`: - -```text -rate_limit: - sync / async: - isLimited: bool - requestsPerMinute: number - maxBurst: number - remaining: number - resetAt: ISO timestamp - authType: "api" or "manual" -usage: - currentPeriodCost: number (USD) - limit: number (USD) - plan: str -``` - -`requestsPerMinute` is the refill rate; `maxBurst` is the bucket capacity. There is no `rate_limit["sync"]["limit"]` field. The Python dataclass does not expose the legacy route's `storage` result. - -```python -limits = client.get_usage_limits() -print("Sync remaining:", limits.rate_limit["sync"]["remaining"]) -print("Resets at:", limits.rate_limit["sync"]["resetAt"]) -print("Period cost in USD:", limits.usage["currentPeriodCost"]) -``` - -This helper resolves account/key-owner usage and can differ from the payer for a selected workspace. For workspace credit and storage state, use [Get billing status](/api-reference/billing/getBillingStatus). Logs APIs also report USD; v2 Billing and run-resource amounts use credits. - -## File inputs - -Include a binary file-like object under the input field your deployed trigger expects: - -```python -import os -from simstudio import SimStudioClient - -with SimStudioClient(api_key=os.environ["SIM_API_KEY"]) as client: - with open("document.pdf", "rb") as document: - result = client.execute_workflow_sync( - os.environ["WORKFLOW_ID"], - input={"documents": [document], "query": "Summarize this document"}, - ) - print(result.output) -``` - -The SDK reads and base64-encodes the bytes, retaining the file name and using its `content_type` attribute when available. Otherwise MIME type defaults to `application/octet-stream`. Validate upload size and content on your backend; the execute request has a 10 MiB JSON-body limit, including base64 overhead. - -## Execute several workflows [#batch-workflow-execution] - -The client has no batch-execution method. A sequential loop makes one request per workflow and handles each result independently: - -```python -for workflow_id, workflow_input in [ - ("workflow-1", {"message": "Analyze this"}), - ("workflow-2", {"message": "Summarize this"}), -]: - try: - result = client.execute_workflow_sync(workflow_id, workflow_input) - print(workflow_id, result.status, result.output, result.error) - except SimStudioError as error: - print(workflow_id, error.code, str(error)) -``` - -Use this inside the configured client context. A deployment-status precheck is optional and cannot guarantee the later run will succeed. - -## Stream through HTTP [#streaming-workflow-execution] - -The current `0.2.x` SDK sends `stream=True` but then parses the response as JSON. It does not expose an SSE iterator; a valid SSE response fails that parser. Use raw HTTP for streaming: - -```python -import os -import requests - -workflow_id = os.environ["WORKFLOW_ID"] -with requests.post( - f"https://www.sim.ai/api/v2/workflows/{workflow_id}/execute", - headers={ - "X-API-Key": os.environ["SIM_API_KEY"], - "X-Sim-Stream-Protocol": "agent-events-v1", - }, - json={"input": {"message": "Count to five"}, "stream": True, - "selectedOutputs": ["agent.content"]}, - stream=True, - timeout=(10, 300), -) as response: - response.raise_for_status() - for line in response.iter_lines(): - if line: - print(line.decode("utf-8")) -``` - -This prints raw SSE. Applications must parse complete events: success ends with `{event:"final",data:...}`, failure ends with `{event:"error",...}`, then `[DONE]`. Preserve structured output and reset events; do not filter only for `chunk` or the obsolete `done` event. The protocol header enables live answer tokens and requires handling `chunk_reset`; reasoning/tool events require their separate flags. - -For Flask or another web backend, authenticate the caller and authorize the workflow before opening the upstream request. Forward all SSE bytes with `text/event-stream`, check upstream HTTP status before starting the downstream response, and close the upstream response if the caller disconnects. See [API streaming](/workflows/deployment/api#streaming) and the [reference client](/workflows/deployment/agent-events#reference-client) for framing and event handling. - -## Upgrade from 0.1.x - -`0.2.0` changed execution requests to `/api/v2`, nests named input under `input`, and sends async/timeout options in the body instead of `X-Execution-Mode` and `X-Execution-Timeout-Seconds` headers. - -Replace `AsyncExecutionResult.job_id` with `run_id`; its old `execution_id` field is removed. Poll with `get_workflow_run(workflow_id, run_id)`, not `get_job_status`. Synchronous metadata is now SDK-built: use `metadata["runId"]` instead of the former `metadata["executionId"]`. Read `status` to distinguish failed and cancelled returns. Logs and trace spans are no longer populated by this adapter. - -Workflow status, legacy jobs, and account usage helpers still use their original routes. This SDK does not expose every v2 operation, manual execution, resume/cancel, caller-selected run headers, or NDJSON/agent-event streaming controls. - -## Related docs - -- [API authentication](/api-reference/authentication) -- [TypeScript SDK](/api-reference/typescript) -- [API getting started](/api-reference/getting-started) -- [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) -- [Python package source and Apache-2.0 license](https://github.com/simstudioai/sim/tree/main/packages/python-sdk) diff --git a/apps/docs/content/docs/api-reference/typescript.mdx b/apps/docs/content/docs/api-reference/typescript.mdx deleted file mode 100644 index 77070283633..00000000000 --- a/apps/docs/content/docs/api-reference/typescript.mdx +++ /dev/null @@ -1,428 +0,0 @@ ---- -title: TypeScript -description: Execute deployed workflows from a server, poll runs, and handle SDK results. ---- - -import { Callout } from '@/components/ui/callout' - -Use `SimStudioClient` from server-side JavaScript or TypeScript to execute deployed workflows. It includes TypeScript declarations, server-queued async execution, rate-limit retries, and file-input conversion. Browser applications should call an authenticated backend that keeps the Sim key private. - -## Install and configure [#installation] - -The ESM package requires Node.js `18+`: - -```bash -bun add simstudio-ts-sdk -``` - -Create a key in **Account settings → Sim API keys**, or use a workspace key if your administrator requires one. Set `SIM_API_KEY` and `WORKFLOW_ID` in your server environment. Deploy the workflow you intend to call; SDK execution uses the active deployment. See [Authentication](/api-reference/authentication) for key types and permissions. - -### Server compatibility - -SDK `0.2.x` executes and polls through `/api/v2`. Your deployment must serve `POST /api/v2/workflows/{workflowId}/execute`; the client does not fall back when that route is absent. Upgrade an older self-hosted server, or intentionally retain a `0.1.x` SDK for legacy execution. - -The default `baseUrl` is `https://www.sim.ai`. Set it to your self-hosted deployment's origin. Versions before `0.2.1` defaulted to `https://sim.ai`; set the `www` origin explicitly when using an older SDK against Sim Cloud to avoid redirects. - -## Run a workflow [#quick-start] - -```typescript -import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk' - -const apiKey = process.env.SIM_API_KEY -const workflowId = process.env.WORKFLOW_ID -if (!apiKey || !workflowId) throw new Error('Set SIM_API_KEY and WORKFLOW_ID') - -const client = new SimStudioClient({ - apiKey, - baseUrl: process.env.SIM_BASE_URL || 'https://www.sim.ai', -}) - -try { - const result = await client.executeWorkflowSync(workflowId, { message: 'Process this data' }) - const runId = result.metadata?.runId - if (!result.success) { - process.stdout.write('The workflow was cancelled.\n') - } else if (runId) { - const run = await client.getWorkflowRun(workflowId, runId, { includeOutput: true }) - process.stdout.write(JSON.stringify({ status: run.status, output: run.output, paused: run.paused })) - } -} catch (error) { - if (error instanceof SimStudioError) { - process.stderr.write(`${error.code}: ${error.message}\n`) - } - throw error -} -``` - -Replace `message` with a field your deployed trigger accepts. A failed synchronous run rejects with `SimStudioError`, carrying the run's error code/message. A resolved `success:false` means cancellation. `success:true` means completed **or paused**, so it does not guarantee the workflow finished. The synchronous SDK result has no `status` field; use its `metadata.runId` with `getWorkflowRun` for the exact state. - -The Python SDK instead returns a failed synchronous result normally. Both SDKs throw for rejected HTTP requests and client/transport errors. - -## Start an asynchronous run [#async-workflow-execution] - -The receipt's `success:true` confirms acceptance, not completion. Poll the returned run ID: - -```typescript -import { setTimeout as delay } from 'node:timers/promises' -import { SimStudioClient } from 'simstudio-ts-sdk' - -const apiKey = process.env.SIM_API_KEY -const workflowId = process.env.WORKFLOW_ID -if (!apiKey || !workflowId) throw new Error('Set SIM_API_KEY and WORKFLOW_ID') -const client = new SimStudioClient({ apiKey }) -const waiting = new Set(['queued', 'pending', 'running', 'redacting']) - -const receipt = await client.executeWorkflow(workflowId, { message: 'Process this dataset' }, { - async: true, - executionTimeoutSeconds: 3600, -}) -if (!('async' in receipt) || !receipt.async) throw new Error('Expected an async run receipt') - -const deadline = Date.now() + 300_000 -let run = await client.getWorkflowRun(workflowId, receipt.runId, { includeOutput: true }) -while (waiting.has(run.status)) { - if (Date.now() >= deadline) { - throw new Error(`Stop waiting locally; run ${receipt.runId} may still be active`) - } - await delay(2000) - run = await client.getWorkflowRun(workflowId, receipt.runId, { includeOutput: true }) -} -process.stdout.write(JSON.stringify({ status: run.status, output: run.output, error: run.error, paused: run.paused })) -``` - -The five-minute polling deadline is an application choice and does not cancel the run. Handle `completed`, `failed`, and `cancelled` separately. Inspect a `paused` run's resume condition: time pauses can resume automatically; human pauses require input. Resume can create another attempt. See [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) and [Resume workflow run](/api-reference/workflow-runs/resumeWorkflowRunV2). - -Async submission rejects `stream:true`, `selectedOutputs`, and other output-shaping options. `executionTimeoutSeconds` accepts an integer from `1` to `604800` and requires `async:true`. The effective cap is the smaller of this value and the account policy. Client `timeout` is separate and uses milliseconds. - -## Handle errors and retries [#error-handling] - -`new SimStudioError(message: string, code?: string, status?: number)` exposes a message, code, and HTTP status when available. An in-band failed run can throw without an HTTP error status. - -| Code | Meaning | -| --- | --- | -| `UNAUTHORIZED` | Missing, invalid, or expired credential | -| `BAD_REQUEST` | V2 request validation failed | -| `TIMEOUT` | The client stopped waiting | -| `RATE_LIMIT_EXCEEDED` | SDK wrapper for HTTP `429` | -| `USAGE_LIMIT_EXCEEDED` | A usage policy rejected the request or run | -| `INVALID_EXECUTION_TIMEOUT` | An unsupported or invalid async timeout option | -| `EXECUTION_ERROR` | Transport, response parsing, or execution-request failure | -| `EXECUTION_FAILED` | Fallback code for a failed synchronous run | -| `STATUS_ERROR`, `USAGE_ERROR` | A status or usage helper failed | - -Other server error codes pass through, including block/child execution failures. V2 reports malformed JSON/request input as `BAD_REQUEST`, rather than `INVALID_JSON`. See [API errors](/api-reference/getting-started#error-handling). - -`executeWithRetry` retries only rate-limit exceptions. By default it makes an initial attempt and up to three retries, using exponential backoff and ±25% jitter: - -```typescript -const result = await client.executeWithRetry('workflow-id', { message: 'Process this' }, { - timeout: 30000, -}, { - maxRetries: 3, - initialDelay: 1000, - maxDelay: 30000, - backoffMultiplier: 2, -}) -process.stdout.write(JSON.stringify(result)) -``` - -All retry delays and `RateLimitInfo.retryAfter` use **milliseconds**. A supplied `Retry-After` takes precedence over calculated backoff; jitter still applies. `maxDelay` caps the calculated delay, not a server-supplied wait. - - -A client timeout or lost response does not prove the run never started. The SDK's timeout stops waiting without aborting its fetch. Reconcile an uncertain execution before starting another run. Use raw v2 requests for caller-selected run IDs and [run-ID recovery](/api-reference/getting-started#sync-vs-async-execution). - - -## Method reference [#api-reference] - -### Constructor - -```text -new SimStudioClient(config: SimStudioConfig) -``` - -`SimStudioConfig` has required `apiKey:string` and optional `baseUrl:string`. Supply an origin without an `/api` suffix; the SDK removes trailing slashes and sends the key in `X-API-Key`. - -### Execute - -```text -executeWorkflow(workflowId: string, input?: any, options?: ExecutionOptions) - -> Promise -executeWorkflowSync(workflowId: string, input?: any, options?: SyncExecutionOptions) - -> Promise -executeWithRetry(workflowId: string, input?: any, options?: ExecutionOptions, - retryOptions?: RetryOptions) -> Promise -``` - -| `ExecutionOptions` field | Behavior and default | -| --- | --- | -| `timeout?:number` | Client wait in milliseconds; default `30000` | -| `async?:boolean` | Default omitted/false; true queues the active deployment | -| `executionTimeoutSeconds?:number` | Async-only integer cap, `1`–`604800`; cannot extend account policy | -| `selectedOutputs?:string[]` | Normalized block-name/path selectors such as `agent.content`; incompatible with async submission | -| `stream?:boolean` | Default omitted/false; the current SDK cannot parse SSE when true; use raw HTTP below | - -An object input becomes the v2 `input` object. A primitive or array is wrapped as `{input:value}` inside it; omitted/null input becomes `{}`. Nested `File` objects are converted when the runtime provides global `File`. - -`executeWorkflowSync` forces `async:false`. `SyncExecutionOptions` excludes `async` and `executionTimeoutSeconds`. `RetryOptions` has optional `maxRetries` (default `3`), `initialDelay` (`1000` ms), `maxDelay` (`30000` ms), and `backoffMultiplier` (`2`). - -The synchronous adapter does not expose v2 `blockOutputs`, even when `selectedOutputs` is supplied. Use a run read or raw HTTP when you need the selected-output map. - -### Read deployment and runs - -```text -getWorkflowStatus(workflowId: string) -> Promise -validateWorkflow(workflowId: string) -> Promise -getWorkflowRun(workflowId: string, runId: string, options?: GetWorkflowRunOptions) - -> Promise -getJobStatus(taskId: string) -> Promise -``` - -`getWorkflowStatus` reads the legacy `/api/workflows/{id}/status` route. `validateWorkflow` returns only `isDeployed`, and returns false if the status lookup fails. It does not verify credentials, inputs, quotas, or deployment readiness. Use the status method directly to distinguish a failed lookup from an undeployed workflow: - -```typescript -const deployment = await client.getWorkflowStatus('workflow-id') -process.stdout.write(JSON.stringify(deployment)) -``` - -`GetWorkflowRunOptions` contains `includeOutput?:boolean` and `selectedOutputs?:string[]`. Output is omitted/null by default; request it with `includeOutput:true` for a completed run. Run-read selectors use **block IDs**, as `blockId` or `blockId.path`, instead of block names. Selected block outputs are independent of `includeOutput`. - -`getJobStatus` polls IDs created by legacy `0.1.x` async execution at `/api/jobs/{taskId}`. Use `getWorkflowRun` for runs started by `0.2.x`. - -### Read limits and update the client - -```text -getRateLimitInfo() -> RateLimitInfo | null -getUsageLimits() -> Promise -setApiKey(apiKey: string) -> void -setBaseUrl(baseUrl: string) -> void -``` - -`getRateLimitInfo` reads cached headers from the most recent response that supplied rate information; it makes no request. `getUsageLimits` calls the legacy account helper. Setters affect subsequent requests. - -## Return types [#types] - -The SDK supplies declarations for these types. Arbitrary workflow input/output and log fields are not schema-typed by the client. - -### WorkflowExecutionResult - -| Field | Declared type | Current adapter behavior | -| --- | --- | --- | -| `success` | `boolean` | True for completed/paused; false for cancelled; failed rejects | -| `executionId` | Optional `string` | V2 run ID | -| `output` | Optional `any` | Workflow output | -| `error` | Optional `string` | Returned message when present | -| `metadata` | Optional object | `duration`, `executionId`, `runId`, `startTime`, `endTime`; permits other keys | -| `totalDuration` | Optional `number` | Duration in milliseconds | -| `logs`, `traceSpans` | Optional `any[]` | Compatibility fields; not populated by the v2 adapter | - -Metadata's duration is an optional number in milliseconds; identifiers/timestamps are optional strings. The current synchronous type has no `status` property. - -### AsyncExecutionResult - -Fields are `success:boolean`, `runId:string`, `statusUrl:string`, `message:string`, and `async:true`. The adapter sets `success:true` and `message:'Workflow execution queued'` for a `202` receipt. - -### WorkflowStatus - -Fields are `isDeployed:boolean`, optional `deployedAt:string`, `needsRedeployment:boolean`, and the optional compatibility field `isPublished:boolean`. The current status endpoint does not populate `isPublished`. - -### WorkflowRunStatus - -The server resource returned by `getWorkflowRun` includes: - -| Field | Shape | -| --- | --- | -| `runId`, `workflowId` | Strings | -| `status` | queued, pending, running, redacting, paused, completed, failed, or cancelled | -| `trigger` | String or null | -| `startedAt`, `endedAt` | ISO timestamp or null; queued startedAt is enqueue time | -| `durationMs` | Number or null | -| `paused` | Pause-detail object or null; inspect `pauseKind` and `resumeAt` | -| `cost` | `{total:number}` in credits, or null when unavailable/withheld | -| `error` | `WorkflowExecutionError` with code/message and optional details, or null | -| `output` | Requested completed-run output, otherwise null | -| `blockOutputs` | Selected block-ID/path values, or null | -| `files` | Produced-file metadata when output is requested/available, otherwise null | - -The `0.2.x` exported interface currently omits `redacting` and `files`, although the runtime returns the server object unchanged. The polling example uses a string set so it can wait through redaction without asserting an unsupported type. The interface declares `paused` and `blockOutputs` as nullable records, and the other fields with the nullability shown above. - -Files have `id`, `name`, byte `size`, MIME `type`, `downloadPath`, and nullable `base64`. The SDK exposes no file-inlining options; use the returned download path with an authenticated HTTP request. Permission policies can withhold costs or recorded outputs. See [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) for the full contract, functional-output permission conflict, and file limits. - -### JobStatusResult - -The legacy type has `taskId:string`, `status:string`, optional `metadata:Record`, `output:unknown`, and `error:string`. It describes legacy job polling, not a v2 run receipt. - -### LargeValueRef - -Oversized nested output can contain a placeholder with required `__simLargeValueRef:true`, `version:1`, `id:string`, `kind:'array'|'object'|'string'|'json'`, and `size:number`. Optional fields are `key:string`, `executionId:string`, and `preview:unknown`. These storage keys are opaque server pointers, not download URLs. See [large outputs](/workflows/deployment/api#oversized-outputs). - -### RateLimitInfo - -Fields are `limit:number`, `remaining:number`, `reset:number`, and optional `retryAfter:number`. V2 ISO reset headers become epoch **milliseconds**; older integer headers retain their epoch-seconds value. For v2: - -```typescript -const rate = client.getRateLimitInfo() -if (rate) { - process.stdout.write(`${rate.remaining} remaining; reset ${new Date(rate.reset).toISOString()}\n`) -} -``` - -Use a known response surface when converting reset values. The usage helper's `resetAt` ISO strings avoid the mixed legacy units. - -### UsageLimits - -```text -success: boolean -rateLimit: - sync / async: - isLimited: boolean - requestsPerMinute: number - maxBurst: number - remaining: number - resetAt: string (ISO timestamp) - authType: string (server values "api" or "manual") -usage: - currentPeriodCost: number (USD) - limit: number (USD) - plan: string -storage: - usedBytes: number - limitBytes: number - percentUsed: number -``` - -`requestsPerMinute` is the refill rate; `maxBurst` is the bucket capacity. There is no `rateLimit.sync.limit` field. - -```typescript -const limits = await client.getUsageLimits() -process.stdout.write(JSON.stringify({ - remaining: limits.rateLimit.sync.remaining, - resetAt: limits.rateLimit.sync.resetAt, - periodCostUSD: limits.usage.currentPeriodCost, - storageBytes: limits.storage.usedBytes, -})) -``` - -This legacy helper resolves account/key-owner usage and can differ from the selected workspace's payer. Use [Get billing status](/api-reference/billing/getBillingStatus) for workspace credits/storage. Logs APIs also report USD; v2 Billing and run-resource amounts use credits. If calculating a usage percentage, handle a zero limit before dividing. - -## File inputs [#file-upload] - -The SDK recursively converts global `File` objects to: - -```typescript -const inlineFile = { - type: 'file', - data: 'data:application/pdf;base64,BASE64_BYTES', - name: 'document.pdf', - mime: 'application/pdf', -} -``` - -You can also supply a URL file: - -```typescript -const urlFile = { - type: 'url', - data: 'https://example.com/document.pdf', - name: 'document.pdf', - mime: 'application/pdf', -} -``` - -Place files under the input field your deployed trigger expects. Run uploads on the backend after authenticating the caller and validating size/content. Base64 overhead counts toward the execute endpoint's 10 MiB JSON-body limit. - -For automatic conversion, this example requires **Node.js 20+**, where `File` is global: - -```typescript -import { readFile } from 'node:fs/promises' -import { SimStudioClient } from 'simstudio-ts-sdk' - -const apiKey = process.env.SIM_API_KEY -const workflowId = process.env.WORKFLOW_ID -if (!apiKey || !workflowId) throw new Error('Set SIM_API_KEY and WORKFLOW_ID') -const client = new SimStudioClient({ apiKey }) -const bytes = await readFile('./document.pdf') -const file = new File([new Uint8Array(bytes)], 'document.pdf', { type: 'application/pdf' }) -const result = await client.executeWorkflowSync(workflowId, { - documents: [file], - query: 'Summarize this document', -}) -process.stdout.write(JSON.stringify(result.output)) -``` - -On Node.js 18, use a serialized base64/URL object. Importing `File` from `node:buffer` only in the caller does not satisfy the SDK's global-File detection. - -## Use from a web backend [#browser-usage] - -Keep the key on the server. Authenticate the application caller, resolve a workflow they may run, validate the input, and only then call the SDK. A trusted server key does not authorize every browser caller to run every workflow. - -This helper is the SDK call to use after those checks: - -```typescript -async function executeAuthorizedWorkflow(workflowId: string, input: Record) { - return client.executeWorkflowSync(workflowId, input, { timeout: 60000 }) -} -``` - -### Express integration [#nodejs-express-integration] - -Call the helper from a POST handler after authentication and authorization middleware. Parse JSON with a request-size limit, map rejected SDK requests to an appropriate HTTP status, and preserve cancellation/paused state instead of wrapping every resolved result as completed success. - -### Next.js API route - -Call the same helper from an authenticated server route. In a Pages Router API handler, reject methods other than POST with `405`; in either router, authorize workflow selection before execution. Set the server's `SIM_API_KEY` environment variable and keep it out of browser bundles. - -## Stream through HTTP [#streaming-workflow-execution] - -The current `0.2.x` SDK sends `stream:true` but unconditionally parses JSON. It has no SSE iterator/callback and cannot consume a valid SSE response. Use raw HTTP instead: - -```typescript -const apiKey = process.env.SIM_API_KEY -const workflowId = process.env.WORKFLOW_ID -if (!apiKey || !workflowId) throw new Error('Set SIM_API_KEY and WORKFLOW_ID') - -const response = await fetch(`https://www.sim.ai/api/v2/workflows/${workflowId}/execute`, { - method: 'POST', - headers: { - 'X-API-Key': apiKey, - 'Content-Type': 'application/json', - 'X-Sim-Stream-Protocol': 'agent-events-v1', - }, - body: JSON.stringify({ input: { message: 'Count to five' }, stream: true, - selectedOutputs: ['agent.content'] }), -}) -if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`) -if (!response.body) throw new Error('Missing SSE response body') - -const decoder = new TextDecoder() -const reader = response.body.getReader() -try { - while (true) { - const { done, value } = await reader.read() - if (done) break - process.stdout.write(decoder.decode(value, { stream: true })) - } - process.stdout.write(decoder.decode()) -} finally { - reader.releaseLock() -} -``` - -This prints raw SSE; network chunks are not necessarily complete events. Success emits `{event:"final",data:...}`, failure emits `{event:"error",...}`, then `[DONE]`. Preserve output and reset events. The protocol header enables live answer tokens and requires `chunk_reset` handling; thinking/tool events require their separate flags. For browsers, have the authenticated backend forward the upstream stream, check HTTP status, and close/cancel it on disconnect. - -See [API streaming](/workflows/deployment/api#streaming) and the [reference client](/workflows/deployment/agent-events#reference-client) for event parsing and protocol details. Synchronous NDJSON heartbeats are a separate transport selected by `Accept: application/x-ndjson`; the SDK has no option for that header. - -## Upgrade from 0.1.x - -`0.2.0` changed execution to `/api/v2`, nests named input under `input`, and puts async/timeout options in the body instead of `X-Execution-Mode` and `X-Execution-Timeout-Seconds` headers. - -Replace `AsyncExecutionResult.jobId` with `runId`; its old `executionId` is removed. Poll with `getWorkflowRun(workflowId, runId)`, not `getJobStatus`. Failed synchronous runs now reject; move failure handling from a `!result.success` branch into `catch`. A resolved false result means cancellation; true also includes paused runs. - -Workflow status, legacy jobs, and account usage helpers still use their original routes. This SDK does not expose every v2 operation, manual draft execution, resume/cancel, caller-selected run headers, or NDJSON/agent-event streaming controls. - -## Related docs - -- [API authentication](/api-reference/authentication) -- [Python SDK](/api-reference/python) -- [API getting started](/api-reference/getting-started) -- [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) -- [TypeScript package source and Apache-2.0 license](https://github.com/simstudioai/sim/tree/main/packages/ts-sdk) diff --git a/apps/docs/content/docs/cli/authentication.mdx b/apps/docs/content/docs/cli/authentication.mdx index 491553f61f0..6f50b83d4c6 100644 --- a/apps/docs/content/docs/cli/authentication.mdx +++ b/apps/docs/content/docs/cli/authentication.mdx @@ -154,7 +154,7 @@ export SIM_WORKSPACE="2f6d0b1c-8a34-4d92-b7e5-31c8a0f45d67" sim workflows run 3a9e21d8-5f47-4c0b-b2ea-91d7c6034ef8 --input '{"source":"nightly"}' --output json ``` -The environment key overrides stored authentication. Use a workspace key only for supported workspace-scoped operations; organization audit logs and fork administration require a personal credential. Never commit credentials. `SIM_CONFIG_DIR` relocates local state when a runner has no writable home directory. +The environment key overrides stored authentication. Use a workspace key only for supported workspace-scoped operations; organization audit logs and fork administration require a personal credential. A job that runs workflows needs a **Full access** key; a **Read only** key suits jobs that only read, such as exporting logs. Choose an expiration your secret rotation covers: an expired key fails with `401` and `API key has expired`. See [key access and expiration](/api-reference/authentication#key-access). Never commit credentials. `SIM_CONFIG_DIR` relocates local state when a runner has no writable home directory. ### GitHub Actions diff --git a/apps/docs/content/docs/cli/credentials.mdx b/apps/docs/content/docs/cli/credentials.mdx index f0f98056185..4a516f7eb40 100644 --- a/apps/docs/content/docs/cli/credentials.mdx +++ b/apps/docs/content/docs/cli/credentials.mdx @@ -87,6 +87,24 @@ sim credentials create [options] +--credentials takes the provider's fields, or the keylessConfig from `sim credentials keyless-subject` (with the required fields filled in). Pass @path or @- to keep secrets out of shell history. + +Keyless (workload identity) credentials take two steps. + +Step 1: run `sim credentials keyless-subject --output json` and configure your cloud to trust the printed issuer, audience, and subject. Its requiredConfigFields lists the values your provider's keylessConfig still needs. + +Step 2: add those values to keylessConfig and pass it to `sim credentials create --credentials @file`. + +The proof expires after 24 hours and works only for the user who requested it, with the same providerId. If creation reports WORKLOAD_IDENTITY_PROOF_EXPIRED, WORKLOAD_IDENTITY_PROOF_INVALID, WORKLOAD_IDENTITY_SUBJECT_REQUIRED, WORKLOAD_IDENTITY_SUBJECT_IN_USE, run step 1 again, trust the new subject, and retry. + +**Examples** + +```bash +sim credentials keyless-subject aws-service-account --output json > subject.json +jq '.keylessConfig + {"roleArn":"arn:aws:iam::123456789012:role/SimWorkflows"}' subject.json > keyless.json +sim credentials create aws-service-account --name "Keyless" --credentials @keyless.json +``` + API request and response schemas: [createServiceAccountCredential](/api-reference/credentials/createServiceAccountCredential). ### Create a short-lived link for connecting an OAuth provider @@ -324,6 +342,44 @@ sim credentials delete [options] API request and response schemas: [deleteCredential](/api-reference/credentials/deleteCredential). +## Other commands + +### Issue the subject and proof for a new keyless (workload identity) credential + +OAuth login or personal API key required. + +```bash +sim credentials keyless-subject +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `providerId` | Yes | Service-account provider for the keyless credential | + + + +Keyless (workload identity) credentials take two steps. + +Step 1: run `sim credentials keyless-subject --output json` and configure your cloud to trust the printed issuer, audience, and subject. Its requiredConfigFields lists the values your provider's keylessConfig still needs. + +Step 2: add those values to keylessConfig and pass it to `sim credentials create --credentials @file`. + +The proof expires after 24 hours and works only for the user who requested it, with the same providerId. If creation reports WORKLOAD_IDENTITY_PROOF_EXPIRED, WORKLOAD_IDENTITY_PROOF_INVALID, WORKLOAD_IDENTITY_SUBJECT_REQUIRED, WORKLOAD_IDENTITY_SUBJECT_IN_USE, run step 1 again, trust the new subject, and retry. + +**Examples** + +```bash +sim credentials keyless-subject aws-service-account --output json > subject.json +jq '.keylessConfig + {"roleArn":"arn:aws:iam::123456789012:role/SimWorkflows"}' subject.json > keyless.json +sim credentials create aws-service-account --name "Keyless" --credentials @keyless.json +``` + +API request and response schemas: [createCredentialWorkloadIdentitySubject](/api-reference/credentials/createCredentialWorkloadIdentitySubject). + ## Related docs - [Credentials](/platform/credentials) diff --git a/apps/docs/content/docs/cli/index.mdx b/apps/docs/content/docs/cli/index.mdx index 0bd341747a7..b701acd5cae 100644 --- a/apps/docs/content/docs/cli/index.mdx +++ b/apps/docs/content/docs/cli/index.mdx @@ -183,4 +183,4 @@ The skills assume `sim` is installed and signed in, so complete [Your first comm - [Scripting](/cli/scripting) - [Workflow imports and workspace sync](/cli/workflow-sync) - [Troubleshooting](/cli/troubleshooting) -- [TypeScript SDK](/api-reference/typescript), [Python SDK](/api-reference/python), and [HTTP API](/api-reference/getting-started) +- [HTTP API](/api-reference/getting-started) diff --git a/apps/docs/content/docs/cli/organizations.mdx b/apps/docs/content/docs/cli/organizations.mdx index d339914d780..99374c76ef6 100644 --- a/apps/docs/content/docs/cli/organizations.mdx +++ b/apps/docs/content/docs/cli/organizations.mdx @@ -229,6 +229,73 @@ sim organizations members remove [options] API request and response schemas: [removeOrganizationMember](/api-reference/organizations/removeOrganizationMember). +## Search source permissions + +### Get search source permission revision + +OAuth login or personal API key required. + +```bash +sim organizations search sources permissions get [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `sourceId` | Yes | Identifier of the configured Search source. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--organization ` | Yes | Organization identifier. | + + + +API request and response schemas: [getSearchSourcePermissions](/api-reference/organizations/getSearchSourcePermissions). + +### Replace search source permissions + +OAuth login or personal API key required. + +This replaces the source’s permission grants. Readers omitted from the new grants lose access. + +```bash +sim organizations search sources permissions replace [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `sourceId` | Yes | Identifier of the configured Search source. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--organization ` | Yes | Organization identifier. | +| `--expected-revision ` | Yes | Permission revision read before exporting the grants. | +| `--permissions ` | Yes | Complete permission grants in the source provider’s format. (JSON, or @path / @- to read a file or stdin). | +| `-y, --yes` | Yes | Confirm this operation. | + + + +API request and response schemas: [replaceSearchSourcePermissions](/api-reference/organizations/replaceSearchSourcePermissions). + ## Invitations ### List organization invitations diff --git a/apps/docs/content/docs/cli/reference.mdx b/apps/docs/content/docs/cli/reference.mdx index ab5fbae41b5..981fdcef182 100644 --- a/apps/docs/content/docs/cli/reference.mdx +++ b/apps/docs/content/docs/cli/reference.mdx @@ -773,8 +773,62 @@ sim credentials create [options] +--credentials takes the provider's fields, or the keylessConfig from `sim credentials keyless-subject` (with the required fields filled in). Pass @path or @- to keep secrets out of shell history. + +Keyless (workload identity) credentials take two steps. + +Step 1: run `sim credentials keyless-subject --output json` and configure your cloud to trust the printed issuer, audience, and subject. Its requiredConfigFields lists the values your provider's keylessConfig still needs. + +Step 2: add those values to keylessConfig and pass it to `sim credentials create --credentials @file`. + +The proof expires after 24 hours and works only for the user who requested it, with the same providerId. If creation reports WORKLOAD_IDENTITY_PROOF_EXPIRED, WORKLOAD_IDENTITY_PROOF_INVALID, WORKLOAD_IDENTITY_SUBJECT_REQUIRED, WORKLOAD_IDENTITY_SUBJECT_IN_USE, run step 1 again, trust the new subject, and retry. + +**Examples** + +```bash +sim credentials keyless-subject aws-service-account --output json > subject.json +jq '.keylessConfig + {"roleArn":"arn:aws:iam::123456789012:role/SimWorkflows"}' subject.json > keyless.json +sim credentials create aws-service-account --name "Keyless" --credentials @keyless.json +``` + API request and response schemas: [createServiceAccountCredential](/api-reference/credentials/createServiceAccountCredential). +### sim credentials keyless-subject + +Issue the subject and proof for a new keyless (workload identity) credential (OAuth login or personal API key required) + +```bash +sim credentials keyless-subject +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `providerId` | Yes | Service-account provider for the keyless credential | + + + +Keyless (workload identity) credentials take two steps. + +Step 1: run `sim credentials keyless-subject --output json` and configure your cloud to trust the printed issuer, audience, and subject. Its requiredConfigFields lists the values your provider's keylessConfig still needs. + +Step 2: add those values to keylessConfig and pass it to `sim credentials create --credentials @file`. + +The proof expires after 24 hours and works only for the user who requested it, with the same providerId. If creation reports WORKLOAD_IDENTITY_PROOF_EXPIRED, WORKLOAD_IDENTITY_PROOF_INVALID, WORKLOAD_IDENTITY_SUBJECT_REQUIRED, WORKLOAD_IDENTITY_SUBJECT_IN_USE, run step 1 again, trust the new subject, and retry. + +**Examples** + +```bash +sim credentials keyless-subject aws-service-account --output json > subject.json +jq '.keylessConfig + {"roleArn":"arn:aws:iam::123456789012:role/SimWorkflows"}' subject.json > keyless.json +sim credentials create aws-service-account --name "Keyless" --credentials @keyless.json +``` + +API request and response schemas: [createCredentialWorkloadIdentitySubject](/api-reference/credentials/createCredentialWorkloadIdentitySubject). + ### sim credentials connect Create a short-lived link for connecting an OAuth provider (OAuth login or personal API key required) @@ -4536,6 +4590,71 @@ sim organizations usage events [options] API request and response schemas: [listOrganizationUsageEvents](/api-reference/organizations/listOrganizationUsageEvents). +### sim organizations search sources permissions get + +Get search source permission revision (OAuth login or personal API key required) + +```bash +sim organizations search sources permissions get [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `sourceId` | Yes | Identifier of the configured Search source. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--organization ` | Yes | Organization identifier. | + + + +API request and response schemas: [getSearchSourcePermissions](/api-reference/organizations/getSearchSourcePermissions). + +### sim organizations search sources permissions replace + +Replace search source permissions (OAuth login or personal API key required) + +This replaces the source’s permission grants. Readers omitted from the new grants lose access. + +```bash +sim organizations search sources permissions replace [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `sourceId` | Yes | Identifier of the configured Search source. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--organization ` | Yes | Organization identifier. | +| `--expected-revision ` | Yes | Permission revision read before exporting the grants. | +| `--permissions ` | Yes | Complete permission grants in the source provider’s format. (JSON, or @path / @- to read a file or stdin). | +| `-y, --yes` | Yes | Confirm this operation. | + + + +API request and response schemas: [replaceSearchSourcePermissions](/api-reference/organizations/replaceSearchSourcePermissions). + ### sim organizations list List organizations (OAuth login or personal API key required) @@ -5103,7 +5222,7 @@ sim selectors get [options] | Option | Required | Description | | --- | --- | --- | -| `--selector-key ` | Yes | Registered selector key from an import or sync preview, such as `gmail.labels`. Accepted values: `airtable.bases`, `airtable.tables`, `asana.workspaces`, `attio.lists`, `attio.objects`, `bigquery.datasets`, `bigquery.tables`, `bitbucket.workspaces`, `bitbucket.repositories`, `calcom.eventTypes`, `calcom.schedules`, `clickup.workspaces`, `clickup.spaces`, `clickup.folders`, `clickup.lists`, `coda.docs`, `coda.pages`, `coda.tables`, `coda.columns`, `coda.rows`, `coda.formulas`, `coda.controls`, `coda.folders`, `coda.permissions`, `confluence.spaces`, `confluence.spacesById`, `confluence.pages`, `google.tasks.lists`, `gauge.organizations`, `gauge.categories`, `gauge.topics`, `gauge.prompts`, `gauge.tags`, `gauge.answers`, `fivetran.groups`, `fivetran.connections`, `gmail.labels`, `google.calendar`, `google.drive`, `google.sheets`, `harmonic.savedSearches`, `jama.projects`, `jama.itemTypes`, `flow.projects`, `flow.categories`, `flow.branches`, `helpscout.mailboxes`, `helpscout.assignees`, `hubspot.lists`, `hubspot.owners`, `hubspot.pipelines`, `hubspot.pipelineStages`, `hubspot.properties`, `jsm.requestTypes`, `jsm.serviceDesks`, `microsoft.planner.plans`, `notion.databases`, `notion.pages`, `netsuite.recordTypes`, `netsuite.asyncTasks`, `pipedrive.pipelines`, `sharepoint.lists`, `trello.boards`, `zoho_desk.organizations`, `zoho_desk.departments`, `zoho_desk.agents`, `zoom.meetings`, `slack.channels`, `snowflake.databases`, `snowflake.schemas`, `snowflake.tables`, `snowflake.warehouses`, `snowflake.roles`, `snowflake.fileFormats`, `snowflake.procedures`, `slack.users`, `outlook.folders`, `outlook.calendars`, `microsoft.teams`, `microsoft.chats`, `microsoft.channels`, `microsoft.planner`, `onedrive.files`, `onedrive.folders`, `sharepoint.sites`, `microsoft.excel`, `microsoft.excel.drives`, `microsoft.excel.sheets`, `microsoft.word`, `wealthbox.contacts`, `jira.issues`, `jira.projects`, `jira.projectKeys`, `linear.projects`, `linear.teams`, `monday.boards`, `monday.groups`, `webflow.sites`, `webflow.collections`, `webflow.items`, `planetscale.databases`, `powerbi.workspaces`, `powerbi.datasets`, `powerbi.reports`, `planetscale.branches`, `planetscale.backups`, `planetscale.deployRequests`, `cloudwatch.logGroups`, `cloudwatch.logStreams`, `imap.mailboxes`, `mcp.tools`, `managedAgent.agents`, `managedAgent.environments`, `managedAgent.vaults`, `managedAgent.memoryStores`, `knowledge.documents`, `sim.workflows`, `table.columns`, `table.outputColumns`, `workspace.secretNames`, `workspace.sandboxes`, `providers.ollamaEmbeddingModels`, `providers.openrouterEmbeddingModels`. | +| `--selector-key ` | Yes | Registered selector key from an import or sync preview, such as `gmail.labels`. Accepted values: `airtable.bases`, `airtable.tables`, `asana.workspaces`, `attio.lists`, `attio.objects`, `bigquery.datasets`, `bigquery.tables`, `bitbucket.workspaces`, `bitbucket.repositories`, `calcom.eventTypes`, `calcom.schedules`, `clickup.workspaces`, `clickup.spaces`, `clickup.folders`, `clickup.lists`, `coda.docs`, `coda.pages`, `coda.tables`, `coda.columns`, `coda.rows`, `coda.formulas`, `coda.controls`, `coda.folders`, `coda.permissions`, `confluence.spaces`, `confluence.spacesById`, `confluence.pages`, `google.tasks.lists`, `gauge.organizations`, `gauge.categories`, `gauge.topics`, `gauge.prompts`, `gauge.tags`, `gauge.answers`, `fivetran.groups`, `fivetran.connections`, `gmail.labels`, `google.calendar`, `google.drive`, `google.sheets`, `harmonic.savedSearches`, `jama.projects`, `jama.itemTypes`, `flow.projects`, `flow.categories`, `flow.branches`, `helpscout.mailboxes`, `helpscout.assignees`, `hubspot.lists`, `hubspot.owners`, `hubspot.pipelines`, `hubspot.pipelineStages`, `hubspot.properties`, `jsm.requestTypes`, `jsm.serviceDesks`, `microsoft.planner.plans`, `notion.databases`, `notion.pages`, `netsuite.recordTypes`, `netsuite.asyncTasks`, `pipedrive.pipelines`, `sharepoint.lists`, `trello.boards`, `zoho_desk.organizations`, `zoho_desk.departments`, `zoho_desk.agents`, `zoom.meetings`, `slack.channels`, `snowflake.databases`, `snowflake.schemas`, `snowflake.tables`, `snowflake.warehouses`, `snowflake.roles`, `snowflake.fileFormats`, `snowflake.procedures`, `slack.users`, `outlook.folders`, `outlook.calendars`, `microsoft.teams`, `microsoft.chats`, `microsoft.channels`, `microsoft.planner`, `onedrive.files`, `onedrive.folders`, `sharepoint.sites`, `microsoft.excel`, `microsoft.excel.drives`, `microsoft.excel.sheets`, `microsoft.word`, `wealthbox.contacts`, `jira.issues`, `jira.projects`, `jira.projectKeys`, `linear.projects`, `linear.teams`, `monday.boards`, `monday.groups`, `webflow.sites`, `webflow.collections`, `webflow.items`, `planetscale.databases`, `powerbi.workspaces`, `powerbi.datasets`, `powerbi.reports`, `planetscale.branches`, `planetscale.backups`, `planetscale.deployRequests`, `cloudwatch.logGroups`, `cloudwatch.logStreams`, `cloudwatch.savedLogGroups`, `cloudwatch.savedLogStreams`, `imap.mailboxes`, `mcp.tools`, `managedAgent.agents`, `managedAgent.environments`, `managedAgent.vaults`, `managedAgent.memoryStores`, `knowledge.documents`, `sim.workflows`, `table.columns`, `table.outputColumns`, `workspace.secretNames`, `workspace.sandboxes`, `providers.ollamaEmbeddingModels`, `providers.openrouterEmbeddingModels`. | | `--context ` | No | Only the dependencies declared by the selector, such as oauthCredential and channelId. Missing OAuth connections require human authorization. Defaults to {}. (JSON, or @path / @- to read a file or stdin). | | `--id ` | Yes | Provider option ID to resolve with the same selector and declared context. | @@ -5125,7 +5244,7 @@ sim selectors list [options] | Option | Required | Description | | --- | --- | --- | -| `--selector-key ` | Yes | Registered selector key from an import or sync preview, such as `gmail.labels`. Accepted values: `airtable.bases`, `airtable.tables`, `asana.workspaces`, `attio.lists`, `attio.objects`, `bigquery.datasets`, `bigquery.tables`, `bitbucket.workspaces`, `bitbucket.repositories`, `calcom.eventTypes`, `calcom.schedules`, `clickup.workspaces`, `clickup.spaces`, `clickup.folders`, `clickup.lists`, `coda.docs`, `coda.pages`, `coda.tables`, `coda.columns`, `coda.rows`, `coda.formulas`, `coda.controls`, `coda.folders`, `coda.permissions`, `confluence.spaces`, `confluence.spacesById`, `confluence.pages`, `google.tasks.lists`, `gauge.organizations`, `gauge.categories`, `gauge.topics`, `gauge.prompts`, `gauge.tags`, `gauge.answers`, `fivetran.groups`, `fivetran.connections`, `gmail.labels`, `google.calendar`, `google.drive`, `google.sheets`, `harmonic.savedSearches`, `jama.projects`, `jama.itemTypes`, `flow.projects`, `flow.categories`, `flow.branches`, `helpscout.mailboxes`, `helpscout.assignees`, `hubspot.lists`, `hubspot.owners`, `hubspot.pipelines`, `hubspot.pipelineStages`, `hubspot.properties`, `jsm.requestTypes`, `jsm.serviceDesks`, `microsoft.planner.plans`, `notion.databases`, `notion.pages`, `netsuite.recordTypes`, `netsuite.asyncTasks`, `pipedrive.pipelines`, `sharepoint.lists`, `trello.boards`, `zoho_desk.organizations`, `zoho_desk.departments`, `zoho_desk.agents`, `zoom.meetings`, `slack.channels`, `snowflake.databases`, `snowflake.schemas`, `snowflake.tables`, `snowflake.warehouses`, `snowflake.roles`, `snowflake.fileFormats`, `snowflake.procedures`, `slack.users`, `outlook.folders`, `outlook.calendars`, `microsoft.teams`, `microsoft.chats`, `microsoft.channels`, `microsoft.planner`, `onedrive.files`, `onedrive.folders`, `sharepoint.sites`, `microsoft.excel`, `microsoft.excel.drives`, `microsoft.excel.sheets`, `microsoft.word`, `wealthbox.contacts`, `jira.issues`, `jira.projects`, `jira.projectKeys`, `linear.projects`, `linear.teams`, `monday.boards`, `monday.groups`, `webflow.sites`, `webflow.collections`, `webflow.items`, `planetscale.databases`, `powerbi.workspaces`, `powerbi.datasets`, `powerbi.reports`, `planetscale.branches`, `planetscale.backups`, `planetscale.deployRequests`, `cloudwatch.logGroups`, `cloudwatch.logStreams`, `imap.mailboxes`, `mcp.tools`, `managedAgent.agents`, `managedAgent.environments`, `managedAgent.vaults`, `managedAgent.memoryStores`, `knowledge.documents`, `sim.workflows`, `table.columns`, `table.outputColumns`, `workspace.secretNames`, `workspace.sandboxes`, `providers.ollamaEmbeddingModels`, `providers.openrouterEmbeddingModels`. | +| `--selector-key ` | Yes | Registered selector key from an import or sync preview, such as `gmail.labels`. Accepted values: `airtable.bases`, `airtable.tables`, `asana.workspaces`, `attio.lists`, `attio.objects`, `bigquery.datasets`, `bigquery.tables`, `bitbucket.workspaces`, `bitbucket.repositories`, `calcom.eventTypes`, `calcom.schedules`, `clickup.workspaces`, `clickup.spaces`, `clickup.folders`, `clickup.lists`, `coda.docs`, `coda.pages`, `coda.tables`, `coda.columns`, `coda.rows`, `coda.formulas`, `coda.controls`, `coda.folders`, `coda.permissions`, `confluence.spaces`, `confluence.spacesById`, `confluence.pages`, `google.tasks.lists`, `gauge.organizations`, `gauge.categories`, `gauge.topics`, `gauge.prompts`, `gauge.tags`, `gauge.answers`, `fivetran.groups`, `fivetran.connections`, `gmail.labels`, `google.calendar`, `google.drive`, `google.sheets`, `harmonic.savedSearches`, `jama.projects`, `jama.itemTypes`, `flow.projects`, `flow.categories`, `flow.branches`, `helpscout.mailboxes`, `helpscout.assignees`, `hubspot.lists`, `hubspot.owners`, `hubspot.pipelines`, `hubspot.pipelineStages`, `hubspot.properties`, `jsm.requestTypes`, `jsm.serviceDesks`, `microsoft.planner.plans`, `notion.databases`, `notion.pages`, `netsuite.recordTypes`, `netsuite.asyncTasks`, `pipedrive.pipelines`, `sharepoint.lists`, `trello.boards`, `zoho_desk.organizations`, `zoho_desk.departments`, `zoho_desk.agents`, `zoom.meetings`, `slack.channels`, `snowflake.databases`, `snowflake.schemas`, `snowflake.tables`, `snowflake.warehouses`, `snowflake.roles`, `snowflake.fileFormats`, `snowflake.procedures`, `slack.users`, `outlook.folders`, `outlook.calendars`, `microsoft.teams`, `microsoft.chats`, `microsoft.channels`, `microsoft.planner`, `onedrive.files`, `onedrive.folders`, `sharepoint.sites`, `microsoft.excel`, `microsoft.excel.drives`, `microsoft.excel.sheets`, `microsoft.word`, `wealthbox.contacts`, `jira.issues`, `jira.projects`, `jira.projectKeys`, `linear.projects`, `linear.teams`, `monday.boards`, `monday.groups`, `webflow.sites`, `webflow.collections`, `webflow.items`, `planetscale.databases`, `powerbi.workspaces`, `powerbi.datasets`, `powerbi.reports`, `planetscale.branches`, `planetscale.backups`, `planetscale.deployRequests`, `cloudwatch.logGroups`, `cloudwatch.logStreams`, `cloudwatch.savedLogGroups`, `cloudwatch.savedLogStreams`, `imap.mailboxes`, `mcp.tools`, `managedAgent.agents`, `managedAgent.environments`, `managedAgent.vaults`, `managedAgent.memoryStores`, `knowledge.documents`, `sim.workflows`, `table.columns`, `table.outputColumns`, `workspace.secretNames`, `workspace.sandboxes`, `providers.ollamaEmbeddingModels`, `providers.openrouterEmbeddingModels`. | | `--context ` | No | Only the dependencies declared by the selector, such as oauthCredential and channelId. Missing OAuth connections require human authorization. Defaults to {}. (JSON, or @path / @- to read a file or stdin). | | `--search ` | No | Provider option search text. | | `--cursor ` | No | Continue from nextCursor returned by a previous result. | @@ -8163,6 +8282,57 @@ sim workflows list [options] API request and response schemas: [listWorkflows](/api-reference/workflows/listWorkflows). +### sim workflows triggers list + +Show the health of every trigger the live deployment serves + +```bash +sim workflows triggers list +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `workflowId` | Yes | Unique workflow identifier. | + + + +API request and response schemas: [listWorkflowTriggers](/api-reference/workflows/listWorkflowTriggers). + +### sim workflows triggers update + +Enable or disable one trigger of the live deployment + +```bash +sim workflows triggers update [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `workflowId` | Yes | Unique workflow identifier. | +| `blockId` | Yes | Trigger block in the deployed workflow. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--enabled ` | Yes | Whether the trigger should fire. A disable lasts until the next deploy or version activation, which re-enables every trigger. Accepted values: `true`, `false`. | + + + +API request and response schemas: [updateWorkflowTrigger](/api-reference/workflows/updateWorkflowTrigger). + ### sim workflows move Move workflows @@ -8784,12 +8954,12 @@ sim workspaces fork-resources [options] API request and response schemas: [listWorkspaceForkResources](/api-reference/workspace-sync/listWorkspaceForkResources). -### sim workspaces members +### sim workspaces members list [#sim-workspaces-members] List workspace members ```bash -sim workspaces members [options] +sim workspaces members list [options] ``` **Options** @@ -8804,6 +8974,68 @@ sim workspaces members [options] API request and response schemas: [listWorkspaceMembers](/api-reference/workspaces/listWorkspaceMembers). +### sim workspaces members remove + +Remove workspace member (OAuth login or personal API key required) + +This removes the member’s workspace access. If it was their last workspace in the organization, their organization membership is removed and their sessions end. + +```bash +sim workspaces members remove [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `userId` | Yes | User identifier of the workspace member, as returned by List workspace members. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `-y, --yes` | Yes | Confirm this operation. | + + + +API request and response schemas: [removeWorkspaceMember](/api-reference/workspaces/removeWorkspaceMember). + +### sim workspaces members update + +Update workspace member (OAuth login or personal API key required) + +```bash +sim workspaces members update [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `userId` | Yes | User identifier of the workspace member, as returned by List workspace members. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--role ` | Yes | New workspace role. Inherited organization Admin access, the workspace owner, and the workspace billing account keep Admin and cannot be changed here. Accepted values: `admin`, `write`, `read`. | + + + +API request and response schemas: [updateWorkspaceMember](/api-reference/workspaces/updateWorkspaceMember). + ### sim workspaces list List workspaces diff --git a/apps/docs/content/docs/cli/selectors.mdx b/apps/docs/content/docs/cli/selectors.mdx index f444991a7ed..368e237b9d9 100644 --- a/apps/docs/content/docs/cli/selectors.mdx +++ b/apps/docs/content/docs/cli/selectors.mdx @@ -25,7 +25,7 @@ sim selectors list [options] | Option | Required | Description | | --- | --- | --- | -| `--selector-key ` | Yes | Registered selector key from an import or sync preview, such as `gmail.labels`. Accepted values: `airtable.bases`, `airtable.tables`, `asana.workspaces`, `attio.lists`, `attio.objects`, `bigquery.datasets`, `bigquery.tables`, `bitbucket.workspaces`, `bitbucket.repositories`, `calcom.eventTypes`, `calcom.schedules`, `clickup.workspaces`, `clickup.spaces`, `clickup.folders`, `clickup.lists`, `coda.docs`, `coda.pages`, `coda.tables`, `coda.columns`, `coda.rows`, `coda.formulas`, `coda.controls`, `coda.folders`, `coda.permissions`, `confluence.spaces`, `confluence.spacesById`, `confluence.pages`, `google.tasks.lists`, `gauge.organizations`, `gauge.categories`, `gauge.topics`, `gauge.prompts`, `gauge.tags`, `gauge.answers`, `fivetran.groups`, `fivetran.connections`, `gmail.labels`, `google.calendar`, `google.drive`, `google.sheets`, `harmonic.savedSearches`, `jama.projects`, `jama.itemTypes`, `flow.projects`, `flow.categories`, `flow.branches`, `helpscout.mailboxes`, `helpscout.assignees`, `hubspot.lists`, `hubspot.owners`, `hubspot.pipelines`, `hubspot.pipelineStages`, `hubspot.properties`, `jsm.requestTypes`, `jsm.serviceDesks`, `microsoft.planner.plans`, `notion.databases`, `notion.pages`, `netsuite.recordTypes`, `netsuite.asyncTasks`, `pipedrive.pipelines`, `sharepoint.lists`, `trello.boards`, `zoho_desk.organizations`, `zoho_desk.departments`, `zoho_desk.agents`, `zoom.meetings`, `slack.channels`, `snowflake.databases`, `snowflake.schemas`, `snowflake.tables`, `snowflake.warehouses`, `snowflake.roles`, `snowflake.fileFormats`, `snowflake.procedures`, `slack.users`, `outlook.folders`, `outlook.calendars`, `microsoft.teams`, `microsoft.chats`, `microsoft.channels`, `microsoft.planner`, `onedrive.files`, `onedrive.folders`, `sharepoint.sites`, `microsoft.excel`, `microsoft.excel.drives`, `microsoft.excel.sheets`, `microsoft.word`, `wealthbox.contacts`, `jira.issues`, `jira.projects`, `jira.projectKeys`, `linear.projects`, `linear.teams`, `monday.boards`, `monday.groups`, `webflow.sites`, `webflow.collections`, `webflow.items`, `planetscale.databases`, `powerbi.workspaces`, `powerbi.datasets`, `powerbi.reports`, `planetscale.branches`, `planetscale.backups`, `planetscale.deployRequests`, `cloudwatch.logGroups`, `cloudwatch.logStreams`, `imap.mailboxes`, `mcp.tools`, `managedAgent.agents`, `managedAgent.environments`, `managedAgent.vaults`, `managedAgent.memoryStores`, `knowledge.documents`, `sim.workflows`, `table.columns`, `table.outputColumns`, `workspace.secretNames`, `workspace.sandboxes`, `providers.ollamaEmbeddingModels`, `providers.openrouterEmbeddingModels`. | +| `--selector-key ` | Yes | Registered selector key from an import or sync preview, such as `gmail.labels`. Accepted values: `airtable.bases`, `airtable.tables`, `asana.workspaces`, `attio.lists`, `attio.objects`, `bigquery.datasets`, `bigquery.tables`, `bitbucket.workspaces`, `bitbucket.repositories`, `calcom.eventTypes`, `calcom.schedules`, `clickup.workspaces`, `clickup.spaces`, `clickup.folders`, `clickup.lists`, `coda.docs`, `coda.pages`, `coda.tables`, `coda.columns`, `coda.rows`, `coda.formulas`, `coda.controls`, `coda.folders`, `coda.permissions`, `confluence.spaces`, `confluence.spacesById`, `confluence.pages`, `google.tasks.lists`, `gauge.organizations`, `gauge.categories`, `gauge.topics`, `gauge.prompts`, `gauge.tags`, `gauge.answers`, `fivetran.groups`, `fivetran.connections`, `gmail.labels`, `google.calendar`, `google.drive`, `google.sheets`, `harmonic.savedSearches`, `jama.projects`, `jama.itemTypes`, `flow.projects`, `flow.categories`, `flow.branches`, `helpscout.mailboxes`, `helpscout.assignees`, `hubspot.lists`, `hubspot.owners`, `hubspot.pipelines`, `hubspot.pipelineStages`, `hubspot.properties`, `jsm.requestTypes`, `jsm.serviceDesks`, `microsoft.planner.plans`, `notion.databases`, `notion.pages`, `netsuite.recordTypes`, `netsuite.asyncTasks`, `pipedrive.pipelines`, `sharepoint.lists`, `trello.boards`, `zoho_desk.organizations`, `zoho_desk.departments`, `zoho_desk.agents`, `zoom.meetings`, `slack.channels`, `snowflake.databases`, `snowflake.schemas`, `snowflake.tables`, `snowflake.warehouses`, `snowflake.roles`, `snowflake.fileFormats`, `snowflake.procedures`, `slack.users`, `outlook.folders`, `outlook.calendars`, `microsoft.teams`, `microsoft.chats`, `microsoft.channels`, `microsoft.planner`, `onedrive.files`, `onedrive.folders`, `sharepoint.sites`, `microsoft.excel`, `microsoft.excel.drives`, `microsoft.excel.sheets`, `microsoft.word`, `wealthbox.contacts`, `jira.issues`, `jira.projects`, `jira.projectKeys`, `linear.projects`, `linear.teams`, `monday.boards`, `monday.groups`, `webflow.sites`, `webflow.collections`, `webflow.items`, `planetscale.databases`, `powerbi.workspaces`, `powerbi.datasets`, `powerbi.reports`, `planetscale.branches`, `planetscale.backups`, `planetscale.deployRequests`, `cloudwatch.logGroups`, `cloudwatch.logStreams`, `cloudwatch.savedLogGroups`, `cloudwatch.savedLogStreams`, `imap.mailboxes`, `mcp.tools`, `managedAgent.agents`, `managedAgent.environments`, `managedAgent.vaults`, `managedAgent.memoryStores`, `knowledge.documents`, `sim.workflows`, `table.columns`, `table.outputColumns`, `workspace.secretNames`, `workspace.sandboxes`, `providers.ollamaEmbeddingModels`, `providers.openrouterEmbeddingModels`. | | `--context ` | No | Only the dependencies declared by the selector, such as oauthCredential and channelId. Missing OAuth connections require human authorization. Defaults to {}. (JSON, or @path / @- to read a file or stdin). | | `--search ` | No | Provider option search text. | | `--cursor ` | No | Continue from nextCursor returned by a previous result. | @@ -49,7 +49,7 @@ sim selectors get [options] | Option | Required | Description | | --- | --- | --- | -| `--selector-key ` | Yes | Registered selector key from an import or sync preview, such as `gmail.labels`. Accepted values: `airtable.bases`, `airtable.tables`, `asana.workspaces`, `attio.lists`, `attio.objects`, `bigquery.datasets`, `bigquery.tables`, `bitbucket.workspaces`, `bitbucket.repositories`, `calcom.eventTypes`, `calcom.schedules`, `clickup.workspaces`, `clickup.spaces`, `clickup.folders`, `clickup.lists`, `coda.docs`, `coda.pages`, `coda.tables`, `coda.columns`, `coda.rows`, `coda.formulas`, `coda.controls`, `coda.folders`, `coda.permissions`, `confluence.spaces`, `confluence.spacesById`, `confluence.pages`, `google.tasks.lists`, `gauge.organizations`, `gauge.categories`, `gauge.topics`, `gauge.prompts`, `gauge.tags`, `gauge.answers`, `fivetran.groups`, `fivetran.connections`, `gmail.labels`, `google.calendar`, `google.drive`, `google.sheets`, `harmonic.savedSearches`, `jama.projects`, `jama.itemTypes`, `flow.projects`, `flow.categories`, `flow.branches`, `helpscout.mailboxes`, `helpscout.assignees`, `hubspot.lists`, `hubspot.owners`, `hubspot.pipelines`, `hubspot.pipelineStages`, `hubspot.properties`, `jsm.requestTypes`, `jsm.serviceDesks`, `microsoft.planner.plans`, `notion.databases`, `notion.pages`, `netsuite.recordTypes`, `netsuite.asyncTasks`, `pipedrive.pipelines`, `sharepoint.lists`, `trello.boards`, `zoho_desk.organizations`, `zoho_desk.departments`, `zoho_desk.agents`, `zoom.meetings`, `slack.channels`, `snowflake.databases`, `snowflake.schemas`, `snowflake.tables`, `snowflake.warehouses`, `snowflake.roles`, `snowflake.fileFormats`, `snowflake.procedures`, `slack.users`, `outlook.folders`, `outlook.calendars`, `microsoft.teams`, `microsoft.chats`, `microsoft.channels`, `microsoft.planner`, `onedrive.files`, `onedrive.folders`, `sharepoint.sites`, `microsoft.excel`, `microsoft.excel.drives`, `microsoft.excel.sheets`, `microsoft.word`, `wealthbox.contacts`, `jira.issues`, `jira.projects`, `jira.projectKeys`, `linear.projects`, `linear.teams`, `monday.boards`, `monday.groups`, `webflow.sites`, `webflow.collections`, `webflow.items`, `planetscale.databases`, `powerbi.workspaces`, `powerbi.datasets`, `powerbi.reports`, `planetscale.branches`, `planetscale.backups`, `planetscale.deployRequests`, `cloudwatch.logGroups`, `cloudwatch.logStreams`, `imap.mailboxes`, `mcp.tools`, `managedAgent.agents`, `managedAgent.environments`, `managedAgent.vaults`, `managedAgent.memoryStores`, `knowledge.documents`, `sim.workflows`, `table.columns`, `table.outputColumns`, `workspace.secretNames`, `workspace.sandboxes`, `providers.ollamaEmbeddingModels`, `providers.openrouterEmbeddingModels`. | +| `--selector-key ` | Yes | Registered selector key from an import or sync preview, such as `gmail.labels`. Accepted values: `airtable.bases`, `airtable.tables`, `asana.workspaces`, `attio.lists`, `attio.objects`, `bigquery.datasets`, `bigquery.tables`, `bitbucket.workspaces`, `bitbucket.repositories`, `calcom.eventTypes`, `calcom.schedules`, `clickup.workspaces`, `clickup.spaces`, `clickup.folders`, `clickup.lists`, `coda.docs`, `coda.pages`, `coda.tables`, `coda.columns`, `coda.rows`, `coda.formulas`, `coda.controls`, `coda.folders`, `coda.permissions`, `confluence.spaces`, `confluence.spacesById`, `confluence.pages`, `google.tasks.lists`, `gauge.organizations`, `gauge.categories`, `gauge.topics`, `gauge.prompts`, `gauge.tags`, `gauge.answers`, `fivetran.groups`, `fivetran.connections`, `gmail.labels`, `google.calendar`, `google.drive`, `google.sheets`, `harmonic.savedSearches`, `jama.projects`, `jama.itemTypes`, `flow.projects`, `flow.categories`, `flow.branches`, `helpscout.mailboxes`, `helpscout.assignees`, `hubspot.lists`, `hubspot.owners`, `hubspot.pipelines`, `hubspot.pipelineStages`, `hubspot.properties`, `jsm.requestTypes`, `jsm.serviceDesks`, `microsoft.planner.plans`, `notion.databases`, `notion.pages`, `netsuite.recordTypes`, `netsuite.asyncTasks`, `pipedrive.pipelines`, `sharepoint.lists`, `trello.boards`, `zoho_desk.organizations`, `zoho_desk.departments`, `zoho_desk.agents`, `zoom.meetings`, `slack.channels`, `snowflake.databases`, `snowflake.schemas`, `snowflake.tables`, `snowflake.warehouses`, `snowflake.roles`, `snowflake.fileFormats`, `snowflake.procedures`, `slack.users`, `outlook.folders`, `outlook.calendars`, `microsoft.teams`, `microsoft.chats`, `microsoft.channels`, `microsoft.planner`, `onedrive.files`, `onedrive.folders`, `sharepoint.sites`, `microsoft.excel`, `microsoft.excel.drives`, `microsoft.excel.sheets`, `microsoft.word`, `wealthbox.contacts`, `jira.issues`, `jira.projects`, `jira.projectKeys`, `linear.projects`, `linear.teams`, `monday.boards`, `monday.groups`, `webflow.sites`, `webflow.collections`, `webflow.items`, `planetscale.databases`, `powerbi.workspaces`, `powerbi.datasets`, `powerbi.reports`, `planetscale.branches`, `planetscale.backups`, `planetscale.deployRequests`, `cloudwatch.logGroups`, `cloudwatch.logStreams`, `cloudwatch.savedLogGroups`, `cloudwatch.savedLogStreams`, `imap.mailboxes`, `mcp.tools`, `managedAgent.agents`, `managedAgent.environments`, `managedAgent.vaults`, `managedAgent.memoryStores`, `knowledge.documents`, `sim.workflows`, `table.columns`, `table.outputColumns`, `workspace.secretNames`, `workspace.sandboxes`, `providers.ollamaEmbeddingModels`, `providers.openrouterEmbeddingModels`. | | `--context ` | No | Only the dependencies declared by the selector, such as oauthCredential and channelId. Missing OAuth connections require human authorization. Defaults to {}. (JSON, or @path / @- to read a file or stdin). | | `--id ` | Yes | Provider option ID to resolve with the same selector and declared context. | diff --git a/apps/docs/content/docs/cli/workflows.mdx b/apps/docs/content/docs/cli/workflows.mdx index 2a3c7e338d6..d0d274cc03c 100644 --- a/apps/docs/content/docs/cli/workflows.mdx +++ b/apps/docs/content/docs/cli/workflows.mdx @@ -431,6 +431,53 @@ sim workflows deployment update [options] API request and response schemas: [updateWorkflowPublicApi](/api-reference/workflows/updateWorkflowPublicApi). +### Show the health of every trigger the live deployment serves + +```bash +sim workflows triggers list +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `workflowId` | Yes | Unique workflow identifier. | + + + +API request and response schemas: [listWorkflowTriggers](/api-reference/workflows/listWorkflowTriggers). + +### Enable or disable one trigger of the live deployment + +```bash +sim workflows triggers update [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `workflowId` | Yes | Unique workflow identifier. | +| `blockId` | Yes | Trigger block in the deployed workflow. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--enabled ` | Yes | Whether the trigger should fire. A disable lasts until the next deploy or version activation, which re-enables every trigger. Accepted values: `true`, `false`. | + + + +API request and response schemas: [updateWorkflowTrigger](/api-reference/workflows/updateWorkflowTrigger). + ### List workflow versions ```bash diff --git a/apps/docs/content/docs/cli/workspaces.mdx b/apps/docs/content/docs/cli/workspaces.mdx index 826b0d42133..cf195f61761 100644 --- a/apps/docs/content/docs/cli/workspaces.mdx +++ b/apps/docs/content/docs/cli/workspaces.mdx @@ -44,7 +44,7 @@ API request and response schemas: [getWorkspace](/api-reference/workspaces/getWo ### List workspace members ```bash -sim workspaces members [options] +sim workspaces members list [options] ``` **Options** @@ -59,6 +59,68 @@ sim workspaces members [options] API request and response schemas: [listWorkspaceMembers](/api-reference/workspaces/listWorkspaceMembers). +### Update workspace member + +OAuth login or personal API key required. + +```bash +sim workspaces members update [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `userId` | Yes | User identifier of the workspace member, as returned by List workspace members. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--role ` | Yes | New workspace role. Inherited organization Admin access, the workspace owner, and the workspace billing account keep Admin and cannot be changed here. Accepted values: `admin`, `write`, `read`. | + + + +API request and response schemas: [updateWorkspaceMember](/api-reference/workspaces/updateWorkspaceMember). + +### Remove workspace member + +OAuth login or personal API key required. + +This removes the member’s workspace access. If it was their last workspace in the organization, their organization membership is removed and their sessions end. + +```bash +sim workspaces members remove [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `userId` | Yes | User identifier of the workspace member, as returned by List workspace members. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `-y, --yes` | Yes | Confirm this operation. | + + + +API request and response schemas: [removeWorkspaceMember](/api-reference/workspaces/removeWorkspaceMember). + ### Create workspace invitations OAuth login or personal API key required. diff --git a/apps/docs/content/docs/integrations/affinity.mdx b/apps/docs/content/docs/integrations/affinity.mdx index c79019c664e..b5046eae52f 100644 --- a/apps/docs/content/docs/integrations/affinity.mdx +++ b/apps/docs/content/docs/integrations/affinity.mdx @@ -2,7 +2,7 @@ title: Affinity description: Read and write companies, people, lists, notes, and relationships in Affinity integration: - type: "affinity" + type: "affinity_v2" color: "#1B1C1E" --- @@ -40,7 +40,6 @@ Write up to 100 non-list field values on one company or person in a single reque | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which entity to write the fields on: companies or persons | | `entityId` | string | Yes | ID of that company or person | | `updates` | json | Yes | Up to 100 field updates as \[\{"id":"<fieldId>","value":\{"type":"…","data":…\}\}\], using the same value shapes as a single field update | @@ -63,7 +62,6 @@ Write up to 100 field values on one list row in a single request. Requires the " | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `listEntryId` | string | Yes | The list entry ID | | `updates` | json | Yes | Up to 100 field updates as \[\{"id":"<fieldId>","value":\{"type":"…","data":…\}\}\], using the same value shapes as a single field update | @@ -86,7 +84,6 @@ Create a list. Its type fixes which entities it can hold, and the API key holder | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `name` | string | Yes | Name of the new list | | `type` | string | Yes | Entity kind the list holds: company, opportunity, or person | | `isPublic` | boolean | No | Whether everyone in the organization can see the list | @@ -115,7 +112,6 @@ Add a selectable option to a dropdown field on a list. A ranked or status option | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `fieldId` | string | Yes | The dropdown field ID on that list | | `type` | string | Yes | Kind of option to create, matching the field. dropdown takes only a label; ranked-dropdown also requires rank and color; status-dropdown additionally requires a status category. Sending a field the kind does not accept is rejected | @@ -149,7 +145,6 @@ Fold a duplicate company or person into the record you are keeping. The merge ru | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | What to merge: companies or persons | | `primaryId` | string | Yes | ID of the record to keep | | `duplicateId` | string | Yes | ID of the duplicate record to fold in | @@ -172,7 +167,6 @@ Write a note — attached to companies, persons, and opportunities, anchored to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `type` | string | Yes | Note shape: entities to attach it to records, interaction to anchor it to a meeting, call, or chat message, or user-reply to reply to a note | | `html` | string | Yes | The note body as HTML | | `companyIds` | json | No | Companies to attach the note to, e.g. \[1, 2\]. Not used on a reply | @@ -220,7 +214,6 @@ Create a reminder on one company, person, or opportunity. A recurring reminder r | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `type` | string | Yes | one-time to fire once, or recurring to reset on a signal | | `entityType` | string | Yes | What the reminder is about: company, person, or opportunity | | `entityId` | string | Yes | ID of that company, person, or opportunity | @@ -262,7 +255,6 @@ Permanently delete a dropdown option on a list field. Every list entry currently | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `fieldId` | string | Yes | The dropdown field ID on that list | | `dropdownOptionId` | string | Yes | The dropdown option ID to delete | @@ -286,7 +278,6 @@ Delete a note you created. Deleting a root note also deletes its replies; deleti | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `noteId` | string | Yes | The note ID to delete | #### Output @@ -308,7 +299,6 @@ Look up one company by ID. Field data is returned only for the Field IDs or Fiel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `companyId` | string | Yes | The company ID | | `fieldIds` | json | No | Field IDs to return values for, e.g. \["affinity-data-location"\]. Mutually exclusive with Field Types | | `fieldTypes` | json | No | Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs | @@ -341,7 +331,6 @@ Verify an Affinity API key and return the tenant, the user behind the key, and t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | #### Output @@ -373,7 +362,6 @@ Read one non-list field value from a company or person. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which entity to read the field from: companies or persons | | `entityId` | string | Yes | ID of that company or person | | `fieldId` | string | Yes | The field ID to read | @@ -400,7 +388,6 @@ Read one list — its name, type, owner, and privacy setting. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | #### Output @@ -427,7 +414,6 @@ Read one row of a list with its entity. Field data is returned only for the Fiel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `listEntryId` | string | Yes | The list entry ID | | `fieldIds` | json | No | Field IDs to return values for, e.g. \["affinity-data-location"\]. Mutually exclusive with Field Types | @@ -456,7 +442,6 @@ Read one field value on a list row. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `listEntryId` | string | Yes | The list entry ID | | `fieldId` | string | Yes | The field ID to read | @@ -483,7 +468,6 @@ Read one dropdown option on a list field. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `fieldId` | string | Yes | The dropdown field ID on that list | | `dropdownOptionId` | string | Yes | The dropdown option ID | @@ -512,7 +496,6 @@ Read the status of one company or person merge, including why it failed if it di | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which merge to read: companies or persons | | `mergeId` | string | Yes | The merge ID | @@ -543,7 +526,6 @@ Read one merge task and how its merges are progressing. Poll this after starting | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which merge task to read: companies or persons | | `taskId` | string | Yes | The merge task ID | @@ -567,7 +549,6 @@ Read one note with its body, author, mentions, and attached records. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `noteId` | string | Yes | The note ID | | `includes` | json | No | Extra properties to return, e.g. \["repliesCount","personsPreview","companiesPreview","opportunitiesPreview"\]. Those four fields are omitted unless requested here | @@ -607,7 +588,6 @@ Read one opportunity and the list it belongs to. Its field data lives on the lis | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `opportunityId` | string | Yes | The opportunity ID | #### Output @@ -633,7 +613,6 @@ Look up one person by ID. Field data is returned only for the Field IDs or Field | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `personId` | string | Yes | The person ID | | `fieldIds` | json | No | Field IDs to return values for, e.g. \["affinity-data-location"\]. Mutually exclusive with Field Types | | `fieldTypes` | json | No | Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs | @@ -667,7 +646,6 @@ Read one saved view — its name, kind, and creation date. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `viewId` | string | Yes | The saved view ID | @@ -692,7 +670,6 @@ Read one transcript with its first 100 fragments. Page the fragments endpoint fo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `transcriptId` | string | Yes | The transcript ID | #### Output @@ -717,7 +694,6 @@ Read one internal user. A user and their person record share the same numeric ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `userId` | string | Yes | The user ID, which is also their person ID | #### Output @@ -745,7 +721,6 @@ Page through logged calls and their participants. Only calls the API key holder | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filter` | string | No | Affinity Filtering Language expression, e.g. "createdAt>=2026-01-01" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -781,7 +756,6 @@ Page through logged chat messages and their participants. Only messages the API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filter` | string | No | Affinity Filtering Language expression, e.g. "createdAt>=2026-01-01" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -820,7 +794,6 @@ Page through companies. Companies come back without field data unless Field IDs | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `ids` | json | No | Restrict the page to these company IDs, e.g. \[1, 2, 3\] | | `fieldIds` | json | No | Field IDs to return values for, e.g. \["affinity-data-location"\]. Mutually exclusive with Field Types | | `fieldTypes` | json | No | Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs | @@ -859,7 +832,6 @@ Find warm paths into a company through shared work history: who in your Affinity | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filter` | string | Yes | Required scope. The only supported filter is target.currentCompany.id, e.g. "target.currentCompany.id=123" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of targets to return per page, 1-50. Defaults to 20 | @@ -889,7 +861,6 @@ Page through email metadata — subject, participants, and timestamps. Affinity | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filter` | string | No | Affinity Filtering Language expression, e.g. "createdAt>=2026-01-01" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -925,7 +896,6 @@ Page through a company's or person's non-list field values. List fields are not | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which entity to read field values from: companies or persons | | `entityId` | string | Yes | ID of that company or person | | `ids` | json | No | Restrict to these field IDs. Mutually exclusive with Field Types | @@ -959,7 +929,6 @@ Page through a company's or person's rows across every list, each carrying that | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which entity to look up the rows of: companies or persons | | `entityId` | string | Yes | ID of that company or person | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -997,7 +966,6 @@ List every list a company or person appears on that the caller can view. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which entity to look up the lists of: companies or persons | | `entityId` | string | Yes | ID of that company or person | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -1030,7 +998,6 @@ List the notes relevant to one company, person, or opportunity — directly atta | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which entity the notes hang off: companies, persons, or opportunities | | `entityId` | string | Yes | ID of that company, person, or opportunity | | `filter` | string | No | Affinity Filtering Language expression, e.g. "createdAt>=2026-01-01" | @@ -1079,7 +1046,6 @@ List who knows a company or person, scored 0.0 to 1.0 by how much the two actual | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which entity to look up relationships for: companies or persons | | `entityId` | string | Yes | ID of that company or person | | `filter` | string | No | Affinity Filtering Language expression. This endpoint filters on interactionScore only, e.g. "interactionScore>=0.5" | @@ -1124,7 +1090,6 @@ List the selectable options on a dropdown or ranked-dropdown company or person f | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which field family the field belongs to: companies or persons | | `fieldId` | string | Yes | The dropdown or ranked-dropdown field ID | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -1158,7 +1123,6 @@ List the non-list company or person fields, with the value type, filter operator | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which fields to describe: companies or persons | | `includes` | json | No | Extra properties to return: \["filterability","sortability"\]. Both are omitted unless requested here | | `filter` | string | No | Affinity Filtering Language expression. This endpoint filters on name only, e.g. "name=~Status" | @@ -1194,7 +1158,6 @@ Page through field value changes across the whole workspace. Built for delta syn | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filter` | string | No | Affinity Filtering Language expression over field.id, listEntry.id, changer.id, changedAt, or actionType. Resume a sync with e.g. "changedAt>2026-06-01T12:00:00Z" | | `orderBy` | json | No | Sort order: \["changedAt"\] for oldest first \(the default\), \["-changedAt"\] for newest first | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -1230,7 +1193,6 @@ Find warm paths into a company through investment history: which investors in yo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filter` | string | Yes | Required scope. The only supported filter is target.currentCompany.id, e.g. "target.currentCompany.id=123" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of targets to return per page, 1-50. Defaults to 20 | @@ -1260,7 +1222,6 @@ Page through the rows of a list. Rows come back without field data unless Field | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `fieldIds` | json | No | Field IDs to return values for, e.g. \["affinity-data-location"\]. Mutually exclusive with Field Types | | `fieldTypes` | json | No | Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs | @@ -1294,7 +1255,6 @@ Page through the history of one list row — who changed which field, when, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `listEntryId` | string | Yes | The list entry ID | | `filter` | string | No | Affinity Filtering Language expression over field.id, changer.id, changedAt, or actionType, e.g. "field.id=field-1234" | @@ -1331,7 +1291,6 @@ Page through every field value on one list row, including the list-specific colu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `listEntryId` | string | Yes | The list entry ID | | `ids` | json | No | Restrict to these field IDs. Mutually exclusive with Field Types | @@ -1365,7 +1324,6 @@ List the selectable options on a dropdown, ranked-dropdown, or status-dropdown f | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `fieldId` | string | Yes | The dropdown field ID on that list | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -1399,7 +1357,6 @@ List the fields available on one list, including its list-specific columns. Use | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `includes` | json | No | Extra properties to return: \["filterability","sortability"\]. Both are omitted unless requested here | | `filter` | string | No | Affinity Filtering Language expression. This endpoint filters on name only, e.g. "name=~Stage" | @@ -1435,7 +1392,6 @@ Page through the lists in the organization that the caller can view. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `term` | string | No | Case-insensitive substring match on the list name | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -1468,7 +1424,6 @@ Page through past and upcoming meetings with their organizer and attendees. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filter` | string | No | Affinity Filtering Language expression, e.g. "createdAt>=2026-01-01" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -1505,7 +1460,6 @@ Page through merge tasks, each summarizing how many of its merges are in progres | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which merge tasks to list: companies or persons | | `filter` | string | No | Affinity Filtering Language expression. This endpoint filters on status only, e.g. "status=in-progress" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -1535,7 +1489,6 @@ Page through the company or person merges the organization has run, with the sta | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which merges to list: companies or persons | | `filter` | string | No | Affinity Filtering Language expression over status or taskId, e.g. "status=failed" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -1572,7 +1525,6 @@ List the companies directly attached to one note. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `noteId` | string | Yes | The note ID | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -1603,7 +1555,6 @@ List the opportunities directly attached to one note. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `noteId` | string | Yes | The note ID | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -1637,7 +1588,6 @@ List the persons directly attached to one note. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `noteId` | string | Yes | The note ID | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -1670,7 +1620,6 @@ Page through the replies on one note, including AI Notetaker replies. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `noteId` | string | Yes | The note ID whose replies to read | | `filter` | string | No | Affinity Filtering Language expression, e.g. "createdAt>=2026-01-01" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -1718,7 +1667,6 @@ Page through every note the caller can see. Replies are excluded. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `includes` | json | No | Extra properties to return, e.g. \["repliesCount","personsPreview","companiesPreview","opportunitiesPreview"\]. Those four fields are omitted unless requested here | | `filter` | string | No | Affinity Filtering Language expression, e.g. "createdAt>=2026-01-01" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -1766,7 +1714,6 @@ Page through opportunities. Field data lives on the list entry, not here — rea | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `ids` | json | No | Restrict the page to these opportunity IDs, e.g. \[1, 2, 3\] | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -1798,7 +1745,6 @@ Page through persons. Persons come back without field data unless Field IDs or F | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `ids` | json | No | Restrict the page to these person IDs, e.g. \[1, 2, 3\] | | `fieldIds` | json | No | Field IDs to return values for, e.g. \["affinity-data-location"\]. Mutually exclusive with Field Types | | `fieldTypes` | json | No | Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs | @@ -1838,7 +1784,6 @@ Page through the reminders the caller can see. Filter by status to surface what | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filter` | string | No | Affinity Filtering Language expression, e.g. "createdAt>=2026-01-01" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -1881,7 +1826,6 @@ Page through the rows of a saved view. The view's own filters and columns decide | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `viewId` | string | Yes | The saved view ID | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -1914,7 +1858,6 @@ List the saved views on a list that the caller can view. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -1944,7 +1887,6 @@ Page through everything said in a meeting, segment by segment with the speaker. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `transcriptId` | string | Yes | The transcript ID | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -1976,7 +1918,6 @@ Page through meeting transcript metadata. Read one transcript to get what was ac | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filter` | string | No | Affinity Filtering Language expression, e.g. "createdAt>=2026-01-01" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | | `limit` | number | No | Number of items to return per page, 1-100. Defaults to 100 | @@ -2004,7 +1945,6 @@ Page through the internal users in the organization. Email addresses and roles a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `term` | string | No | Case-insensitive match across first name, last name, and primary email | | `filter` | string | No | Affinity Filtering Language expression over id or status, e.g. "status=active" | | `cursor` | string | No | Cursor from a previous page, returned as nextCursor or prevCursor | @@ -2039,7 +1979,6 @@ Search companies by filters, sorts, and a free-text term. Requires the "Export A | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filters` | json | No | Filter group as \{operator: "and"\|"or", filters: \[...\]\}, at most 50 leaves. Each leaf is \{valueType, fieldId, operator, value\}, and a leaf may itself be a nested group | | `searchTerm` | string | No | Free-text term matched against the searchable fields. At least 3 characters | | `searchFieldIds` | json | No | Field IDs the search term is matched against. Defaults to the searchable fields | @@ -2083,7 +2022,6 @@ Search files by keyword, ordered by relevance. Narrow to specific files or to on | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `prompt` | string | Yes | What to search for. Between 3 and 500 characters | | `ids` | json | No | Restrict the search to these file IDs. Cannot be combined with Company ID | | `companyId` | string | No | Restrict the search to one company's files. Cannot be combined with file IDs | @@ -2111,7 +2049,6 @@ Search the rows of one list by filters, sorts, and a free-text term. Requires th | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID to search | | `filters` | json | No | Filter group as \{operator: "and"\|"or", filters: \[...\]\}, at most 50 leaves. Each leaf is \{valueType, fieldId, operator, value\}, and a leaf may itself be a nested group | | `searchTerm` | string | No | Free-text term matched against the searchable fields. At least 3 characters | @@ -2151,7 +2088,6 @@ Search notes by keyword, ordered by relevance. Narrow to specific notes or to on | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `prompt` | string | Yes | What to search for. Between 3 and 500 characters | | `ids` | json | No | Restrict the search to these note IDs. Cannot be combined with Company ID | | `companyId` | string | No | Restrict the search to one company's notes. Cannot be combined with note IDs | @@ -2178,7 +2114,6 @@ Search persons by filters, sorts, and a free-text term. Requires the "Export All | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `filters` | json | No | Filter group as \{operator: "and"\|"or", filters: \[...\]\}, at most 50 leaves. Each leaf is \{valueType, fieldId, operator, value\}, and a leaf may itself be a nested group | | `searchTerm` | string | No | Free-text term matched against the searchable fields. At least 3 characters | | `searchFieldIds` | json | No | Field IDs the search term is matched against. Defaults to the searchable fields | @@ -2223,7 +2158,6 @@ Find companies from a description in plain language — industry, technology, st | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `prompt` | string | Yes | What to look for, in plain language, e.g. "climate tech companies in our pipeline". Up to 500 characters | | `listIds` | json | No | Restrict the search to companies on these lists, e.g. \[1, 2\] | | `limit` | number | No | Maximum number of companies to return, 1-100. Defaults to 100 | @@ -2255,7 +2189,6 @@ Write one non-list field value on a company or person. The value type must match | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `entityType` | string | Yes | Which entity to write the field on: companies or persons | | `entityId` | string | Yes | ID of that company or person | | `fieldId` | string | Yes | The field ID to write | @@ -2280,7 +2213,6 @@ Write one field value on a list row. Requires the "Export data from Lists" permi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `listEntryId` | string | Yes | The list entry ID | | `fieldId` | string | Yes | The field ID to write | @@ -2305,7 +2237,6 @@ Change a dropdown option on a list field. Every field is optional — supply onl | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `listId` | string | Yes | The list ID | | `fieldId` | string | Yes | The dropdown field ID on that list | | `dropdownOptionId` | string | Yes | The dropdown option ID to update | @@ -2334,7 +2265,6 @@ Rewrite a note's body or replace which records it is attached to. Each list of I | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Affinity API key, sent as a bearer token | | `noteId` | string | Yes | The note ID to update | | `html` | string | No | Replacement note body as HTML | | `companyIds` | json | No | Replacement set of attached companies, e.g. \[1, 2\]. Send \[\] to detach every company; omit to leave them unchanged | diff --git a/apps/docs/content/docs/integrations/agentmail.mdx b/apps/docs/content/docs/integrations/agentmail.mdx index b979bf7a1e6..9f06a9cb608 100644 --- a/apps/docs/content/docs/integrations/agentmail.mdx +++ b/apps/docs/content/docs/integrations/agentmail.mdx @@ -2,7 +2,7 @@ title: AgentMail description: Manage email inboxes, threads, and messages with AgentMail integration: - type: "agentmail" + type: "agentmail_v2" color: "#000000" --- @@ -15,7 +15,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" ## Use the block [#usage-instructions] -Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels. Requires an API key. +Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels. ## Actions @@ -29,7 +29,6 @@ Create a new email draft in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox to create the draft in | | `to` | string | No | Recipient email addresses \(comma-separated\) | | `subject` | string | No | Draft subject line | @@ -72,7 +71,6 @@ Create a new email inbox with AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `username` | string | No | Username for the inbox email address | | `domain` | string | No | Domain for the inbox email address | | `displayName` | string | No | Display name for the inbox | @@ -99,7 +97,6 @@ Delete an email draft in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the draft | | `draftId` | string | Yes | ID of the draft to delete | @@ -121,7 +118,6 @@ Delete an email inbox in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox to delete | #### Output @@ -142,7 +138,6 @@ Delete an email thread in AgentMail (moves to trash, or permanently deletes if a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the thread | | `threadId` | string | Yes | ID of the thread to delete | | `permanent` | boolean | No | Force permanent deletion instead of moving to trash | @@ -165,7 +160,6 @@ Forward an email message to new recipients in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the message | | `messageId` | string | Yes | ID of the message to forward | | `to` | string | Yes | Recipient email addresses \(comma-separated\) | @@ -194,7 +188,6 @@ Get details of a specific email draft in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox the draft belongs to | | `draftId` | string | Yes | ID of the draft to retrieve | @@ -230,7 +223,6 @@ Get details of a specific email inbox in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox to retrieve | #### Output @@ -255,7 +247,6 @@ Get details of a specific email message in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the message | | `messageId` | string | Yes | ID of the message to retrieve | @@ -288,7 +279,6 @@ Get details of a specific email thread including messages in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the thread | | `threadId` | string | Yes | ID of the thread to retrieve | @@ -330,7 +320,6 @@ List email drafts in an inbox in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox to list drafts from | | `limit` | number | No | Maximum number of drafts to return | | `pageToken` | string | No | Pagination token for next page of results | @@ -366,7 +355,6 @@ List all email inboxes in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `limit` | number | No | Maximum number of inboxes to return | | `pageToken` | string | No | Pagination token for next page of results | @@ -395,7 +383,6 @@ List messages in an inbox in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox to list messages from | | `limit` | number | No | Maximum number of messages to return | | `pageToken` | string | No | Pagination token for next page of results | @@ -427,7 +414,6 @@ List email threads in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox to list threads from | | `limit` | number | No | Maximum number of threads to return | | `pageToken` | string | No | Pagination token for next page of results | @@ -463,7 +449,6 @@ Reply to an existing email message in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox to reply from | | `messageId` | string | Yes | ID of the message to reply to | | `text` | string | No | Plain text reply body | @@ -492,7 +477,6 @@ Send an existing email draft in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the draft | | `draftId` | string | Yes | ID of the draft to send | @@ -515,7 +499,6 @@ Send an email message from an AgentMail inbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox to send from | | `to` | string | Yes | Recipient email address \(comma-separated for multiple\) | | `subject` | string | Yes | Email subject line | @@ -545,7 +528,6 @@ Update an existing email draft in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the draft | | `draftId` | string | Yes | ID of the draft to update | | `to` | string | No | Recipient email addresses \(comma-separated\) | @@ -588,7 +570,6 @@ Update the display name of an email inbox in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox to update | | `displayName` | string | Yes | New display name for the inbox | @@ -614,7 +595,6 @@ Add or remove labels on an email message in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the message | | `messageId` | string | Yes | ID of the message to update | | `addLabels` | string | No | Comma-separated labels to add to the message | @@ -639,7 +619,6 @@ Add or remove labels on an email thread in AgentMail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the thread | | `threadId` | string | Yes | ID of the thread to update | | `addLabels` | string | No | Comma-separated labels to add to the thread | diff --git a/apps/docs/content/docs/integrations/agentphone.mdx b/apps/docs/content/docs/integrations/agentphone.mdx index 66747ea70b9..5b15f00c917 100644 --- a/apps/docs/content/docs/integrations/agentphone.mdx +++ b/apps/docs/content/docs/integrations/agentphone.mdx @@ -2,7 +2,7 @@ title: AgentPhone description: Provision numbers, send SMS and iMessage, and place voice calls with AgentPhone integration: - type: "agentphone" + type: "agentphone_v2" color: "linear-gradient(135deg, #1a1a1a 0%, #0a2a14 100%)" --- @@ -17,7 +17,7 @@ Retrieve call transcripts and conversation history for downstream processing, an ## Use the block [#usage-instructions] -Provision SMS- and voice-enabled numbers, send messages and tapback reactions, place outbound calls, manage conversations and contacts, and inspect usage with an AgentPhone API key. +Provision SMS- and voice-enabled numbers, send messages and tapback reactions, place outbound calls, manage conversations and contacts, and inspect usage. ## Actions @@ -31,7 +31,6 @@ Initiate an outbound voice call from an AgentPhone agent | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `agentId` | string | Yes | Agent that will handle the call | | `toNumber` | string | Yes | Phone number to call in E.164 format \(e.g. +14155551234\) | | `fromNumberId` | string | No | Phone number ID to use as caller ID. Must belong to the agent. If omitted, the agent's first assigned number is used. | @@ -64,7 +63,6 @@ Create a new contact in AgentPhone | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `phoneNumber` | string | Yes | Phone number in E.164 format \(e.g. +14155551234\) | | `name` | string | Yes | Contact's full name | | `email` | string | No | Contact's email address | @@ -94,7 +92,6 @@ Provision a new SMS- and voice-enabled phone number | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `country` | string | No | Two-letter country code \(e.g. US, CA\). Defaults to US. | | `areaCode` | string | No | Preferred area code \(US/CA only, e.g. "415"\). Best-effort — may be ignored if unavailable. | | `agentId` | string | No | Optionally attach the number to an agent immediately | @@ -123,7 +120,6 @@ Delete a contact by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `contactId` | string | Yes | Contact ID | #### Output @@ -145,7 +141,6 @@ Fetch a call and its full transcript | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `callId` | string | Yes | ID of the call to retrieve | #### Output @@ -185,7 +180,6 @@ Get the full ordered transcript for a call | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `callId` | string | Yes | ID of the call to retrieve the transcript for | #### Output @@ -210,7 +204,6 @@ Fetch a single contact by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `contactId` | string | Yes | Contact ID | #### Output @@ -237,7 +230,6 @@ Get a conversation along with its recent messages | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `conversationId` | string | Yes | Conversation ID | | `messageLimit` | number | No | Number of recent messages to include \(default 50, max 100\) | @@ -277,7 +269,6 @@ Get paginated messages for a conversation | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `conversationId` | string | Yes | Conversation ID | | `limit` | number | No | Number of messages to return \(default 50, max 200\) | | `before` | string | No | Return messages received before this ISO 8601 timestamp | @@ -311,7 +302,6 @@ Fetch messages received on a specific phone number | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `numberId` | string | Yes | ID of the phone number | | `limit` | number | No | Number of messages to return \(default 50, max 200\) | | `before` | string | No | Return messages received before this ISO 8601 timestamp | @@ -343,7 +333,6 @@ Retrieve current usage statistics for the AgentPhone account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | #### Output @@ -367,7 +356,6 @@ Get a daily breakdown of usage (messages, calls, webhooks) for the last N days | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `days` | number | No | Number of days to return \(1-365, default 30\) | #### Output @@ -393,7 +381,6 @@ Get monthly usage aggregation (messages, calls, webhooks) for the last N months | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `months` | number | No | Number of months to return \(1-24, default 6\) | #### Output @@ -419,7 +406,6 @@ List voice calls for this AgentPhone account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `limit` | number | No | Number of results to return \(default 20, max 100\) | | `offset` | number | No | Number of results to skip \(min 0\) | | `status` | string | No | Filter by status \(completed, in-progress, failed\) | @@ -461,7 +447,6 @@ List contacts for this AgentPhone account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `search` | string | No | Filter by name or phone number \(case-insensitive contains\) | | `limit` | number | No | Number of results to return \(default 50, max 200\) | | `offset` | number | No | Number of results to skip \(min 0\) | @@ -493,7 +478,6 @@ List conversations (message threads) for this AgentPhone account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `limit` | number | No | Number of results to return \(default 20, max 100\) | | `offset` | number | No | Number of results to skip \(min 0\) | @@ -527,7 +511,6 @@ List all phone numbers provisioned for this AgentPhone account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `limit` | number | No | Number of results to return \(default 20, max 100\) | | `offset` | number | No | Number of results to skip \(min 0\) | @@ -558,7 +541,6 @@ Send an iMessage tapback reaction to a message (iMessage only) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `messageId` | string | Yes | ID of the message to react to | | `reaction` | string | Yes | Reaction type: love, like, dislike, laugh, emphasize, or question | @@ -583,7 +565,6 @@ Release (delete) a phone number. This action is irreversible. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `numberId` | string | Yes | ID of the phone number to release | #### Output @@ -605,7 +586,6 @@ Send an outbound SMS or iMessage from an AgentPhone agent | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `agentId` | string | Yes | Agent sending the message | | `toNumber` | string | Yes | Recipient phone number in E.164 format \(e.g. +14155551234\) | | `body` | string | Yes | Message text to send | @@ -634,7 +614,6 @@ Update a contact's fields | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `contactId` | string | Yes | Contact ID | | `phoneNumber` | string | No | New phone number in E.164 format | | `name` | string | No | New contact name | @@ -665,7 +644,6 @@ Update conversation metadata (stored state). Pass null to clear existing metadat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | AgentPhone API key | | `conversationId` | string | Yes | Conversation ID | | `metadata` | json | No | Custom key-value metadata to store on the conversation. Pass null to clear existing metadata. | diff --git a/apps/docs/content/docs/integrations/ahrefs.mdx b/apps/docs/content/docs/integrations/ahrefs.mdx index 3e4abf864e3..b023699a7b2 100644 --- a/apps/docs/content/docs/integrations/ahrefs.mdx +++ b/apps/docs/content/docs/integrations/ahrefs.mdx @@ -2,7 +2,7 @@ title: Ahrefs description: SEO analysis with Ahrefs integration: - type: "ahrefs" + type: "ahrefs_v2" color: "#FFFFFF" --- @@ -15,7 +15,7 @@ Use [Ahrefs](https://ahrefs.com/) in Sim to retrieve domain and backlink metrics ## Use the block [#usage-instructions] -Analyze domain ratings, backlinks, organic keywords, top pages, and more. Requires an Ahrefs API key with access to the selected endpoints; API v3 is available on Lite and higher plans. +Analyze domain ratings, backlinks, organic keywords, top pages, and more. API v3 is available on Lite and higher plans. ## Actions @@ -31,7 +31,6 @@ Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating sh | --------- | ---- | -------- | ----------- | | `target` | string | Yes | The target domain to analyze \(e.g., example.com\) | | `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -56,7 +55,6 @@ Get a one-call organic and paid search overview for a target domain or URL: orga | `country` | string | No | Country code for traffic data. Example: "us", "gb", "de" | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\). Example: "domain" | | `date` | string | No | Date to report metrics on, in YYYY-MM-DD format \(defaults to today\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -88,7 +86,6 @@ Get a list of backlinks pointing to a target domain or URL. Returns details abou | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\). Example: "domain" | | `history` | string | No | Historical scope: "live" \(currently live backlinks\), "all_time" \(default, includes lost backlinks\), or "since:YYYY-MM-DD" \(backlinks found since a date\). | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -118,7 +115,6 @@ Get backlink and referring domain totals for a target domain or URL, both curren | `target` | string | Yes | The target domain or URL to analyze. Example: "example.com" or "https://example.com/page" | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\). Example: "domain" | | `date` | string | No | Date to report metrics on, in YYYY-MM-DD format \(defaults to today\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -146,7 +142,6 @@ Get a list of domains that link to a target domain or URL. Returns unique referr | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\). Example: "domain" | | `history` | string | No | Historical scope: "live" \(currently live\), "all_time" \(default, includes lost domains\), or "since:YYYY-MM-DD" \(domains found since a date\). | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -175,7 +170,6 @@ Get a list of broken backlinks pointing to a target domain or URL. Useful for id | `target` | string | Yes | The target domain or URL to analyze. Example: "example.com" or "https://example.com/page" | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\). Example: "domain" | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -205,7 +199,6 @@ Get organic keywords that a target domain or URL ranks for in Google search resu | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\). Example: "domain" | | `date` | string | No | Date to report metrics on, in YYYY-MM-DD format \(defaults to today\) | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -236,7 +229,6 @@ Get domains that compete with a target domain or URL for the same organic keywor | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\). Example: "domain" | | `date` | string | No | Date to report metrics on, in YYYY-MM-DD format \(defaults to today\) | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -267,7 +259,6 @@ Get the top pages of a target domain sorted by organic traffic. Returns page URL | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\). Example: "domain" | | `date` | string | No | Date to report metrics on, in YYYY-MM-DD format \(defaults to today\) | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -294,7 +285,6 @@ Get detailed metrics for a keyword including search volume, keyword difficulty, | --------- | ---- | -------- | ----------- | | `keyword` | string | Yes | The keyword to analyze | | `country` | string | No | Country code for keyword data. Example: "us", "gb", "de" \(default: "us"\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -334,7 +324,6 @@ Get a target domain's pages that receive paid search traffic, sorted by estimate | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\) | | `date` | string | No | Date to report metrics on, in YYYY-MM-DD format \(defaults to today\) | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -364,7 +353,6 @@ Get the anchor text distribution for a target domain or URL's backlinks, showing | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\) | | `history` | string | No | Historical scope: "live" \(currently live\), "all_time" \(default, includes lost backlinks\), or "since:YYYY-MM-DD" \(backlinks found since a date\) | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -395,7 +383,6 @@ Get keyword ideas related to a seed keyword: terms the same top-ranking pages al | `terms` | string | No | Type of related keywords to return: "also_rank_for", "also_talk_about", or "all" \(default: "all"\) | | `viewFor` | string | No | Whether to derive related terms from the top 10 or top 100 ranking pages \(default: "top_10"\) | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -427,7 +414,6 @@ Get the historical Domain Rating (DR) trend for a target domain or URL over a da | `dateFrom` | string | Yes | Start date of the historical period, in YYYY-MM-DD format | | `dateTo` | string | No | End date of the historical period, in YYYY-MM-DD format \(defaults to today\) | | `historyGrouping` | string | No | Time interval for grouping data points: "daily", "weekly", or "monthly" \(default: "monthly"\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -456,7 +442,6 @@ Get the historical organic and paid traffic trend for a target domain or URL ove | `historyGrouping` | string | No | Time interval for grouping data points: "daily", "weekly", or "monthly" \(default: "monthly"\) | | `country` | string | No | Country code for traffic data. Example: "us", "gb", "de" \(default: "us"\) | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -486,7 +471,6 @@ Get the historical referring domains trend for a target domain or URL over a dat | `dateTo` | string | No | End date of the historical period, in YYYY-MM-DD format \(defaults to today\) | | `historyGrouping` | string | No | Time interval for grouping data points: "daily", "weekly", or "monthly" \(default: "monthly"\) | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -514,7 +498,6 @@ Get the historical organic keyword ranking distribution for a target domain or U | `historyGrouping` | string | No | Time interval for grouping data points: "daily", "weekly", or "monthly" \(default: "monthly"\) | | `country` | string | No | Country code for search results. Example: "us", "gb", "de" \(default: "us"\) | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains, default\), exact \(exact URL match\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -545,7 +528,6 @@ Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traff | `protocol` | string | No | Protocol applied to every target: "both" \(default\), "http", or "https" | | `country` | string | No | Country code for traffic data. Example: "us", "gb", "de" \(default: "us"\) | | `volumeMode` | string | No | Search volume calculation: "monthly" or "average" \(default: "monthly"\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -580,7 +562,6 @@ Get crawled pages from an Ahrefs Site Audit project with health and SEO metrics: | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | | `offset` | number | No | Number of results to skip, for pagination | | `issueId` | string | No | Only return pages affected by this issue ID | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -614,7 +595,6 @@ Get ranking overview metrics for the keywords tracked in an Ahrefs Rank Tracker | `dateCompared` | string | No | Comparison date in YYYY-MM-DD format, to compute position/traffic deltas | | `volumeMode` | string | No | Search volume calculation: "monthly" or "average" \(default: "monthly"\) | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -650,7 +630,6 @@ Get the full SERP (search engine results page) for a keyword tracked in an Ahref | `date` | string | No | Timestamp to return the last available SERP Overview at, in YYYY-MM-DDThh:mm:ss format | | `locationId` | number | No | Location ID of the tracked keyword, if tracked at a specific location | | `languageCode` | string | No | Language code of the tracked keyword | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -689,7 +668,6 @@ Get competitor rankings for the keywords tracked in an Ahrefs Rank Tracker proje | `dateCompared` | string | No | Comparison date in YYYY-MM-DD format, to compute position/traffic deltas | | `volumeMode` | string | No | Search volume calculation: "monthly" or "average" \(default: "monthly"\) | | `limit` | number | No | Maximum number of results to return. Example: 50 \(default: 1000\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -723,7 +701,6 @@ Get aggregate competitor stats for an Ahrefs Rank Tracker project: each competit | `date` | string | Yes | Date to report metrics for, in YYYY-MM-DD format | | `device` | string | Yes | Rankings device type: "desktop" or "mobile" | | `volumeMode` | string | No | Search volume calculation: "monthly" or "average" \(default: "monthly"\) | -| `apiKey` | string | Yes | Ahrefs API Key | #### Output diff --git a/apps/docs/content/docs/integrations/algolia.mdx b/apps/docs/content/docs/integrations/algolia.mdx index ea40da2799e..a819e9226a9 100644 --- a/apps/docs/content/docs/integrations/algolia.mdx +++ b/apps/docs/content/docs/integrations/algolia.mdx @@ -2,7 +2,7 @@ title: Algolia description: Search and manage Algolia indices integration: - type: "algolia" + type: "algolia_v2" color: "#003DFF" --- @@ -29,8 +29,6 @@ Search an Algolia index | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia API Key | | `indexName` | string | Yes | Name of the Algolia index to search | | `query` | string | Yes | Search query text | | `hitsPerPage` | number | No | Number of hits per page \(default: 20\) | @@ -82,8 +80,6 @@ Add or replace a record in an Algolia index | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia Admin API Key | | `indexName` | string | Yes | Name of the Algolia index | | `objectID` | string | No | Object ID for the record \(auto-generated if not provided\) | | `record` | json | Yes | JSON object representing the record to add | @@ -109,8 +105,6 @@ Get a record by objectID from an Algolia index | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia API Key | | `indexName` | string | Yes | Name of the Algolia index | | `objectID` | string | Yes | The objectID of the record to retrieve | | `attributesToRetrieve` | string | No | Comma-separated list of attributes to retrieve | @@ -134,8 +128,6 @@ Retrieve multiple records by objectID from one or more Algolia indices | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia API Key | | `indexName` | string | Yes | Default index name for all requests | | `requests` | json | Yes | Array of objects specifying records to retrieve. Each must have "objectID" and optionally "indexName" and "attributesToRetrieve". | @@ -158,8 +150,6 @@ Partially update a record in an Algolia index without replacing it entirely | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia Admin API Key | | `indexName` | string | Yes | Name of the Algolia index | | `objectID` | string | Yes | The objectID of the record to update | | `attributes` | json | Yes | JSON object with attributes to update. Supports built-in operations like \{"stock": \{"_operation": "Decrement", "value": 1\}\} | @@ -185,8 +175,6 @@ Delete a record by objectID from an Algolia index | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia Admin API Key | | `indexName` | string | Yes | Name of the Algolia index | | `objectID` | string | Yes | The objectID of the record to delete | @@ -209,8 +197,6 @@ Browse and iterate over all records in an Algolia index using cursor pagination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia API Key \(must have browse ACL\) | | `indexName` | string | Yes | Name of the Algolia index to browse | | `query` | string | No | Search query to filter browsed records | | `filters` | string | No | Filter string to narrow down results | @@ -247,8 +233,6 @@ Perform batch add, update, partial update, or delete operations on records in an | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia Admin API Key | | `indexName` | string | Yes | Name of the Algolia index | | `requests` | json | Yes | Array of batch operations. Each item has "action" \(addObject, updateObject, partialUpdateObject, partialUpdateObjectNoCreate, deleteObject, delete, clear\) and "body" \(the record data; must include objectID for update/delete; use an empty object \{\} for the index-level delete/clear actions\) | @@ -271,8 +255,6 @@ List all indices in an Algolia application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia API Key | | `page` | number | No | Page number for paginating indices \(default: not paginated\) | | `hitsPerPage` | number | No | Number of indices per page \(default: 100\) | @@ -307,8 +289,6 @@ Retrieve the settings of an Algolia index | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia API Key | | `indexName` | string | Yes | Name of the Algolia index | #### Output @@ -338,8 +318,6 @@ Update the settings of an Algolia index | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia Admin API Key \(must have editSettings ACL\) | | `indexName` | string | Yes | Name of the Algolia index | | `settings` | json | Yes | JSON object with settings to update \(e.g., \{"searchableAttributes": \["name", "description"\], "customRanking": \["desc\(popularity\)"\]\}\) | | `forwardToReplicas` | boolean | No | Whether to apply changes to replica indices \(default: false\) | @@ -363,8 +341,6 @@ Delete an entire Algolia index and all its records | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia Admin API Key \(must have deleteIndex ACL\) | | `indexName` | string | Yes | Name of the Algolia index to delete | #### Output @@ -386,8 +362,6 @@ Copy or move an Algolia index to a new destination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia Admin API Key | | `indexName` | string | Yes | Name of the source index | | `operation` | string | Yes | Operation to perform: "copy" or "move" | | `destination` | string | Yes | Name of the destination index | @@ -412,8 +386,6 @@ Clear all records from an Algolia index while keeping settings, synonyms, and ru | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia Admin API Key \(must have deleteIndex ACL\) | | `indexName` | string | Yes | Name of the Algolia index to clear | #### Output @@ -435,8 +407,6 @@ Delete all records matching a filter from an Algolia index | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia Admin API Key \(must have deleteIndex ACL\) | | `indexName` | string | Yes | Name of the Algolia index | | `filters` | string | No | Filter expression to match records for deletion \(e.g., "category:outdated"\) | | `facetFilters` | json | No | Array of facet filters \(e.g., \["brand:Acme"\]\) | @@ -466,8 +436,6 @@ Check whether an Algolia indexing task has finished publishing | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `applicationId` | string | Yes | Algolia Application ID | -| `apiKey` | string | Yes | Algolia API Key | | `indexName` | string | Yes | Name of the Algolia index the task ran against | | `taskID` | number | Yes | The taskID returned by a previous write operation | diff --git a/apps/docs/content/docs/integrations/apify.mdx b/apps/docs/content/docs/integrations/apify.mdx index c0e6a7929d3..816e02ad40c 100644 --- a/apps/docs/content/docs/integrations/apify.mdx +++ b/apps/docs/content/docs/integrations/apify.mdx @@ -2,7 +2,7 @@ title: Apify description: Run Apify actors and retrieve results integration: - type: "apify" + type: "apify_v2" color: "#FFFFFF" --- @@ -29,7 +29,6 @@ Run an APIFY actor synchronously and get results (max 5 minutes) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | APIFY API token from console.apify.com/account#/integrations | | `actorId` | string | Yes | Actor ID or username/actor-name. Examples: "apify/web-scraper", "janedoe/my-actor", "moJRLRc85AitArpNN" | | `input` | string | No | Actor input as JSON string. Example: \{ "startUrls": \[ \{ "url": "https://example.com" \} \], "maxPages": 10 \} | | `memory` | number | No | Memory in megabytes allocated for the actor run \(128-32768\). Example: 1024 for 1GB, 2048 for 2GB | @@ -57,7 +56,6 @@ Start an APIFY Actor run and poll it within the same workflow step until complet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | APIFY API token from console.apify.com/account#/integrations | | `actorId` | string | Yes | Actor ID or username/actor-name. Examples: "apify/web-scraper", "janedoe/my-actor", "moJRLRc85AitArpNN" | | `input` | string | No | Actor input as JSON string. Example: \{ "startUrls": \[ \{ "url": "https://example.com" \} \], "maxPages": 10 \} | | `waitForFinish` | number | No | Initial wait time in seconds \(0-60\) before polling starts. Example: 30 | @@ -88,7 +86,6 @@ Run a saved APIFY actor task synchronously and get dataset items (max 5 minutes) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | APIFY API token from console.apify.com/account#/integrations | | `taskId` | string | Yes | Task ID or username/task-name. Examples: "janedoe/my-task", "moJRLRc85AitArpNN" | | `input` | string | No | JSON string that overrides the task's saved input. Example: \{ "startUrls": \[ \{ "url": "https://example.com" \} \] \} | | `itemLimit` | number | No | Max dataset items to return \(1-250000\). Example: 500 | @@ -116,7 +113,6 @@ Retrieve items stored in an APIFY dataset | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | APIFY API token from console.apify.com/account#/integrations | | `datasetId` | string | Yes | Dataset ID to read items from. Example: "9RnD3Pql2vGZkc5H5" | | `itemLimit` | number | No | Max items to return \(1-250000\). Default: all items. Example: 500 | | `offset` | number | No | Number of items to skip at the start. Default: 0 | @@ -143,7 +139,6 @@ Get the status and details of an APIFY actor run | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | APIFY API token from console.apify.com/account#/integrations | | `runId` | string | Yes | Actor run ID to fetch. Example: "HG7ML7M8z78YcAPEB" | #### Output diff --git a/apps/docs/content/docs/integrations/apollo.mdx b/apps/docs/content/docs/integrations/apollo.mdx index 92d7307f0d7..d932a6c37bf 100644 --- a/apps/docs/content/docs/integrations/apollo.mdx +++ b/apps/docs/content/docs/integrations/apollo.mdx @@ -2,7 +2,7 @@ title: Apollo description: Search, enrich, and manage contacts with Apollo.io integration: - type: "apollo" + type: "apollo_v2" color: "#EBF212" --- @@ -29,7 +29,6 @@ Search Apollo's database for people using demographic filters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `person_titles` | array | No | Job titles to search for \(e.g., \["CEO", "VP of Sales"\]\) | | `include_similar_titles` | boolean | No | Whether to return people with job titles similar to person_titles | | `person_locations` | array | No | Locations to search in \(e.g., \["San Francisco, CA", "New York, NY"\]\) | @@ -65,7 +64,6 @@ Enrich data for a single person using Apollo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `first_name` | string | No | First name of the person | | `last_name` | string | No | Last name of the person | | `name` | string | No | Full name of the person \(alternative to first_name/last_name\) | @@ -98,7 +96,6 @@ Enrich data for up to 10 people at once using Apollo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `people` | array | Yes | Array of people to enrich \(max 10\) | | `reveal_personal_emails` | boolean | No | Reveal personal email addresses \(uses credits\) | | `reveal_phone_number` | boolean | No | Reveal phone numbers \(uses credits, requires webhook_url\) | @@ -126,7 +123,6 @@ Search Apollo's database for companies using filters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `organization_locations` | array | No | Company HQ locations \(cities, US states, or countries\) | | `organization_not_locations` | array | No | Exclude companies whose HQ is in these locations | | `organization_num_employees_ranges` | array | No | Employee count ranges as "min,max" strings \(e.g., \["1,10", "250,500", "10000,20000"\]\) | @@ -158,7 +154,6 @@ Enrich data for a single organization using Apollo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `domain` | string | Yes | Company domain \(e.g., "apollo.io", "acme.com"\) | #### Output @@ -180,7 +175,6 @@ Enrich data for up to 10 organizations at once using Apollo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `domains` | array | Yes | Array of company domains to enrich \(max 10, no www. or @, e.g., \["apollo.io", "stripe.com"\]\) | #### Output @@ -205,7 +199,6 @@ Create a new contact in your Apollo database | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `first_name` | string | Yes | First name of the contact | | `last_name` | string | Yes | Last name of the contact | | `email` | string | No | Email address of the contact | @@ -244,7 +237,6 @@ Update an existing contact in your Apollo database | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `contact_id` | string | Yes | ID of the contact to update \(e.g., "con_abc123"\) | | `first_name` | string | No | First name of the contact | | `last_name` | string | No | Last name of the contact | @@ -283,7 +275,6 @@ Search your team's contacts in Apollo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `q_keywords` | string | No | Keywords to search for | | `contact_stage_ids` | array | No | Filter by contact stage IDs | | `contact_label_ids` | array | No | Filter by Apollo label IDs \(lists\) | @@ -311,7 +302,6 @@ Create up to 100 contacts at once in your Apollo database. Supports deduplicatio | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `contacts` | array | Yes | Array of contacts to create \(max 100\). Each contact may include first_name, last_name, email, title, organization_name, account_id, owner_id, contact_stage_id, linkedin_url, phone \(single string\) or phone_numbers \(array of \{raw_number, position\}\), contact_emails, typed_custom_fields, and CRM IDs \(salesforce_contact_id, hubspot_id, team_id\) for cross-system matching | | `append_label_names` | array | No | Label names to add to all contacts in this request \(e.g., \["Hot Lead"\]\) | | `run_dedupe` | boolean | No | Enable deduplication to prevent creating duplicate contacts. When true, existing contacts are returned without modification | @@ -338,7 +328,6 @@ Update up to 100 existing contacts at once in your Apollo database. Each contact | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `contact_ids` | array | No | Array of contact IDs to update. Must be paired with an object-form contact_attributes specifying the fields to apply uniformly to all listed contacts. | | `contact_attributes` | json | No | Required. Either an array of per-contact updates \(each with id\) — used standalone — or a single object of attributes to apply to all contact_ids. Supported fields: owner_id, email, organization_name, title, first_name, last_name, account_id, present_raw_address, linkedin_url, typed_custom_fields | | `async` | boolean | No | Force asynchronous processing. Automatically enabled for >100 contacts | @@ -364,7 +353,6 @@ Create a new account (company) in your Apollo database | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `name` | string | Yes | Company name \(e.g., "Acme Corporation"\) | | `domain` | string | No | Company domain without www. prefix \(e.g., "acme.com"\) | | `phone` | string | No | Primary phone number for the account | @@ -392,7 +380,6 @@ Update an existing account in your Apollo database | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `account_id` | string | Yes | ID of the account to update \(e.g., "acc_abc123"\) | | `name` | string | No | Company name \(e.g., "Acme Corporation"\) | | `domain` | string | No | Company domain \(e.g., "acme.com"\) | @@ -421,7 +408,6 @@ Search your team's accounts in Apollo. Display limit: 50,000 records (100 record | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `q_organization_name` | string | No | Filter accounts by organization name \(partial-match search\) | | `account_stage_ids` | array | No | Filter by account stage IDs | | `account_label_ids` | array | No | Filter by account label IDs | @@ -449,7 +435,6 @@ Create up to 100 accounts at once in your Apollo database. Set run_dedupe=true t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `accounts` | array | Yes | Array of accounts to create \(max 100\). Each account should include a name, and may optionally include domain, phone, phone_status_cd, raw_address, owner_id, linkedin_url, facebook_url, twitter_url, salesforce_id, and hubspot_id. | | `append_label_names` | array | No | Array of label names to add to ALL accounts in this request | | `run_dedupe` | boolean | No | When true, performs aggressive deduplication by domain, organization_id, and name \(defaults to false\) | @@ -478,7 +463,6 @@ Update up to 1000 existing accounts at once in your Apollo database (higher limi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `account_ids` | array | No | Array of account IDs to update with the same values \(max 1000\). Use with name/owner_id for uniform updates. Use either this OR account_attributes. | | `name` | string | No | When using account_ids, apply this name to all accounts | | `owner_id` | string | No | When using account_ids, apply this owner to all accounts | @@ -508,7 +492,6 @@ Create a new deal for an account in your Apollo database (master key required) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `name` | string | Yes | Name of the opportunity/deal \(e.g., "Enterprise License - Q1"\) | | `account_id` | string | No | ID of the account this opportunity belongs to \(e.g., "acc_abc123"\) | | `amount` | string | No | Monetary value as a plain number string with no commas or currency symbols | @@ -536,7 +519,6 @@ Search and list all deals/opportunities in your team's Apollo account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `sort_by_field` | string | No | Sort field: "amount", "is_closed", or "is_won" | | `page` | number | No | Page number for pagination \(e.g., 1, 2, 3\) | | `per_page` | number | No | Results per page, max 100 \(e.g., 25, 50, 100\) | @@ -562,7 +544,6 @@ Retrieve complete details of a specific deal/opportunity by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `opportunity_id` | string | Yes | ID of the opportunity to retrieve \(e.g., "opp_abc123"\) | #### Output @@ -584,7 +565,6 @@ Update an existing deal/opportunity in your Apollo database | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key | | `opportunity_id` | string | Yes | ID of the opportunity to update \(e.g., "opp_abc123"\) | | `name` | string | No | Name of the opportunity/deal \(e.g., "Enterprise License - Q1"\) | | `amount` | string | No | Monetary value as a plain number string with no commas or currency symbols | @@ -612,7 +592,6 @@ Search for sequences/campaigns in your team's Apollo account (master key require | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `q_name` | string | No | Search sequences by name \(e.g., "Outbound Q1", "Follow-up"\) | | `page` | number | No | Page number for pagination \(e.g., 1, 2, 3\) | | `per_page` | number | No | Results per page, max 100 \(e.g., 25, 50, 100\) | @@ -638,7 +617,6 @@ Add contacts to an Apollo sequence | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `sequence_id` | string | Yes | ID of the sequence to add contacts to \(e.g., "seq_abc123"\) | | `contact_ids` | array | No | Array of contact IDs to add to the sequence \(e.g., \["con_abc123", "con_def456"\]\). Either contact_ids or label_names must be provided. | | `label_names` | array | No | Array of label names to identify contacts to add to the sequence. Either contact_ids or label_names must be provided. | @@ -681,7 +659,6 @@ Create one or more tasks in Apollo (one task per contact_id, master key required | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `user_id` | string | Yes | ID of the Apollo user the task is assigned to | | `contact_ids` | array | Yes | Array of contact IDs. One task is created per contact. | | `priority` | string | No | Task priority: "high", "medium", or "low" \(defaults to "medium"\) | @@ -709,7 +686,6 @@ Search for tasks in Apollo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | | `sort_by_field` | string | No | Sort field: "task_due_at" or "task_priority" | | `open_factor_names` | array | No | Filter by status. Common values: \["task_types"\] for open tasks, \["task_completed_at"\] for completed tasks. | | `page` | number | No | Page number for pagination \(e.g., 1, 2, 3\) | @@ -734,7 +710,6 @@ Get list of team's linked email accounts in Apollo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Apollo API key \(master key required\) | #### Output diff --git a/apps/docs/content/docs/integrations/appconfig.mdx b/apps/docs/content/docs/integrations/appconfig.mdx index d139e93f943..a74de4b4201 100644 --- a/apps/docs/content/docs/integrations/appconfig.mdx +++ b/apps/docs/content/docs/integrations/appconfig.mdx @@ -2,7 +2,7 @@ title: AWS AppConfig description: Manage and retrieve configuration with AWS AppConfig integration: - type: "appconfig" + type: "appconfig_v2" color: "linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)" --- @@ -12,12 +12,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} Use [AWS AppConfig](https://docs.aws.amazon.com/appconfig/) to manage applications, environments, configuration profiles, and deployments. Read deployed configuration at runtime or create hosted configuration versions for a rollout. -Authentication uses an AWS access key ID and secret access key. The associated IAM principal needs the relevant `appconfig:*` permissions (for example `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession` for retrieval, and `appconfig:StartDeployment` for rollouts). +It signs in with a saved [AWS credential](/integrations/aws-credentials). The associated IAM principal needs the relevant `appconfig:*` permissions (for example `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession` for retrieval, and `appconfig:StartDeployment` for rollouts). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Manage applications, environments, and configuration profiles, create and read hosted configuration versions, run and inspect deployments, and retrieve the latest deployed configuration at runtime. Requires AWS access key and secret access key. +Manage applications, environments, and configuration profiles, create and read hosted configuration versions, run and inspect deployments, and retrieve the latest deployed configuration at runtime. ## Actions @@ -32,8 +32,6 @@ Retrieve the latest deployed configuration for an AppConfig application, environ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID or name to retrieve configuration for | | `environmentId` | string | Yes | The environment ID or name to retrieve configuration for | | `configurationProfileId` | string | Yes | The configuration profile ID or name to retrieve | @@ -59,8 +57,6 @@ List applications in AWS AppConfig | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `maxResults` | number | No | Maximum number of applications to return \(1-50\) | | `nextToken` | string | No | Pagination token from a previous response | @@ -88,8 +84,6 @@ Create an application in AWS AppConfig | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Name of the application to create | | `description` | string | No | Description of the application | @@ -115,8 +109,6 @@ Get details about a single AWS AppConfig application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID to retrieve | #### Output @@ -140,8 +132,6 @@ Update the name or description of an AWS AppConfig application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID to update | | `name` | string | No | New name for the application | | `description` | string | No | New description for the application | @@ -168,8 +158,6 @@ Delete an AWS AppConfig application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID to delete | #### Output @@ -192,8 +180,6 @@ List environments for an AWS AppConfig application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the environments | | `maxResults` | number | No | Maximum number of environments to return \(1-50\) | | `nextToken` | string | No | Pagination token from a previous response | @@ -224,8 +210,6 @@ Create an environment for an AWS AppConfig application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID to create the environment in | | `name` | string | Yes | Name of the environment to create | | `description` | string | No | Description of the environment | @@ -253,8 +237,6 @@ Get details about a single AWS AppConfig environment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the environment | | `environmentId` | string | Yes | The environment ID to retrieve | @@ -284,8 +266,6 @@ Update the name or description of an AWS AppConfig environment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the environment | | `environmentId` | string | Yes | The environment ID to update | | `name` | string | No | New name for the environment | @@ -314,8 +294,6 @@ Delete an AWS AppConfig environment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the environment | | `environmentId` | string | Yes | The environment ID to delete | @@ -340,8 +318,6 @@ List configuration profiles for an AWS AppConfig application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the configuration profiles | | `maxResults` | number | No | Maximum number of configuration profiles to return \(1-50\) | | `nextToken` | string | No | Pagination token from a previous response | @@ -373,8 +349,6 @@ Create a configuration profile in an AWS AppConfig application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID to create the configuration profile in | | `name` | string | Yes | Name of the configuration profile | | `locationUri` | string | Yes | Where the configuration is stored. Use "hosted" for AppConfig-hosted configurations, or an SSM/S3 URI | @@ -406,8 +380,6 @@ Get details about a single AWS AppConfig configuration profile | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the configuration profile | | `configurationProfileId` | string | Yes | The configuration profile ID to retrieve | @@ -438,8 +410,6 @@ Update the name, description, or retrieval role of an AppConfig configuration pr | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the configuration profile | | `configurationProfileId` | string | Yes | The configuration profile ID to update | | `name` | string | No | New name for the configuration profile | @@ -470,8 +440,6 @@ Delete an AWS AppConfig configuration profile | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the configuration profile | | `configurationProfileId` | string | Yes | The configuration profile ID to delete | @@ -496,8 +464,6 @@ Create a new hosted configuration version for an AppConfig configuration profile | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the configuration profile | | `configurationProfileId` | string | Yes | The configuration profile ID to add the version to | | `content` | string | Yes | The configuration content \(e.g., a JSON or YAML document\) | @@ -530,8 +496,6 @@ Retrieve a specific hosted configuration version from an AppConfig profile | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the configuration profile | | `configurationProfileId` | string | Yes | The configuration profile ID to read the version from | | `versionNumber` | number | Yes | The version number to retrieve | @@ -561,8 +525,6 @@ List hosted configuration versions for an AWS AppConfig configuration profile | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the configuration profile | | `configurationProfileId` | string | Yes | The configuration profile ID to list versions for | | `maxResults` | number | No | Maximum number of versions to return \(1-50\) | @@ -595,8 +557,6 @@ Delete a specific hosted configuration version from an AppConfig profile | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID that owns the configuration profile | | `configurationProfileId` | string | Yes | The configuration profile ID that owns the version | | `versionNumber` | number | Yes | The version number to delete | @@ -623,8 +583,6 @@ List deployment strategies available in AWS AppConfig | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `maxResults` | number | No | Maximum number of deployment strategies to return \(1-50\) | | `nextToken` | string | No | Pagination token from a previous response | @@ -657,8 +615,6 @@ Start deploying a configuration version to an AWS AppConfig environment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID to deploy in | | `environmentId` | string | Yes | The environment ID to deploy to | | `deploymentStrategyId` | string | Yes | The deployment strategy ID to use | @@ -688,8 +644,6 @@ Get details about a specific AWS AppConfig deployment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID of the deployment | | `environmentId` | string | Yes | The environment ID of the deployment | | `deploymentNumber` | number | Yes | The sequence number of the deployment | @@ -724,8 +678,6 @@ List deployments for an AWS AppConfig environment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID of the deployments | | `environmentId` | string | Yes | The environment ID of the deployments | | `maxResults` | number | No | Maximum number of deployments to return \(1-50\) | @@ -760,8 +712,6 @@ Stop an in-progress AWS AppConfig deployment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `applicationId` | string | Yes | The application ID of the deployment | | `environmentId` | string | Yes | The environment ID of the deployment | | `deploymentNumber` | number | Yes | The sequence number of the deployment to stop | diff --git a/apps/docs/content/docs/integrations/ashby.mdx b/apps/docs/content/docs/integrations/ashby.mdx index 97b66423074..ba0a259f5d0 100644 --- a/apps/docs/content/docs/integrations/ashby.mdx +++ b/apps/docs/content/docs/integrations/ashby.mdx @@ -2,7 +2,7 @@ title: Ashby description: Manage candidates, jobs, and applications in Ashby integration: - type: "ashby" + type: "ashby_v2" color: "#5D4ED6" --- @@ -63,7 +63,6 @@ Adds a tag to a candidate in Ashby and returns the updated candidate. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `candidateId` | string | Yes | The UUID of the candidate to add the tag to | | `tagId` | string | Yes | The UUID of the tag to add | @@ -122,7 +121,6 @@ Remove personally identifiable information from an Ashby candidate while retaini | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `candidateId` | string | Yes | UUID of the candidate to anonymize | @@ -180,7 +178,6 @@ Changes the source attributed to an existing application, so programmatically cr | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `applicationId` | string | Yes | UUID of the application whose source should change | | `sourceId` | string | No | UUID of the source to attribute the application to, as returned by List Sources. Omit only when unsetSource is true. | @@ -240,7 +237,6 @@ Moves an application to a different interview stage. Requires an archive reason | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `applicationId` | string | Yes | The UUID of the application to update the stage of | | `interviewStageId` | string | Yes | The UUID of the interview stage to move the application to | @@ -301,7 +297,6 @@ Creates a new application for a candidate on a job. Optionally specify interview | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `candidateId` | string | Yes | The UUID of the candidate to consider for the job | | `jobId` | string | Yes | The UUID of the job to consider the candidate for | @@ -366,7 +361,6 @@ Creates a new candidate record in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `name` | string | Yes | The candidate full name | | `email` | string | No | Primary email address for the candidate | @@ -434,7 +428,6 @@ Creates a note on a candidate in Ashby. Supports plain text and HTML content (bo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `candidateId` | string | Yes | The UUID of the candidate to add the note to | | `note` | string | Yes | The note content. If noteType is text/html, supports: <b>, <i>, <u>, <a>, <ul>, <ol>, <li>, <code>, <pre> | @@ -469,7 +462,6 @@ Permanently deletes an application in Ashby. Requires the candidatesDelete permi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `applicationId` | string | Yes | UUID of the application to delete | @@ -491,7 +483,6 @@ Retrieves full details about a single application by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `applicationId` | string | No | The UUID of the application to fetch | | `submittedFormInstanceId` | string | No | Submitted application-form instance UUID to use instead of applicationId | | `expand` | json | No | Ashby-supported application expansions to include | @@ -550,7 +541,6 @@ Retrieves full details about a single candidate by their ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `candidateId` | string | No | The UUID of the candidate to fetch | | `externalMappingId` | string | No | External mapping ID to use instead of the Ashby candidate UUID | @@ -608,7 +598,6 @@ Retrieves full details about a single job by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `jobId` | string | Yes | The UUID of the job to fetch | | `includeUnpublishedJobPostingIds` | boolean | No | Include IDs for unpublished job postings on this job | @@ -666,7 +655,6 @@ Retrieves full details about a single job posting by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `jobPostingId` | string | Yes | The UUID of the job posting to fetch | | `jobBoardId` | string | No | Optional job board UUID. If omitted, returns posting for the external job board. | | `expandJob` | boolean | No | Whether to expand and include the related job object in the response | @@ -742,7 +730,6 @@ Retrieves full details about a single offer by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `offerId` | string | Yes | The UUID of the offer to fetch | | `excludeFormDefinition` | boolean | No | Omit the offer form definition from the response | @@ -800,7 +787,6 @@ Retrieves one Ashby headcount opening by UUID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `openingId` | string | Yes | Opening UUID | #### Output @@ -857,7 +843,6 @@ Lists submitted interview feedback, optionally for one application, with paginat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `applicationId` | string | No | Application UUID | | `cursor` | string | No | Pagination cursor | | `perPage` | number | No | Results per page \(1-100\) | @@ -894,7 +879,6 @@ Lists the full stage history and allowed actions for an application. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `applicationId` | string | Yes | Application UUID | | `cursor` | string | No | Pagination cursor | | `perPage` | number | No | Results per page \(1-100\) | @@ -927,7 +911,6 @@ Lists all applications in an Ashby organization with pagination and optional fil | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default 100\) | | `status` | string | No | Application status to include: Active, Hired, Archived, or Lead | @@ -958,7 +941,6 @@ Lists all archive reasons configured in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `includeArchived` | boolean | No | Whether to include archived archive reasons in the response \(default false\) | #### Output @@ -983,7 +965,6 @@ Lists all candidate tags configured in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `includeArchived` | boolean | No | Whether to include archived candidate tags \(default false\) | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `syncToken` | string | No | Sync token from a previous response to fetch only changed results | @@ -1013,7 +994,6 @@ Lists all candidates in an Ashby organization with cursor-based pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default 100\) | | `createdAfter` | string | No | Only return candidates created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) | @@ -1041,7 +1021,6 @@ Lists all custom field definitions configured in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default and max 100\) | | `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then | @@ -1079,7 +1058,6 @@ Lists all departments in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default and max 100\) | | `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then | @@ -1114,7 +1092,6 @@ Lists interview schedules in Ashby, optionally filtered by application or interv | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `applicationId` | string | No | The UUID of the application to list interview schedules for | | `interviewStageId` | string | No | The UUID of the interview stage to list interview schedules for | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | @@ -1171,7 +1148,6 @@ Lists Ashby interview plans, including optional archived plans and incremental c | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `includeArchived` | boolean | No | Include archived interview plans | | `cursor` | string | No | Pagination cursor | | `perPage` | number | No | Results per page \(1-100\) | @@ -1203,7 +1179,6 @@ Lists the ordered stages in an Ashby interview plan. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `interviewPlanId` | string | Yes | Interview plan UUID | #### Output @@ -1230,7 +1205,6 @@ Lists all job postings in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `location` | string | No | Filter by location name \(case sensitive\) | | `department` | string | No | Filter by department name \(case sensitive\) | | `listedOnly` | boolean | No | When true, only returns listed \(publicly visible\) job postings \(default false\) | @@ -1275,7 +1249,6 @@ Lists all jobs in an Ashby organization. By default returns Open, Closed, and Ar | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default and max 100\). Ashby silently caps larger values rather than erroring. | | `syncToken` | string | No | Opaque token from a prior sync to fetch only jobs changed since then. Ashby only returns a new syncToken on the last page, so drain moreDataAvailable/nextCursor before persisting it. | @@ -1308,7 +1281,6 @@ Lists all locations configured in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default and max 100\) | | `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then | @@ -1351,7 +1323,6 @@ Lists all notes on a candidate with pagination support. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `candidateId` | string | Yes | The UUID of the candidate to list notes for | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(1-100\) | @@ -1385,7 +1356,6 @@ Lists all offers with their latest version in an Ashby organization. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page | | `createdAfter` | string | No | Only return offers created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) | @@ -1416,7 +1386,6 @@ Lists all openings in Ashby with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default 100\) | | `createdAfter` | string | No | Only return openings created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) | @@ -1442,7 +1411,6 @@ Lists all candidate sources configured in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `includeArchived` | boolean | No | When true, includes archived sources in results \(default false\) | #### Output @@ -1470,7 +1438,6 @@ Lists all users in Ashby with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default 100\) | | `includeDeactivated` | boolean | No | When true, includes deactivated users in results \(default false\) | @@ -1497,7 +1464,6 @@ Removes a tag from a candidate in Ashby and returns the updated candidate. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `candidateId` | string | Yes | The UUID of the candidate to remove the tag from | | `tagId` | string | Yes | The UUID of the tag to remove | @@ -1556,7 +1522,6 @@ Searches for candidates by name and/or email with AND logic. Results are limited | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `name` | string | No | Candidate name to search for \(combined with email using AND logic\) | | `email` | string | No | Candidate email to search for \(combined with name using AND logic\) | | `limit` | number | No | Maximum matches to return \(1-100\) | @@ -1579,7 +1544,6 @@ Searches Ashby jobs by title and/or requisition ID. Provide at least one of thes | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `title` | string | No | Job title search text | | `requisitionId` | string | No | Custom requisition ID | | `limit` | number | No | Maximum matches \(1-100\) | @@ -1603,7 +1567,6 @@ Searches Ashby headcount openings by human-readable identifier. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `identifier` | string | Yes | Opening identifier | #### Output @@ -1660,7 +1623,6 @@ Searches Ashby users by exact email address. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `email` | string | Yes | User email address | #### Output @@ -1681,7 +1643,6 @@ Sets the value of a single custom field on an Ashby Application, Candidate, Job, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `objectId` | string | Yes | UUID of the object to set the field on \(application, candidate, job, or opening\) | | `objectType` | string | Yes | Type of the object: Application, Candidate, Job, or Opening | @@ -1706,7 +1667,6 @@ Sets several custom field values on one Ashby Application, Candidate, Job, or Op | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `objectId` | string | Yes | UUID of the object to set the fields on \(application, candidate, job, or opening\) | | `objectType` | string | Yes | Type of the object: Application, Candidate, Job, or Opening | @@ -1730,7 +1690,6 @@ Updates an existing candidate record in Ashby. Only provided fields are changed. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `candidateId` | string | Yes | The UUID of the candidate to update | | `name` | string | No | Updated full name, or null | @@ -1803,7 +1762,6 @@ Transfers an application to another job, interview plan, and stage. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `onBehalfOfUserId` | string | No | Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls | | `applicationId` | string | Yes | Application UUID | | `jobId` | string | Yes | Destination job UUID | @@ -1865,7 +1823,6 @@ Securely uploads a file and attaches it to an Ashby candidate. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `candidateId` | string | Yes | Candidate UUID | | `file` | file | Yes | Stored file to attach to the candidate | | `fileName` | string | No | Optional filename override | @@ -1925,7 +1882,6 @@ Securely uploads a resume and sets it as the Ashby candidate resume. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Ashby API Key | | `candidateId` | string | Yes | Candidate UUID | | `file` | file | Yes | Stored resume file | | `fileName` | string | No | Optional filename override | diff --git a/apps/docs/content/docs/integrations/athena.mdx b/apps/docs/content/docs/integrations/athena.mdx index 90bacc9c569..23663e485cd 100644 --- a/apps/docs/content/docs/integrations/athena.mdx +++ b/apps/docs/content/docs/integrations/athena.mdx @@ -2,7 +2,7 @@ title: Athena description: Run SQL queries on data in Amazon S3 using AWS Athena integration: - type: "athena" + type: "athena_v2" color: "linear-gradient(45deg, #4D27A8 0%, #A166FF 100%)" --- @@ -10,12 +10,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -[Amazon Athena](https://aws.amazon.com/athena/) queries data in Amazon S3 with SQL. Start a query, check its execution status, then retrieve the results; cancel a query when needed. Run parameterized queries with execution parameters and prepared statements, reuse recent results, and read runtime statistics after a query completes. The integration also manages named queries, browses data catalogs, databases, and table schemas, and inspects workgroup configuration. Connect with an AWS access key and secret access key. +[Amazon Athena](https://aws.amazon.com/athena/) queries data in Amazon S3 with SQL. Start a query, check its execution status, then retrieve the results; cancel a query when needed. Run parameterized queries with execution parameters and prepared statements, reuse recent results, and read runtime statistics after a query completes. The integration also manages named queries, browses data catalogs, databases, and table schemas, and inspects workgroup configuration. Connect with a saved [AWS credential](/integrations/aws-credentials). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Execute SQL queries against data in S3, check query status and runtime statistics, retrieve results, manage named queries and prepared statements, and inspect data catalogs, databases, tables, and workgroups. Requires AWS access key and secret access key. +Execute SQL queries against data in S3, check query status and runtime statistics, retrieve results, manage named queries and prepared statements, and inspect data catalogs, databases, tables, and workgroups. ## Actions @@ -30,8 +30,6 @@ Start an SQL query execution in AWS Athena | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryString` | string | Yes | SQL query string to execute | | `database` | string | No | Database name within the catalog | | `catalog` | string | No | Data catalog name \(default: AwsDataCatalog\) | @@ -60,8 +58,6 @@ Get the status and details of an Athena query execution | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryExecutionId` | string | Yes | Query execution ID to check | #### Output @@ -98,8 +94,6 @@ Retrieve the results of a completed Athena query execution | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryExecutionId` | string | Yes | Query execution ID to get results for | | `maxResults` | number | No | Maximum number of rows to return \(1-999\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -126,8 +120,6 @@ Stop a running Athena query execution | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryExecutionId` | string | Yes | Query execution ID to stop | #### Output @@ -149,8 +141,6 @@ List recent Athena query execution IDs | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `workGroup` | string | No | Workgroup to list executions for \(default: primary\) | | `maxResults` | number | No | Maximum number of results \(0-50\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -175,8 +165,6 @@ Get the status and details of up to 50 Athena query executions in one call | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryExecutionIds` | string | Yes | Comma-separated query execution IDs to check \(up to 50\) | #### Output @@ -218,8 +206,6 @@ Create a saved/named query in AWS Athena | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Name for the saved query | | `database` | string | Yes | Database the query runs against | | `queryString` | string | Yes | SQL query string to save | @@ -245,8 +231,6 @@ Get details of a saved/named query in AWS Athena | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `namedQueryId` | string | Yes | Named query ID to retrieve | #### Output @@ -273,8 +257,6 @@ List saved/named query IDs in AWS Athena | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `workGroup` | string | No | Workgroup to list named queries for | | `maxResults` | number | No | Maximum number of results \(0-50\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -299,8 +281,6 @@ Delete a saved/named query in AWS Athena | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `namedQueryId` | string | Yes | Named query ID to delete | #### Output @@ -322,8 +302,6 @@ List the databases available in an Athena data catalog | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `catalogName` | string | Yes | Data catalog name to list databases from \(e.g., AwsDataCatalog\) | | `workGroup` | string | No | Workgroup for which the metadata is being fetched \(required for IAM Identity Center enabled catalogs\) | | `maxResults` | number | No | Maximum number of results \(1-50\) | @@ -352,8 +330,6 @@ List tables and their column/partition metadata for an Athena database | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `catalogName` | string | Yes | Data catalog name \(e.g., AwsDataCatalog\) | | `databaseName` | string | Yes | Database name to list tables from | | `expression` | string | No | Regex filter that pattern-matches table names | @@ -394,8 +370,6 @@ Get runtime statistics (timeline, row counts, and output stage) for a completed | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryExecutionId` | string | Yes | Query execution ID to get runtime statistics for | #### Output @@ -438,8 +412,6 @@ Get the details of up to 50 Athena named queries by ID in a single call | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `namedQueryIds` | string | Yes | Comma-separated named query IDs \(up to 50\) | #### Output @@ -471,8 +443,6 @@ Update the name, description, or SQL of an existing Athena named query (database | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `namedQueryId` | string | Yes | Named query ID to update | | `name` | string | Yes | New name for the query \(1-128 characters\) | | `queryString` | string | Yes | New SQL query text | @@ -497,8 +467,6 @@ Create a parameterized prepared statement in an Athena workgroup for use with EX | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `statementName` | string | Yes | Prepared statement name \(letters, digits, _ @ : ; must start with a letter or underscore\) | | `workGroup` | string | Yes | Workgroup the prepared statement belongs to | | `queryStatement` | string | Yes | SQL statement with ? placeholders for parameters | @@ -523,8 +491,6 @@ Get the SQL and metadata of a prepared statement in an Athena workgroup | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `statementName` | string | Yes | Prepared statement name \(letters, digits, _ @ : ; must start with a letter or underscore\) | | `workGroup` | string | Yes | Workgroup the prepared statement belongs to | @@ -551,8 +517,6 @@ Get the details of up to 256 prepared statements in an Athena workgroup by name | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `preparedStatementNames` | string | Yes | Comma-separated prepared statement names \(up to 256\) | | `workGroup` | string | Yes | Workgroup the prepared statement belongs to | @@ -584,8 +548,6 @@ Replace the SQL and description of an existing prepared statement in an Athena w | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `statementName` | string | Yes | Prepared statement name \(letters, digits, _ @ : ; must start with a letter or underscore\) | | `workGroup` | string | Yes | Workgroup the prepared statement belongs to | | `queryStatement` | string | Yes | New SQL statement with ? placeholders for parameters | @@ -610,8 +572,6 @@ List the prepared statements saved in an Athena workgroup | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `workGroup` | string | Yes | Workgroup to list prepared statements for | | `maxResults` | number | No | Maximum number of results \(1-50\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -638,8 +598,6 @@ Delete a prepared statement from an Athena workgroup | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `statementName` | string | Yes | Prepared statement name \(letters, digits, _ @ : ; must start with a letter or underscore\) | | `workGroup` | string | Yes | Workgroup the prepared statement belongs to | @@ -662,8 +620,6 @@ List the data catalogs (data sources) registered in the AWS account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `workGroup` | string | No | Workgroup name \(required for IAM Identity Center requests\) | | `maxResults` | number | No | Maximum number of results \(2-50\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -693,8 +649,6 @@ Get the type, status, and connection parameters of an Athena data catalog | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Data catalog name \(e.g., AwsDataCatalog\) | | `workGroup` | string | No | Workgroup name \(required for IAM Identity Center requests\) | @@ -723,8 +677,6 @@ Get a single database (name, description, and parameters) from an Athena data ca | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `catalogName` | string | Yes | Data catalog name \(e.g., AwsDataCatalog\) | | `databaseName` | string | Yes | Database name | | `workGroup` | string | No | Workgroup name \(required for IAM Identity Center enabled catalogs\) | @@ -750,8 +702,6 @@ Get the columns, partition keys, and properties of a single table in an Athena d | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `catalogName` | string | Yes | Data catalog name \(e.g., AwsDataCatalog\) | | `databaseName` | string | Yes | Database name | | `tableName` | string | Yes | Table name | @@ -788,8 +738,6 @@ List the Athena workgroups available in the AWS account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `maxResults` | number | No | Maximum number of results \(1-50\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -821,8 +769,6 @@ Get the configuration of an Athena workgroup, including its result location, enc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `workGroup` | string | Yes | Workgroup name | #### Output diff --git a/apps/docs/content/docs/integrations/aws-credentials.mdx b/apps/docs/content/docs/integrations/aws-credentials.mdx new file mode 100644 index 00000000000..65308e11f36 --- /dev/null +++ b/apps/docs/content/docs/integrations/aws-credentials.mdx @@ -0,0 +1,85 @@ +--- +title: AWS credentials +description: Save an AWS identity once and use it across every AWS block, with access keys, an assumed IAM role, or a keyless role that trusts Sim through OpenID Connect +--- + +import { Callout } from '@/components/ui/callout' +import { Step, Steps } from '@/components/ui/steps' + +An AWS credential holds one AWS identity for the whole workspace. Every AWS block (S3, DynamoDB, Lambda, SQS, Athena, CloudWatch, and the rest) signs in with a saved credential, so keys live in one place instead of in each block. + + +Blocks added before saved credentials, which take access keys typed into the block, are marked legacy and keep working exactly as they are. Upgrade one by replacing it with the current block and selecting a credential. + + +## Choose how Sim signs in + +| Option | What Sim stores | When to use it | +| --- | --- | --- | +| **IAM role (keyless, via Sim OIDC)** | Nothing secret: only the role ARN | Recommended. No long-lived keys to rotate or leak. | +| **Access keys that assume an IAM role** | An access key pair, used only to assume the role | Your AWS account doesn't allow OIDC providers, or you need an external ID. | +| **Access keys** | An access key pair | The same behavior as keys typed into a block, shared across blocks. | + +Every option resolves on Sim's servers at run time. The saved keys and the sessions Sim gets from them stay out of block outputs, logs, and anything a model sees, with two exceptions AWS requires: an S3 presigned URL made with a temporary session contains that session's token, and actions that issue credentials (STS **Assume Role** and **Get Session Token**, IAM **Create Access Key**) return what they issue. Anyone who can use the credential can run those actions to the extent its IAM permissions allow, so grant it only what your workflows need. Each assumed-role session is named `sim-`, so runs are traceable in CloudTrail. + + +STS **Get Session Token** works only with an **Access keys** credential: AWS refuses it for role sessions. An STS **Assume Role** call made with a role credential is role chaining, which AWS caps at one hour regardless of the requested duration. + + +## Set up a keyless IAM role [#keyless-role] + + + + In Sim, open **Integrations**, add an **AWS credential**, and choose **IAM role (keyless, via Sim OIDC)**. Keep the dialog open; it shows the values for the next steps. + + + In the AWS IAM console, open **Identity providers → Add provider**, choose **OpenID Connect**, and enter the dialog's **Issuer** as the **Provider URL**, and its **Audience**. You only do this once per AWS account. + + + Enter the role's ARN (`arn:aws:iam:::role/`) in the dialog first. The **Role trust policy** fills in its account ID; until then it shows ``, which IAM rejects. + + + Create the role with that name (or edit the existing one) and set its trust policy to the **Role trust policy** shown in the dialog. It trusts only this credential's **Subject**, so no other Sim credential, workspace, or customer can assume the role. + + + Attach the permissions your workflows need to the role, then save. Sim verifies the connection by assuming the role before saving it, and refuses a role that would also trust a different subject. + + + + +The subject is unique to the credential. Reconnecting a keyless credential keeps its subject by default, so the role's trust policy keeps working; choose **Issue a new subject** only if you also update the trust policy. + + +### From the API or CLI [#keyless-api-cli] + +Run `sim credentials keyless-subject aws-service-account`, trust the printed subject as above, then add `roleArn` (optionally `stsRegion` and `sessionDurationSeconds`) to its `keylessConfig` and pass it to `sim credentials create aws-service-account`. See [the CLI credentials reference](/cli/credentials) for the full two-step example; the proof expires after 24 hours and works only for the user who requested it. + +## Use access keys + +Create an IAM user (or use an existing one) with an access key, choose **Access keys** or **Access keys that assume an IAM role**, and enter the key pair. For an assumed role, add the role ARN and, if the role's trust policy requires one, the external ID. For AWS GovCloud or China keys, set the **STS region** (for example `us-gov-west-1`). Sim checks the keys with `sts:GetCallerIdentity` before saving. + +To rotate a key, create the new key in AWS, reconnect the credential with it, confirm a run succeeds, then deactivate and delete the old key. Every block using the credential switches at once. + +## Self-hosted deployments + +Keyless roles need two things on a self-hosted deployment: + +- `WORKLOAD_IDENTITY_SIGNING_KEY`: a PKCS#8 PEM RSA private key that signs Sim's tokens. +- An issuer AWS can reach over the public internet: an `https` URL on the default port, served by a certificate from a public CA. It defaults to `/oidc/workload`; set `WORKLOAD_IDENTITY_ISSUER` to pin it. Trust policies reference this URL, so never change it once roles trust it. Sim keeps keyless off when the issuer isn't `https` or names a port, query, or fragment, because AWS can't use it. + +Generate a key with `openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048`. + +To rotate the signing key without failed runs: + +1. Set the new key as `WORKLOAD_IDENTITY_PREVIOUS_KEY` and redeploy (including any background workers that run workflows). Sim publishes it but keeps signing with the current key. +2. Wait at least 5 minutes, so every cached copy of the published keys includes the new one. If any keyless [Snowflake connection](/integrations/snowflake-service-account#keyless-service-user) exists, wait 60 minutes instead: Snowflake caches an issuer's keys for up to an hour. +3. Swap them: set the new key as `WORKLOAD_IDENTITY_SIGNING_KEY` and the old key as `WORKLOAD_IDENTITY_PREVIOUS_KEY`, and redeploy everything that runs workflows. +4. Once every instance signs with the new key, wait 5 more minutes (10 with keyless Snowflake connections, whose tokens last that long), then remove `WORKLOAD_IDENTITY_PREVIOUS_KEY`. + +Without a signing key, the keyless option is unavailable and access keys work as usual. + +## Troubleshooting + +- **`InvalidIdentityToken` or "No OpenIDConnect provider found":** the IAM identity provider's URL or audience doesn't match the dialog exactly, or AWS can't reach the issuer. Open `/.well-known/openid-configuration` from outside your network; it must load over `https`. +- **"Sim could not assume this role: Not authorized to perform sts:AssumeRoleWithWebIdentity":** the role's trust policy doesn't trust this credential's subject. Paste the trust policy from the dialog again; if you chose **Issue a new subject**, update the policy to the new one. +- **"This role trusts every Sim subject":** the trust policy is missing the `sub` condition. Use the policy from the dialog. diff --git a/apps/docs/content/docs/integrations/azure_devops.mdx b/apps/docs/content/docs/integrations/azure_devops.mdx index 8b2bcc313c2..f376227d22d 100644 --- a/apps/docs/content/docs/integrations/azure_devops.mdx +++ b/apps/docs/content/docs/integrations/azure_devops.mdx @@ -2,7 +2,7 @@ title: Azure DevOps description: Interact with Azure DevOps pipelines, builds, and work items integration: - type: "azure_devops" + type: "azure_devops_v2" color: "#0078D4" --- @@ -34,7 +34,6 @@ List all pipelines in an Azure DevOps project. Returns pipeline ID, name, folder | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `orderBy` | string | No | Field to sort results by \(e.g. "name"\) | | `top` | number | No | Maximum number of pipelines to return | @@ -66,7 +65,6 @@ Get details for a specific pipeline in an Azure DevOps project, including config | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `pipelineId` | number | Yes | ID of the pipeline to retrieve | | `pipelineVersion` | number | No | Specific revision of the pipeline to retrieve \(defaults to latest\) | @@ -105,7 +103,6 @@ List runs for a specific pipeline in an Azure DevOps project. Returns run ID, na | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `pipelineId` | number | Yes | ID of the pipeline whose runs to list | @@ -138,7 +135,6 @@ Get details for a specific pipeline run in an Azure DevOps project. Returns run | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `pipelineId` | number | Yes | ID of the pipeline | | `runId` | number | Yes | ID of the run to retrieve | @@ -177,7 +173,6 @@ List builds in an Azure DevOps project. Optionally filter by pipeline definition | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `definitionIds` | string | No | Comma-separated pipeline definition IDs to filter by \(e.g. "1,2,3"\) | | `top` | number | No | Maximum number of builds to return | @@ -219,7 +214,6 @@ List all log entries for a specific build in Azure DevOps. Returns log IDs, type | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `buildId` | number | Yes | The build ID whose logs to list | @@ -250,7 +244,6 @@ Fetch the text content of a specific build log in Azure DevOps. Use List Build L | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `buildId` | number | Yes | The build ID containing the log | | `logId` | number | Yes | The log entry ID to fetch \(from List Build Logs\) | @@ -277,7 +270,6 @@ Get the execution timeline for an Azure DevOps build — every stage, job, and t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `buildId` | number | Yes | ID of the build whose timeline to retrieve | @@ -322,7 +314,6 @@ Get work item references associated with commits between two builds in Azure Dev | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `fromBuildId` | number | Yes | The older build ID \(start of range\) | | `toBuildId` | number | Yes | The newer build ID \(end of range\) | @@ -350,7 +341,6 @@ Execute a WIQL query to search for work items in Azure DevOps and return full fi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `wiqlQuery` | string | Yes | WIQL query string \(e.g. "SELECT \[System.Id\] FROM workitems WHERE \[System.State\] = 'Doing' ORDER BY \[System.Id\] ASC"\). Use TOP N to limit results. | @@ -383,7 +373,6 @@ Fetch full details of a single work item by ID from Azure DevOps, including titl | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `workItemId` | number | Yes | The work item ID to fetch | @@ -414,7 +403,6 @@ Fetch full details for multiple work items by ID from Azure DevOps. Pass comma-s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `ids` | string | Yes | Comma-separated work item IDs to fetch \(e.g. "123,456,789"\). Lists longer than 200 IDs are chunked automatically. | @@ -447,7 +435,6 @@ Create a new Basic-process work item (Issue, Task, or Epic) in Azure DevOps. Ret | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `workItemType` | string | Yes | Basic-process work item type to create \("Issue", "Task", or "Epic"\). Use Issue for bug or defect tracking. | | `title` | string | Yes | Title of the new work item | @@ -491,7 +478,6 @@ Update one or more fields on an existing work item in Azure DevOps. Provide only | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `workItemId` | number | Yes | ID of the work item to update | | `title` | string | No | New title for the work item \(optional\) | @@ -535,7 +521,6 @@ Add a comment to a work item in Azure DevOps. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `workItemId` | number | Yes | ID of the work item to comment on | | `text` | string | Yes | Comment text \(HTML supported, e.g. "<p>My comment</p>"\) | @@ -571,7 +556,6 @@ List comments for an Azure DevOps work item. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `organization` | string | Yes | Azure DevOps organization name | | `project` | string | Yes | Azure DevOps project name | | `workItemId` | number | Yes | ID of the work item whose comments should be listed | | `top` | number | No | Maximum number of comments to return | diff --git a/apps/docs/content/docs/integrations/bitbucket-service-account.mdx b/apps/docs/content/docs/integrations/bitbucket-service-account.mdx new file mode 100644 index 00000000000..afa099a3e2a --- /dev/null +++ b/apps/docs/content/docs/integrations/bitbucket-service-account.mdx @@ -0,0 +1,66 @@ +--- +title: Bitbucket access tokens +description: Connect Bitbucket Cloud to Sim with a workspace, project, or repository access token instead of a personal account +--- + +import { Callout } from '@/components/ui/callout' +import { Step, Steps } from '@/components/ui/steps' + +Connect Bitbucket Cloud with an access token when a workflow should act as an integration rather than as the person who connected it. An access token belongs to a workspace, project, or repository, not to a user, so it keeps working when people leave and its reach is limited to what it was created for. + +Bitbucket shows actions taken with the token (comments, merges, pipeline runs) under the token's name. + +## Prerequisites + +Admin access to the workspace, project, or repository the token is for. Workspace and project access tokens require a Bitbucket Cloud Premium plan; repository access tokens do not. + +## Create the access token [#creating-the-access-token] + + + + Open the repository, project, or workspace settings in Bitbucket Cloud and go to **Access tokens** + + + Click **Create access token** and give it a name (e.g. `Sim workflows`) — this is the name Bitbucket shows on everything the workflow does + + + Select **Repositories: Read** (`repository`). Add **Pull requests**, **Repositories: Write**, or **Pipelines** scopes for the actions the workflow takes + + + Create the token and copy it right away — Bitbucket shows it only once + + + + +A repository access token can only reach its repository, and a project access token only the repositories in its project. Treat the token like a password; Sim encrypts it at rest. + + +## Add the access token to Sim [#adding-the-access-token-to-sim] + + + + Open **Integrations** from your workspace sidebar + + + Search for "Bitbucket" and open it, then click **Add to Sim** and choose **Add access token** + + + Paste the token and enter a repository it can read as `workspace/repository` (the `my-workspace/my-repository` in `bitbucket.org/my-workspace/my-repository`). For a repository access token, that is its own repository + + + Click **Add access token**. Sim verifies the token by reading that repository once, so the token needs the `repository` scope + + + +## Use the credential in workflows [#using-the-credential-in-workflows] + +Add a Bitbucket block and select the saved access token in the account dropdown, then choose the workspace and repository. The block calls `api.bitbucket.org` with the token, exactly as it does with a connected account. + +Two things need a signed-in account and do not work with an access token: + +- **List Workspaces** lists the workspaces of the person who connected the account, so Sim refuses to run it with an access token. Use a connected account for it. +- The workspace picker lists a person's workspaces, so it cannot load them for an access token. Switch the workspace field to manual entry and type the slug. + +## Rotate tokens and handle permission failures + +If validation succeeds but an action fails, the token is likely missing that action's scope, or the repository is outside the token's project or repository. Access tokens cannot be edited after creation: create a replacement with the right scopes, update the credential in Sim, test the workflow, and then revoke the old token. See [Bitbucket's access token documentation](https://support.atlassian.com/bitbucket-cloud/docs/access-tokens/). diff --git a/apps/docs/content/docs/integrations/bitbucket.mdx b/apps/docs/content/docs/integrations/bitbucket.mdx index b3d0cc8a810..d0712cd7a4e 100644 --- a/apps/docs/content/docs/integrations/bitbucket.mdx +++ b/apps/docs/content/docs/integrations/bitbucket.mdx @@ -12,9 +12,11 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} Use [Bitbucket](https://bitbucket.org/) Cloud to read repositories, branches, commits, files, and diffs; create or delete branches; and manage pull requests and comments. Pipeline actions list runs and steps, start or stop runs, and retrieve step logs. -Connect a Bitbucket account through OAuth. Triggers can start workflows on pushes, repository changes, build status updates, and pull request events; Sim manages their webhooks. For example, a workflow can read a failed pipeline's logs or review a newly opened pull request. +Connect a Bitbucket account through OAuth, or a workspace, project, or repository [access token](/integrations/bitbucket-service-account). Triggers can start workflows on pushes, repository changes, build status updates, and pull request events; Sim manages their webhooks. For example, a workflow can read a failed pipeline's logs or review a newly opened pull request. {/* MANUAL-CONTENT-END */} +Connection guides: [Access tokens](/integrations/bitbucket-service-account). + ## OAuth scopes [#oauth-scopes] Sim requests these scopes when someone connects a Bitbucket account. For a self-hosted deployment, register your own app with the provider using the settings below. See [Integrations and OAuth](/platform/self-hosting/integrations-oauth) for setup. diff --git a/apps/docs/content/docs/integrations/brex.mdx b/apps/docs/content/docs/integrations/brex.mdx index 85d02c8dc38..db1e97f70a1 100644 --- a/apps/docs/content/docs/integrations/brex.mdx +++ b/apps/docs/content/docs/integrations/brex.mdx @@ -2,7 +2,7 @@ title: Brex description: Manage expenses, receipts, transactions, and team data in Brex integration: - type: "brex" + type: "brex_v2" color: "#171717" --- @@ -37,7 +37,6 @@ List expenses in the Brex account with optional filters for user, status, paymen | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `userIds` | string | No | Comma-separated user IDs to filter expenses by owner | | `statuses` | string | No | Comma-separated expense statuses to filter by: DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED | | `paymentStatuses` | string | No | Comma-separated payment statuses to filter by: NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED | @@ -103,7 +102,6 @@ Get a single Brex expense by its ID, including merchant, user, and receipt detai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `expenseId` | string | Yes | ID of the expense to fetch | #### Output @@ -170,7 +168,6 @@ Update the memo of a Brex card expense | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `expenseId` | string | Yes | ID of the card expense to update | | `memo` | string | Yes | New memo for the expense | @@ -206,7 +203,6 @@ Upload a receipt file and attach it to a specific Brex card expense | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `expenseId` | string | Yes | ID of the card expense to attach the receipt to | | `file` | file | Yes | Receipt file to upload \(max 50 MB\) | | `receiptName` | string | No | Receipt file name including extension \(defaults to the uploaded file name\) | @@ -231,7 +227,6 @@ Upload a receipt file and let Brex automatically match it with existing expenses | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `file` | file | Yes | Receipt file to upload \(max 50 MB\) | | `receiptName` | string | No | Receipt file name including extension \(defaults to the uploaded file name\) | @@ -255,7 +250,6 @@ List settled card transactions for all Brex card accounts | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `userIds` | string | No | Comma-separated user IDs to filter transactions by cardholder | | `postedAtStart` | string | No | Only include transactions posted at or after this ISO 8601 timestamp | | `cursor` | string | No | Pagination cursor from a previous response | @@ -294,7 +288,6 @@ List transactions for a Brex cash account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `accountId` | string | Yes | ID of the cash account to list transactions for | | `postedAtStart` | string | No | Only include transactions posted at or after this ISO 8601 timestamp | | `cursor` | string | No | Pagination cursor from a previous response | @@ -328,7 +321,6 @@ List all Brex card accounts with balances and limits | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | #### Output @@ -360,7 +352,6 @@ List all Brex cash accounts with balances and account details | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of accounts to return \(default 100, max 1000\) | @@ -395,7 +386,6 @@ Get a Brex cash account by ID, or the primary cash account when no ID is provide | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `accountId` | string | No | ID of the cash account \(defaults to the primary cash account\) | #### Output @@ -427,7 +417,6 @@ List finalized statements for the primary Brex card account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of statements to return \(default 100, max 1000\) | @@ -458,7 +447,6 @@ List finalized statements for a Brex cash account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `accountId` | string | Yes | ID of the cash account to list statements for | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of statements to return \(default 100, max 1000\) | @@ -490,7 +478,6 @@ List users in the Brex account, optionally filtered by email | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `email` | string | No | Filter users by exact email address | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of users to return \(default 100, max 1000\) | @@ -523,7 +510,6 @@ Get a Brex user by their ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `userId` | string | Yes | ID of the user to fetch | #### Output @@ -552,7 +538,6 @@ Get the Brex user associated with the API token | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | #### Output @@ -580,7 +565,6 @@ List departments in the Brex account, optionally filtered by name | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `name` | string | No | Filter departments by name | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of departments to return \(default 100, max 1000\) | @@ -607,7 +591,6 @@ List locations in the Brex account, optionally filtered by name | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `name` | string | No | Filter locations by name | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of locations to return \(default 100, max 1000\) | @@ -634,7 +617,6 @@ List job titles in the Brex account, optionally filtered by name | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `name` | string | No | Filter titles by name | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of titles to return \(default 100, max 1000\) | @@ -660,7 +642,6 @@ List cards in the Brex account, optionally filtered by card owner | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `userId` | string | No | Filter cards by the ID of the card owner | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of cards to return \(default 100, max 1000\) | @@ -695,7 +676,6 @@ Get the Brex company associated with the API token | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | #### Output @@ -718,7 +698,6 @@ List budgets in the Brex account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of budgets to return \(default 100, max 1000\) | @@ -755,7 +734,6 @@ Get a Brex budget by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `budgetId` | string | Yes | ID of the budget to fetch | #### Output @@ -789,7 +767,6 @@ Create a new budget in the Brex account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `name` | string | Yes | Name for the budget | | `description` | string | Yes | Description of what the budget is used for | | `parentBudgetId` | string | Yes | ID of the parent budget | @@ -831,7 +808,6 @@ Archive a Brex budget, making any spend limits beneath it unusable for future ex | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `budgetId` | string | Yes | ID of the budget to archive | #### Output @@ -853,7 +829,6 @@ List spend limits in the Brex account, optionally filtered by member user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `memberUserIds` | string | No | Comma-separated user IDs to filter spend limits by member | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of spend limits to return \(default 100, max 1000\) | @@ -901,7 +876,6 @@ Get a Brex spend limit by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `spendLimitId` | string | Yes | ID of the spend limit to fetch | #### Output @@ -945,7 +919,6 @@ Create a new spend limit (hard-authorization card program) in the Brex account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `name` | string | Yes | Name for the spend limit | | `periodRecurrenceType` | string | Yes | Period type of the spend limit \(PER_WEEK, PER_MONTH, PER_QUARTER, PER_YEAR, ONE_TIME\) | | `spendType` | string | Yes | Whether the spend limit can only be spent from cards it provisions \(BUDGET_PROVISIONED_CARDS_ONLY, NON_BUDGET_PROVISIONED_CARDS_ALLOWED\) | @@ -1009,7 +982,6 @@ List vendors in the Brex account, optionally filtered by name | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `name` | string | No | Filter vendors by name | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of vendors to return \(default 100, max 1000\) | @@ -1038,7 +1010,6 @@ Get a Brex vendor by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `vendorId` | string | Yes | ID of the vendor to fetch | #### Output @@ -1063,7 +1034,6 @@ Create a new vendor in the Brex account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `companyName` | string | Yes | Name for the vendor \(must be unique\) | | `email` | string | No | Email address for the vendor | | `phone` | string | No | Phone number for the vendor | @@ -1090,7 +1060,6 @@ Update an existing vendor in the Brex account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `vendorId` | string | Yes | ID of the vendor to update | | `companyName` | string | No | New name for the vendor | | `email` | string | No | New email address for the vendor | @@ -1118,7 +1087,6 @@ List money transfers in the Brex account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `cursor` | string | No | Pagination cursor from a previous response | | `limit` | string | No | Number of transfers to return \(default 100, max 1000\) | @@ -1158,7 +1126,6 @@ Get a Brex money transfer by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `transferId` | string | Yes | ID of the transfer to fetch | #### Output @@ -1195,7 +1162,6 @@ Create a money transfer from a Brex cash account to a vendor | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | | `cashAccountId` | string | Yes | ID of the Brex cash account to send the transfer from \(found via the /accounts endpoint\) | | `vendorPaymentInstrumentId` | string | Yes | ID of the vendor's payment instrument to send the transfer to \(from the vendor's payment_accounts\) | | `amount` | number | Yes | Amount to transfer, in the smallest unit of the currency \(e.g., cents for USD\) | diff --git a/apps/docs/content/docs/integrations/brightdata.mdx b/apps/docs/content/docs/integrations/brightdata.mdx index 8c5d3804195..a371651af8e 100644 --- a/apps/docs/content/docs/integrations/brightdata.mdx +++ b/apps/docs/content/docs/integrations/brightdata.mdx @@ -2,7 +2,7 @@ title: Bright Data description: Scrape websites, search engines, and extract structured data integration: - type: "brightdata" + type: "brightdata_v2" color: "#FFFFFF" --- @@ -29,7 +29,6 @@ Fetch page content with Bright Data Web Unlocker, including its supported anti-b | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Bright Data API token | | `zone` | string | Yes | Web Unlocker zone name from your Bright Data dashboard \(e.g., "web_unlocker1"\) | | `url` | string | Yes | The URL to scrape \(e.g., "https://example.com/page") | | `format` | string | No | Response format: "raw" for HTML or "json" for parsed content. Defaults to "raw" | @@ -56,7 +55,6 @@ Search Google, Bing, DuckDuckGo, or Yandex and get structured search results usi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Bright Data API token | | `zone` | string | Yes | SERP API zone name from your Bright Data dashboard \(e.g., "serp_api1"\) | | `query` | string | Yes | The search query \(e.g., "best project management tools"\) | | `searchEngine` | string | No | Search engine to use: "google", "bing", "duckduckgo", or "yandex". Defaults to "google" | @@ -88,7 +86,6 @@ AI-powered web discovery that finds and ranks results by intent. Returns up to 2 | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Bright Data API token | | `query` | string | Yes | The search query \(e.g., "competitor pricing changes enterprise plan"\) | | `numResults` | number | No | Number of results to return \(1-20\). Defaults to 10 | | `mode` | string | No | Search depth and ranking mode: "standard" \(balanced\), "deep" \(exhaustive, broader search\), "fast" \(optimized for speed\), or "zeroRanking" \(raw volume without AI filtering\). Defaults to "standard" | @@ -123,7 +120,6 @@ Scrape URLs synchronously using a Bright Data pre-built scraper and get structur | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Bright Data API token | | `datasetId` | string | Yes | Dataset scraper ID from your Bright Data dashboard \(e.g., "gd_l1viktl72bvl7bjuj0"\) | | `urls` | string | Yes | JSON array of URL objects to scrape, up to 20 \(e.g., \[ \{ "url": "https://example.com/product" \} \]\) | | `format` | string | No | Output format: "json", "ndjson", or "csv". Defaults to "json" | @@ -149,7 +145,6 @@ Trigger a Bright Data pre-built scraper to extract structured data from URLs. Su | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Bright Data API token | | `datasetId` | string | Yes | Dataset scraper ID from your Bright Data dashboard \(e.g., "gd_l1viktl72bvl7bjuj0"\) | | `urls` | string | Yes | JSON array of URL objects to scrape \(e.g., \[ \{ "url": "https://example.com/product" \} \]\) | | `format` | string | No | Output format: "json" or "csv". Defaults to "json" | @@ -174,7 +169,6 @@ Check the progress of an async Bright Data scraping job. Returns status: startin | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Bright Data API token | | `snapshotId` | string | Yes | The snapshot ID returned when the collection was triggered \(e.g., "s_m4x7enmven8djfqak"\) | #### Output @@ -197,7 +191,6 @@ Download the results of a completed Bright Data scraping job using its snapshot | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Bright Data API token | | `snapshotId` | string | Yes | The snapshot ID returned when the collection was triggered \(e.g., "s_m4x7enmven8djfqak"\) | | `format` | string | No | Output format: "json", "ndjson", "jsonl", or "csv". Defaults to "json" | | `compress` | boolean | No | Whether to compress the results | @@ -222,7 +215,6 @@ Cancel an active Bright Data scraping job using its snapshot ID. Terminates data | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Bright Data API token | | `snapshotId` | string | Yes | The snapshot ID of the collection to cancel \(e.g., "s_m4x7enmven8djfqak"\) | #### Output diff --git a/apps/docs/content/docs/integrations/browser_use.mdx b/apps/docs/content/docs/integrations/browser_use.mdx index 6e76c7f1459..8e5a7bfc10f 100644 --- a/apps/docs/content/docs/integrations/browser_use.mdx +++ b/apps/docs/content/docs/integrations/browser_use.mdx @@ -2,7 +2,7 @@ title: Browser Use description: Run browser automation tasks integration: - type: "browser_use" + type: "browser_use_v2" color: "#181C1E" --- @@ -42,7 +42,6 @@ Runs a browser automation task using BrowserUse | `highlightElements` | boolean | No | Highlight interactive elements on the page \(default true\) | | `metadata` | json | No | Custom key-value metadata \(up to 10 pairs\) for tracking | | `model` | string | No | LLM model identifier \(e.g. browser-use-2.0\) | -| `apiKey` | string | Yes | API key for BrowserUse API | | `profile_id` | string | No | Browser profile ID for persistent sessions \(cookies, login state\) | #### Output diff --git a/apps/docs/content/docs/integrations/buffer.mdx b/apps/docs/content/docs/integrations/buffer.mdx index 573604c8971..95e00e956ac 100644 --- a/apps/docs/content/docs/integrations/buffer.mdx +++ b/apps/docs/content/docs/integrations/buffer.mdx @@ -2,7 +2,7 @@ title: Buffer description: Schedule and publish social media posts across connected channels integration: - type: "buffer" + type: "buffer_v2" color: "#FFFFFF" --- @@ -14,7 +14,7 @@ Use [Buffer](https://buffer.com/) to draft, queue, schedule, publish, edit, and ## Authentication -Create an API key under Buffer **Settings → API** and enter it in **API Key**. Sim sends it as a Bearer token to Buffer's GraphQL API. The key can reach the organizations and channels available to its account, subject to its permissions. +Create an API key under Buffer **Settings → API** and add it as a Buffer credential. Sim sends it as a Bearer token to Buffer's GraphQL API. The key can reach the organizations and channels available to its account, subject to its permissions. Enable the permissions your workflow needs: `account:read`, `posts:read`, `posts:write`, `ideas:read`, `ideas:write`, and `insights:read`. The block includes 13 operations; preview operations are excluded. @@ -47,7 +47,6 @@ Create a post in Buffer for a channel — add it to the queue, share it immediat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `channelId` | string | Yes | Channel to create the post for \(find it with the Get Channels operation\) | | `text` | string | No | Text content of the post \(required unless media is attached\) | | `mode` | string | Yes | How to share the post: addToQueue, shareNext, shareNow, or customScheduled \(requires dueAt\) | @@ -379,7 +378,6 @@ Edit an existing Buffer post — update its text, schedule, or media. Attaching | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `postId` | string | Yes | ID of the post to edit | | `text` | string | No | New text content of the post | | `mode` | string | No | Change the share mode to addToQueue, shareNext, shareNow, or customScheduled \(requires dueAt\). Omit to keep the current schedule | @@ -711,7 +709,6 @@ List posts in a Buffer organization, optionally filtered by channel and status ( | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `organizationId` | string | Yes | Buffer organization ID \(find it with the Get Account operation\) | | `channelIds` | string | No | Comma-separated channel IDs to filter by | | `status` | string | No | Comma-separated statuses to filter by: draft, needs_approval, scheduled, sending, sent, error | @@ -1338,7 +1335,6 @@ Get a single Buffer post by ID, including its status, schedule, and media | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `postId` | string | Yes | ID of the post to fetch | #### Output @@ -1654,7 +1650,6 @@ Delete a Buffer post by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `postId` | string | Yes | ID of the post to delete | #### Output @@ -1676,7 +1671,6 @@ List the social media channels connected to a Buffer organization, including the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `organizationId` | string | Yes | Buffer organization ID \(find it with the Get Account operation\) | | `filter` | json | No | Filter channels by locked status or product. Schema | @@ -1762,7 +1756,6 @@ Get one connected channel, including its posting schedule, limits and network se | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key with account:read permission | | `channelId` | string | Yes | Buffer channel ID | #### Output @@ -1847,7 +1840,6 @@ Aggregate published post metrics across an organization or selected channels ove | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key with insights:read permission | | `organizationId` | string | Yes | Buffer organization ID | | `startDateTime` | string | Yes | Start of aggregation window as an ISO 8601 timestamp | | `endDateTime` | string | Yes | End of aggregation window as an ISO 8601 timestamp | @@ -1879,7 +1871,6 @@ Check sent and scheduled post counts against each channel’s daily limit for a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key with posts:read permission | | `channelIds` | json | Yes | Channel ID array or comma-separated IDs; all channels must belong to the same organization | | `date` | string | No | ISO 8601 timestamp for the date to check; omit for today | @@ -1906,7 +1897,6 @@ Save a content idea to a Buffer organization for later drafting and scheduling | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `organizationId` | string | Yes | Buffer organization ID \(find it with the Get Account operation\) | | `text` | string | No | Text content of the idea | | `title` | string | No | Optional title for the idea | @@ -1965,7 +1955,6 @@ List content ideas saved in a Buffer organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `organizationId` | string | Yes | Buffer organization ID \(find it with the Get Account operation\) | | `limit` | number | No | Maximum number of ideas to return \(default 20\) | | `after` | string | No | Pagination cursor from a previous page \(pageInfo.endCursor\) | @@ -2060,7 +2049,6 @@ List idea groups (board columns) in a Buffer organization, including the group I | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `organizationId` | string | Yes | Buffer organization ID \(find it with the Get Account operation\) | #### Output @@ -2084,7 +2072,6 @@ Get the authenticated Buffer account, including its organizations and their IDs | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Buffer API key | | `organizationFilter` | json | No | Filter account organizations by organization ID. Schema | #### Output diff --git a/apps/docs/content/docs/integrations/calendly.mdx b/apps/docs/content/docs/integrations/calendly.mdx index 59b04c7b283..1b257c6091d 100644 --- a/apps/docs/content/docs/integrations/calendly.mdx +++ b/apps/docs/content/docs/integrations/calendly.mdx @@ -2,7 +2,7 @@ title: Calendly description: Manage Calendly scheduling and events integration: - type: "calendly" + type: "calendly_v2" color: "#FFFFFF" --- @@ -17,7 +17,7 @@ Webhook subscriptions require a paid plan and the permissions for their user or ## Use the block [#usage-instructions] -Manage event types, scheduled events, invitees, and webhooks. Trigger workflows based on Calendly webhook events (invitee scheduled, invitee canceled, routing form submitted). Requires Personal Access Token. +Manage event types, scheduled events, invitees, and webhooks. Trigger workflows based on Calendly webhook events (invitee scheduled, invitee canceled, routing form submitted). ## Actions @@ -31,7 +31,6 @@ Get information about the currently authenticated Calendly user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | #### Output @@ -61,7 +60,6 @@ Get information about a specific Calendly user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `userUuid` | string | Yes | User UUID. Format: UUID \(e.g., "abc123-def456"\), full URI \(e.g., "https://api.calendly.com/users/abc123-def456"), or the constant "me" for the authenticated user | #### Output @@ -95,7 +93,6 @@ Retrieve a list of all event types for a user or organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `user` | string | No | Return only event types that belong to this user. Format: URI \(e.g., "https://api.calendly.com/users/abc123-def456") | | `organization` | string | No | Return only event types that belong to this organization. Format: URI \(e.g., "https://api.calendly.com/organizations/abc123-def456") | | `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) | @@ -140,7 +137,6 @@ Get detailed information about a specific event type | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `eventTypeUuid` | string | Yes | Event type UUID. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456") | #### Output @@ -181,7 +177,6 @@ Retrieve bookable time slots for an event type within a date range | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `eventTypeUri` | string | Yes | Event type to check. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456") | | `startTime` | string | Yes | Start of the availability range. Cannot be in the past. Format: ISO 8601 \(e.g., "2024-01-01T00:00:00Z"\) | | `endTime` | string | Yes | End of the availability range. Must be in the future and no more than 31 days after the start. Format: ISO 8601 \(e.g., "2024-01-15T00:00:00Z"\) | @@ -208,7 +203,6 @@ Retrieve a list of scheduled events for a user or organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `user` | string | No | Return events that belong to this user. Either "user" or "organization" must be provided. Format: URI \(e.g., "https://api.calendly.com/users/abc123-def456") | | `organization` | string | No | Return events that belong to this organization. Either "user" or "organization" must be provided. Format: URI \(e.g., "https://api.calendly.com/organizations/abc123-def456") | | `invitee_email` | string | No | Return events where invitee has this email | @@ -259,7 +253,6 @@ Get detailed information about a specific scheduled event | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `eventUuid` | string | Yes | Scheduled event UUID. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/scheduled_events/abc123-def456") | #### Output @@ -304,7 +297,6 @@ Retrieve a list of invitees for a scheduled event | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `eventUuid` | string | Yes | Scheduled event UUID. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/scheduled_events/abc123-def456") | | `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) | | `email` | string | No | Filter invitees by email address | @@ -353,7 +345,6 @@ Retrieve a single invitee of a scheduled event, including their intake answers | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `eventUuid` | string | Yes | Scheduled event UUID. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/scheduled_events/abc123-def456") | | `inviteeUuid` | string | Yes | Invitee UUID. Format: UUID \(e.g., "abc123-def456"\) or full invitee URI \(e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456") | @@ -416,7 +407,6 @@ Book a meeting by creating an invitee on an event type at a chosen time. Require | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `eventTypeUri` | string | Yes | Event type to book. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456") | | `startTime` | string | Yes | Start time of the booking in UTC. Must be an available slot. Format: ISO 8601 \(e.g., "2024-01-15T09:00:00Z"\) | | `inviteeEmail` | string | Yes | Email address of the invitee being booked | @@ -463,7 +453,6 @@ Cancel a scheduled event | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `eventUuid` | string | Yes | Scheduled event UUID to cancel. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/scheduled_events/abc123-def456") | | `reason` | string | No | Reason for cancellation \(will be sent to invitees\) | @@ -489,7 +478,6 @@ Mark an invitee of a scheduled event as a no-show | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `inviteeUri` | string | Yes | Full invitee URI to mark as a no-show. Format: URI \(e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456") | #### Output @@ -513,7 +501,6 @@ Remove the no-show status from an invitee | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `noShowUuid` | string | Yes | No-show UUID to remove. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/invitee_no_shows/abc123-def456") | #### Output @@ -535,7 +522,6 @@ Create a single-use scheduling link for an event type | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `eventTypeUri` | string | Yes | Event type the link books. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456") | #### Output @@ -559,7 +545,6 @@ Retrieve a user's internal and external busy times within a date range, based on | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `user` | string | Yes | User whose busy times are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/users/abc123-def456") | | `startTime` | string | Yes | Start of the requested range. Cannot be in the past. Format: ISO 8601 \(e.g., "2024-01-01T00:00:00Z"\) | | `endTime` | string | Yes | End of the requested range. Must be after the start and no more than 7 days later. Format: ISO 8601 \(e.g., "2024-01-07T00:00:00Z"\) | @@ -589,7 +574,6 @@ Retrieve a user's availability schedules, working hours, and date overrides | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `user` | string | Yes | User whose schedules are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/users/abc123-def456") | #### Output @@ -622,7 +606,6 @@ Retrieve the members of an organization, including each member profile and role | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `organization` | string | No | Return memberships that belong to this organization. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/organizations/abc123-def456") | | `user` | string | No | Return memberships that belong to this user. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/users/abc123-def456") | | `email` | string | No | Filter memberships by member email address | @@ -671,7 +654,6 @@ Retrieve the routing forms of an organization, including their questions | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `organization` | string | Yes | Organization whose routing forms are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/organizations/abc123-def456") | | `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) | | `pageToken` | string | No | Page token for pagination. Format: opaque string from previous response next_page_token | @@ -713,7 +695,6 @@ Retrieve the submissions of a routing form, including answers and routing result | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Calendly Personal Access Token | | `formUri` | string | Yes | Routing form whose submissions are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/routing_forms/abc123-def456") | | `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) | | `pageToken` | string | No | Page token for pagination. Format: opaque string from previous response next_page_token | diff --git a/apps/docs/content/docs/integrations/checkr.mdx b/apps/docs/content/docs/integrations/checkr.mdx index 5f616b42873..8fb8013af06 100644 --- a/apps/docs/content/docs/integrations/checkr.mdx +++ b/apps/docs/content/docs/integrations/checkr.mdx @@ -2,7 +2,7 @@ title: Checkr description: Run and manage background checks with Checkr integration: - type: "checkr" + type: "checkr_v2" color: "#009BB5" --- @@ -33,7 +33,6 @@ Add a tag to a report, keeping its existing tags. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | | `tag` | string | Yes | Tag to add, e.g. To Review | @@ -56,7 +55,6 @@ Continue or skip the remaining screenings on a report that is paused awaiting a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the paused report | | `decision` | string | Yes | continue to run the remaining screenings, or skip_remaining to skip them and complete the report | @@ -78,7 +76,6 @@ Cancel an adverse action so the post-adverse action notice is not sent. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `adverseActionId` | string | Yes | ID of the adverse action to cancel | #### Output @@ -123,7 +120,6 @@ Cancel a continuous check to stop monitoring the candidate. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `continuousCheckId` | string | Yes | ID of the continuous check to cancel | #### Output @@ -153,7 +149,6 @@ Cancel a pending invitation so the candidate can no longer complete it. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `invitationId` | string | Yes | ID of the invitation to cancel | #### Output @@ -187,7 +182,6 @@ Cancel a subscription so no further recurring checks run. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `subscriptionId` | string | Yes | ID of the subscription to cancel | #### Output @@ -224,7 +218,6 @@ Complete a report now, canceling any pending or suspended screenings. Canceled s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report to complete | #### Output @@ -300,7 +293,6 @@ Start the adverse action process on a report by sending the pre-adverse action n | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report the adverse action is based on | | `adverseItemIds` | array | Yes | IDs of the adverse items to cite, from List Adverse Items | | `postNoticeScheduledAt` | string | No | ISO 8601 time to send the post-adverse action notice; defaults to 7 days after creation | @@ -349,7 +341,6 @@ Create a candidate to screen. Only email is required when the candidate will be | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `email` | string | Yes | Candidate's email address | | `firstName` | string | No | Candidate's first name | | `middleName` | string | No | Candidate's middle name \(required to order a report unless noMiddleName is true\) | @@ -413,7 +404,6 @@ Enroll a candidate in continuous criminal or motor vehicle monitoring so new rec | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | Yes | ID of the candidate to enroll | | `continuousCheckType` | string | Yes | Type of monitoring: criminal or mvr | | `mvrEnrollmentType` | string | No | For mvr checks, the enrollment type: commercial for commercial drivers or standard | @@ -447,7 +437,6 @@ Create a geo to group candidates by work location. A geo with the same name and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `name` | string | Yes | Name of the geo, e.g. San Francisco | | `state` | string | Yes | Two-letter US state for the geo, e.g. CA | | `city` | string | No | A major city within the state | @@ -477,7 +466,6 @@ Invite a candidate to complete a background check through the Checkr-hosted appl | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | Yes | ID of the candidate to invite | | `package` | string | Yes | Slug of the package to run, e.g. driver_pro | | `node` | string | No | custom_id of the account hierarchy node \(required for hierarchy-enabled accounts\) | @@ -515,7 +503,6 @@ Order a background check report for a candidate using a package. Use only when y | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | Yes | ID of the candidate to screen | | `package` | string | Yes | Slug of the package to run, e.g. driver_pro | | `node` | string | No | custom_id of the account hierarchy node \(required for hierarchy-enabled accounts\) | @@ -596,7 +583,6 @@ Schedule a recurring background check for a candidate, running a package every i | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | Yes | ID of the candidate to screen | | `package` | string | Yes | Slug of the package to run on each occurrence | | `startDate` | string | Yes | Date of the first check in YYYY-MM-DD format | @@ -639,7 +625,6 @@ Request removal of a candidate's personally identifiable information. Fails if P | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | Yes | ID of the candidate whose PII should be removed | | `deletionContactEmail` | string | Yes | Email address of the person requesting the PII removal | | `deletionContactFirstName` | string | No | First name of the person requesting the PII removal | @@ -687,7 +672,6 @@ Delete a geo. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `geoId` | string | Yes | ID of the geo to delete | #### Output @@ -709,7 +693,6 @@ Retrieve the authenticated Checkr account, including whether it is authorized to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | #### Output @@ -749,7 +732,6 @@ Retrieve an adverse action by ID, including its status and notice delivery state | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `adverseActionId` | string | Yes | ID of the adverse action | #### Output @@ -794,7 +776,6 @@ Retrieve a candidate by ID, including their report and geo IDs. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | Yes | ID of the candidate to retrieve | #### Output @@ -839,7 +820,6 @@ Retrieve a continuous check by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `continuousCheckId` | string | Yes | ID of the continuous check | #### Output @@ -869,7 +849,6 @@ Retrieve a report or candidate document by ID, including a download link valid f | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `documentId` | string | Yes | ID of the document | #### Output @@ -898,7 +877,6 @@ Retrieve a geo by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `geoId` | string | Yes | ID of the geo | #### Output @@ -926,7 +904,6 @@ Retrieve an invitation by ID, including its status and invitation URL. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `invitationId` | string | Yes | ID of the invitation to retrieve | | `includeDeleted` | boolean | No | Whether to return the invitation even if it was canceled | @@ -961,7 +938,6 @@ Retrieve an account hierarchy node by its custom ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `nodeCustomId` | string | Yes | Custom ID of the node | | `includePackages` | boolean | No | Include the package slugs available to the node | @@ -988,7 +964,6 @@ Retrieve a package by ID, including its slug, price, and screenings. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `packageId` | string | Yes | ID of the package | #### Output @@ -1020,7 +995,6 @@ Retrieve a program by ID, including its package and geo IDs. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `programId` | string | Yes | ID of the program | #### Output @@ -1047,7 +1021,6 @@ Retrieve a report by ID, including its status, result, adjudication, and the IDs | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report to retrieve | #### Output @@ -1123,7 +1096,6 @@ Retrieve the estimated completion date for a report. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | #### Output @@ -1145,7 +1117,6 @@ Retrieve checkpoint progress for an Enterprise Progressive report, including whi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the progressive report | #### Output @@ -1181,7 +1152,6 @@ List the tags on a report. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | #### Output @@ -1203,7 +1173,6 @@ Retrieve the results of one screening in a report, such as an SSN trace, county | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `screeningType` | string | Yes | Screening type, one of: ssn_trace, sex_offender_search, global_watchlist_search, national_criminal_search, county_criminal_search, state_criminal_search, federal_criminal_search, federal_district_criminal_search, federal_civil_search, federal_district_civil_search, motor_vehicle_report, drug_alcohol_clearinghouse_search, fmcsa_pre_employment_screening_program_search, education_verification, employment_verification, personal_reference_verification, professional_reference_verification, professional_license_verification, social_media_screening, facis_search, identity_data_evaluation, international_adverse_media_search, international_criminal_search, international_education_verification, international_employment_verification, international_global_watchlist_search, international_identity_document_validation, international_motor_vehicle_report | | `screeningId` | string | Yes | ID of the screening, taken from the matching report field | @@ -1239,7 +1208,6 @@ Retrieve a subscription by ID, including its schedule and next occurrence. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `subscriptionId` | string | Yes | ID of the subscription | #### Output @@ -1276,7 +1244,6 @@ Retrieve a verification by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `verificationId` | string | Yes | ID of the verification | #### Output @@ -1305,7 +1272,6 @@ List the adverse actions for a report, optionally filtered by context. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | | `context` | string | No | Only return adverse actions with this context | @@ -1354,7 +1320,6 @@ List the adverse items on a report that can be cited in an adverse action. The r | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | #### Output @@ -1379,7 +1344,6 @@ List the Assess results for a report, including the ruleset applied and which ru | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | #### Output @@ -1405,7 +1369,6 @@ List documents a candidate provided, such as driver licenses and consent forms. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | Yes | ID of the candidate | | `documentTypes` | array | No | Only return these document types, e.g. driver_license, consent | @@ -1436,7 +1399,6 @@ List candidates, optionally filtered by email, name, custom ID, adjudication, da | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `email` | string | No | Only return candidates with this email address | | `fullName` | string | No | Only return candidates with this full name | | `customId` | string | No | Only return candidates with this custom ID | @@ -1499,7 +1461,6 @@ List the continuous checks a candidate is enrolled in. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | Yes | ID of the candidate | #### Output @@ -1530,7 +1491,6 @@ List US counties for one or more states, with their FIPS codes. Use the county n | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `states` | string | No | Comma-separated state FIPS codes, e.g. 08,06 for Colorado and California. Leave empty for every US county | #### Output @@ -1554,7 +1514,6 @@ List geos (work locations used for compliance), optionally filtered by name or s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `name` | string | No | Only return geos with this name | | `state` | string | No | Only return geos in this two-letter state | | `page` | number | No | Page number to retrieve \(starts at 1\) | @@ -1588,7 +1547,6 @@ List invitations, optionally filtered by candidate or status. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | No | Only return invitations for this candidate ID | | `status` | string | No | Only return invitations with this status \(pending, completed, expired\) | | `page` | number | No | Page number to retrieve \(starts at 1\) | @@ -1628,7 +1586,6 @@ List the nodes in the account hierarchy. Hierarchy-enabled accounts need a node | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `includePackages` | boolean | No | Include the package slugs available to each node | | `orderBy` | string | No | Sort field: custom_id or created_at \(default created_at\) | | `order` | string | No | Sort direction: asc or desc \(default asc\) | @@ -1661,7 +1618,6 @@ List the background check packages on the account, with their slugs, prices, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `page` | number | No | Page number to retrieve \(starts at 1\) | | `perPage` | number | No | Number of records per page, from 0 to 100 \(default 25\) | @@ -1697,7 +1653,6 @@ List programs, which group packages and geos, optionally filtered by name. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `name` | string | No | Only return programs with this name | | `page` | number | No | Page number to retrieve \(starts at 1\) | | `perPage` | number | No | Number of records per page, from 0 to 100 \(default 25\) | @@ -1729,7 +1684,6 @@ List the address history found for a report. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | | `page` | number | No | Page number to retrieve \(starts at 1\) | | `perPage` | number | No | Number of records per page, from 0 to 100 \(default 25\) | @@ -1758,7 +1712,6 @@ List recurring check subscriptions, optionally filtered by candidate, status, or | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | No | Only return subscriptions for this candidate ID | | `status` | string | No | Only return subscriptions with this status \(active, inactive\) | | `createdAfter` | string | No | Only return subscriptions created after this date | @@ -1803,7 +1756,6 @@ List the users on the Checkr account and their roles. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `page` | number | No | Page number to retrieve \(starts at 1\) | | `perPage` | number | No | Number of records per page, from 0 to 100 \(default 25\) | @@ -1833,7 +1785,6 @@ List the verifications on a report, such as requests for the candidate to confir | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | #### Output @@ -1863,7 +1814,6 @@ Remove a tag from a report. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | | `tag` | string | Yes | Tag to remove | @@ -1886,7 +1836,6 @@ Replace all tags on a report with the given list. Pass [] to remove every tag. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report | | `tags` | array | Yes | Complete list of tags; an empty list removes every tag | @@ -1909,7 +1858,6 @@ Update a candidate. Once a report is ordered only empty fields can change, excep | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `candidateId` | string | Yes | ID of the candidate to update | | `email` | string | No | Candidate's email address | | `firstName` | string | No | Candidate's first name | @@ -1973,7 +1921,6 @@ Change the node or work locations of a continuous check. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `continuousCheckId` | string | Yes | ID of the continuous check to update | | `node` | string | No | custom_id of the account hierarchy node \(required for hierarchy-enabled accounts\) | | `workLocations` | array | No | Work locations \(required for hierarchy-enabled accounts\), e.g. \[\{"country":"US","state":"CA","city":"San Francisco"\}\] | @@ -2005,7 +1952,6 @@ Add a city to a geo. A geo’s city can only be set once. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `geoId` | string | Yes | ID of the geo | | `city` | string | Yes | City to set on the geo | @@ -2034,7 +1980,6 @@ Upgrade a report to a different package or mark it as engaged. Provide a package | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `reportId` | string | Yes | ID of the report to update | | `package` | string | No | Slug of the package to upgrade the report to | | `adjudication` | string | No | Adjudication to set; Checkr accepts only "engaged" | @@ -2112,7 +2057,6 @@ Change the package, schedule, node, or work locations of a subscription. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Checkr production secret API key | | `subscriptionId` | string | Yes | ID of the subscription to update | | `package` | string | No | Slug of the package to run on each occurrence | | `startDate` | string | No | Date of the first check in YYYY-MM-DD format | diff --git a/apps/docs/content/docs/integrations/circleback.mdx b/apps/docs/content/docs/integrations/circleback.mdx index 8a1362227f2..ddc9656c0a3 100644 --- a/apps/docs/content/docs/integrations/circleback.mdx +++ b/apps/docs/content/docs/integrations/circleback.mdx @@ -2,7 +2,7 @@ title: Circleback description: Read Circleback meetings, notes, transcripts, and action items integration: - type: "circleback" + type: "circleback_v2" color: "linear-gradient(180deg, #E0F7FA 0%, #FFFFFF 100%)" --- @@ -33,7 +33,6 @@ Lists meetings from Circleback with optional ownership, status, tag, and attende | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `ownership` | string | No | Which meetings to return: All, Mine, or Shared. Defaults to Mine | | `statuses` | string | No | Comma-separated meeting statuses to filter by | | `tagIds` | string | No | Comma-separated tag IDs to filter by | @@ -91,7 +90,6 @@ Gets a Circleback meeting by ID, including notes, attendees, action items, insig | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `meetingId` | string | Yes | The unique identifier of the meeting | #### Output @@ -142,7 +140,6 @@ Searches Circleback meetings by name and content, with optional filters for spec | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `searchTerm` | string | No | The text to search for across meeting names and content | | `meetingIds` | string | No | Comma-separated meeting IDs to restrict the search to | | `tagIds` | string | No | Comma-separated tag IDs to restrict the search to | @@ -200,7 +197,6 @@ Gets the full transcript for a Circleback meeting. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `meetingId` | string | Yes | The unique identifier of the meeting | #### Output @@ -224,7 +220,6 @@ Updates the name, notes, or private notes of a Circleback meeting. The API retur | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `meetingId` | string | Yes | The unique identifier of the meeting | | `name` | string | No | The new name of the meeting | | `notes` | string | No | The new meeting notes in Markdown | @@ -252,7 +247,6 @@ Deletes a Circleback meeting. Only the owner of the meeting can delete it. Retur | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `meetingId` | string | Yes | The unique identifier of the meeting | #### Output @@ -303,7 +297,6 @@ Lists action items across the authenticated user meetings, filtered by assignee, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `assigneeType` | string | No | The assignee scope to filter by: Me, NotMe, Profile, MyWorkspace, OutsideMyWorkspace, Unassigned, or Anyone. Defaults to Me | | `assigneeProfileId` | string | No | Profile ID of the assignee to filter by | | `assigneeTeamId` | string | No | Team ID of the assignee to filter by | @@ -344,7 +337,6 @@ Updates the title, description, status, or assignee of a Circleback action item. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `actionItemId` | string | Yes | The unique identifier of the action item | | `title` | string | No | The new title of the action item | | `description` | string | No | The new detailed description of the action item | @@ -380,7 +372,6 @@ Deletes a Circleback action item. Returns the deleted action item. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `actionItemId` | string | Yes | The unique identifier of the action item | #### Output @@ -412,7 +403,6 @@ Lists upcoming calendar events from the authenticated user connected calendars, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `startTimeLookbackHours` | string | No | How many hours in the past to include calendar meetings from | | `startTimeLookaheadHours` | string | No | How many hours in the future to include calendar meetings from | | `sortDirection` | string | No | The direction calendar meetings are sorted in by start time: ascending or descending. Defaults to ascending | @@ -462,7 +452,6 @@ Lists the companies whose people attend the authenticated user meetings, with op | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `tagIds` | string | No | Comma-separated tag IDs. Filters companies to those with meetings so tagged | | `cursor` | string | No | Pagination cursor from a previous response | @@ -490,7 +479,6 @@ Gets a company by its domain from Circleback, including its people and external | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `domain` | string | Yes | The website domain of the company to fetch, such as example.com | #### Output @@ -525,7 +513,6 @@ Lists the people who attend the authenticated user meetings, ordered by most rec | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `domains` | string | No | Comma-separated company domains to filter people by | | `tagIds` | string | No | Comma-separated tag IDs. Filters people to attendees of meetings so tagged | | `limit` | string | No | The maximum number of people to return | @@ -558,7 +545,6 @@ Gets a person by their profile ID from Circleback, including their profile detai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `profileId` | string | Yes | The unique identifier of the person profile | #### Output @@ -589,7 +575,6 @@ Lists every tag available to the authenticated Circleback user. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | #### Output @@ -612,7 +597,6 @@ Creates a new tag in Circleback. If a tag with the same name already exists, a c | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `tagName` | string | Yes | The display name of the tag | | `tagDescription` | string | No | A description of the tag | @@ -636,7 +620,6 @@ Updates the name or description of a Circleback tag. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `tagId` | string | Yes | The unique identifier of the tag | | `tagName` | string | No | The new display name of the tag | | `tagDescription` | string | No | The new description of the tag | @@ -661,7 +644,6 @@ Permanently deletes a Circleback tag and removes it from every meeting it was ap | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `tagId` | string | Yes | The unique identifier of the tag | #### Output @@ -684,7 +666,6 @@ Applies an existing Circleback tag to one or more meetings and returns the updat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `tagId` | string | Yes | The unique identifier of the tag to apply | | `meetingIds` | string | No | Comma-separated IDs of the meetings to tag | @@ -737,7 +718,6 @@ Removes a Circleback tag from one or more meetings and returns the updated meeti | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Circleback API key | | `tagId` | string | Yes | The unique identifier of the tag to remove | | `meetingIds` | string | No | Comma-separated IDs of the meetings to remove the tag from | diff --git a/apps/docs/content/docs/integrations/clerk.mdx b/apps/docs/content/docs/integrations/clerk.mdx index 3334f6d625b..ccf78446121 100644 --- a/apps/docs/content/docs/integrations/clerk.mdx +++ b/apps/docs/content/docs/integrations/clerk.mdx @@ -2,7 +2,7 @@ title: Clerk description: Manage users, organizations, and sessions in Clerk integration: - type: "clerk" + type: "clerk_v2" color: "#131316" --- @@ -31,7 +31,6 @@ List all users in your Clerk application with optional filtering and pagination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `limit` | number | No | Number of results per page \(e.g., 10, 50, 100; range: 1-500, default: 10\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 10, 20\) | | `orderBy` | string | No | Sort field with optional +/- prefix for direction \(default: -created_at\) | @@ -86,7 +85,6 @@ Retrieve a single user by their ID from Clerk | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | Yes | The ID of the user to retrieve \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | #### Output @@ -140,7 +138,6 @@ Create a new user in your Clerk application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `emailAddress` | string | No | Email addresses for the user \(comma-separated for multiple\) | | `phoneNumber` | string | No | Phone numbers for the user \(comma-separated for multiple\) | | `username` | string | No | Username for the user \(must be unique\) | @@ -191,7 +188,6 @@ Update an existing user in your Clerk application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | Yes | The ID of the user to update \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `firstName` | string | No | First name of the user | | `lastName` | string | No | Last name of the user | @@ -244,7 +240,6 @@ Delete a user from your Clerk application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | Yes | The ID of the user to delete \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | #### Output @@ -268,7 +263,6 @@ Ban a user, preventing them from signing in | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | Yes | The ID of the user to ban \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | #### Output @@ -297,7 +291,6 @@ Remove a ban from a user, allowing them to sign in again | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | Yes | The ID of the user to unban \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | #### Output @@ -326,7 +319,6 @@ Lock a user account, blocking sign-in attempts | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | Yes | The ID of the user to lock \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | #### Output @@ -355,7 +347,6 @@ Unlock a previously locked user account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | Yes | The ID of the user to unlock \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | #### Output @@ -384,7 +375,6 @@ Retrieve a user's OAuth access token for a connected external provider (e.g. Goo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | Yes | The ID of the user \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `provider` | string | Yes | OAuth provider slug, e.g. google, github, microsoft, discord \(without the oauth_ prefix\) | @@ -414,7 +404,6 @@ List all organizations in your Clerk application with optional filtering | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `limit` | number | No | Number of results per page \(e.g., 10, 50, 100; range: 1-500, default: 10\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 10, 20\) | | `includeMembersCount` | boolean | No | Include member count for each organization | @@ -454,7 +443,6 @@ Retrieve a single organization by ID or slug from Clerk | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `organizationId` | string | Yes | The ID or slug of the organization to retrieve \(e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC or my-org-slug\) | #### Output @@ -488,7 +476,6 @@ Create a new organization in your Clerk application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `name` | string | Yes | Name of the organization | | `createdBy` | string | Yes | User ID of the creator who will become admin \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `slug` | string | No | Slug identifier for the organization | @@ -527,7 +514,6 @@ Update an existing organization in your Clerk application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `organizationId` | string | Yes | The ID of the organization to update \(e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `name` | string | No | Name of the organization | | `slug` | string | No | Slug identifier for the organization | @@ -565,7 +551,6 @@ Delete an organization from your Clerk application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `organizationId` | string | Yes | The ID of the organization to delete \(e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | #### Output @@ -589,7 +574,6 @@ List members of a Clerk organization with optional filtering and pagination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `organizationId` | string | Yes | The ID of the organization \(e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `limit` | number | No | Number of results per page \(e.g., 10, 50, 100; range: 1-500, default: 10\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 10, 20\) | @@ -631,7 +615,6 @@ Add a user as a member of a Clerk organization with a given role | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `organizationId` | string | Yes | The ID of the organization \(e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `userId` | string | Yes | ID of the user to add as a member | | `role` | string | Yes | Role to assign, e.g. org:admin or org:member | @@ -669,7 +652,6 @@ Change a member's role within a Clerk organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `organizationId` | string | Yes | The ID of the organization \(e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `userId` | string | Yes | ID of the member whose role is being changed | | `role` | string | Yes | New role to assign, e.g. org:admin or org:member | @@ -707,7 +689,6 @@ Remove a member from a Clerk organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `organizationId` | string | Yes | The ID of the organization \(e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `userId` | string | Yes | ID of the member to remove | @@ -744,7 +725,6 @@ Invite a user by email to join a Clerk organization with a given role | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `organizationId` | string | Yes | The ID of the organization \(e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `emailAddress` | string | Yes | Email address of the user to invite | | `role` | string | Yes | Role to assign on acceptance, e.g. org:admin or org:member | @@ -788,7 +768,6 @@ List pending and past invitations for a Clerk organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `organizationId` | string | Yes | The ID of the organization \(e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | | `status` | string | No | Filter by status: pending, accepted, revoked, or expired | | `emailAddress` | string | No | Filter by invited email address | @@ -831,7 +810,6 @@ List sessions for a user or client in your Clerk application | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | No | User ID to list sessions for \(e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC; required if clientId not provided\) | | `clientId` | string | No | Client ID to list sessions for \(required if userId not provided\) | | `status` | string | No | Filter by session status \(abandoned, active, ended, expired, pending, removed, replaced, revoked\) | @@ -868,7 +846,6 @@ Retrieve a single session by ID from Clerk | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `sessionId` | string | Yes | The ID of the session to retrieve \(e.g., sess_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | #### Output @@ -899,7 +876,6 @@ Revoke a session to immediately invalidate it | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `sessionId` | string | Yes | The ID of the session to revoke \(e.g., sess_2NNEqL2nrIRdJ194ndJqAHwEfxC\) | #### Output @@ -930,7 +906,6 @@ List email/phone/web3-wallet identifiers on your Clerk instance allowlist | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `limit` | number | No | Number of results per page \(e.g., 10, 50, 100; range: 1-500, default: 10\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 10, 20\) | @@ -960,7 +935,6 @@ Add an email, phone number, or web3 wallet to your Clerk instance allowlist | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `identifier` | string | Yes | Email address, phone number, or web3 wallet to allow \(wildcards like *@example.com supported for email\) | | `notify` | boolean | No | Whether to notify the identifier owner by email \(default false\) | @@ -988,7 +962,6 @@ Remove an identifier from your Clerk instance allowlist | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `identifierId` | string | Yes | ID of the allowlist identifier to delete | #### Output @@ -1012,7 +985,6 @@ List email/phone/web3-wallet identifiers on your Clerk instance blocklist | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | #### Output @@ -1039,7 +1011,6 @@ Add an email, phone number, or web3 wallet to your Clerk instance blocklist to p | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `identifier` | string | Yes | Email address, phone number, or web3 wallet to block | #### Output @@ -1065,7 +1036,6 @@ Remove an identifier from your Clerk instance blocklist | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `identifierId` | string | Yes | ID of the blocklist identifier to delete | #### Output @@ -1089,7 +1059,6 @@ List custom JWT templates configured on your Clerk instance | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | #### Output @@ -1120,7 +1089,6 @@ Retrieve a single custom JWT template by ID from Clerk | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `templateId` | string | Yes | ID of the JWT template to retrieve | #### Output @@ -1150,7 +1118,6 @@ Create an actor token to impersonate a user (God Mode / act-as-user), e.g. for s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `userId` | string | Yes | ID of the user to impersonate | | `actor` | json | Yes | Actor JSON object identifying who is impersonating, must include a "sub" field, e.g. \{"sub": "user_support_agent_id"\} | | `expiresInSeconds` | number | No | Seconds until the token expires \(default 3600\) | @@ -1182,7 +1149,6 @@ Revoke an actor token before it is used or expires | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `secretKey` | string | Yes | The Clerk Secret Key for API authentication | | `actorTokenId` | string | Yes | ID of the actor token to revoke | #### Output diff --git a/apps/docs/content/docs/integrations/cloudflare.mdx b/apps/docs/content/docs/integrations/cloudflare.mdx index 8c5cbd865ee..1e854a397ad 100644 --- a/apps/docs/content/docs/integrations/cloudflare.mdx +++ b/apps/docs/content/docs/integrations/cloudflare.mdx @@ -2,7 +2,7 @@ title: Cloudflare description: Manage DNS, WAF, Zero Trust access, and edge infrastructure integration: - type: "cloudflare" + type: "cloudflare_v2" color: "#F5F6FA" --- @@ -77,7 +77,6 @@ Creates a Cloudflare Access (Zero Trust) application that puts an identity check | `policies` | string | No | JSON array of policies to attach. Entries may be reusable policy IDs or inline policy objects, e.g. \["<POLICY_ID>"\] | | `saasApp` | string | No | JSON SaaS configuration, required for the saas type and rejected on every other type. SAML, e.g. \{ "auth_type": "saml", "consumer_service_url": "https://example.com/acs", "sp_entity_id": "https://example.com" \}; OIDC, e.g. \{ "auth_type": "oidc", "client_id": "...", "redirect_uris": \[ "https://example.com/callback" \] \} | | `targetCriteria` | string | No | JSON array of infrastructure target criteria, required for the infrastructure and rdp types and rejected on every other type, e.g. \[\{"port":22,"protocol":"SSH","target_attributes":\{"hostname":\["production"\]\}\}\] | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -476,7 +475,6 @@ Creates a new Access group. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | -| `apiKey` | string | Yes | Cloudflare API token | | `exclude` | json | No | JSON array. Rules evaluated with a NOT logical operator. To match a policy, a user cannot meet any of the Exclude rules. Fields: group, any_valid_service_token, auth_context, auth_method, azureAD, certificate, common_name, geo, device_posture, email_domain, email_list, email, everyone, external_evaluation, github-organization, gsuite, login_method, ip_list, ip, okta, saml, oidc, service_token, linked_app_token, user_risk_score, cloudflare_account_member. | | `include` | json | Yes | JSON array. Rules evaluated with an OR logical operator. A user needs to meet only one of the Include rules. Fields: group, any_valid_service_token, auth_context, auth_method, azureAD, certificate, common_name, geo, device_posture, email_domain, email_list, email, everyone, external_evaluation, github-organization, gsuite, login_method, ip_list, ip, okta, saml, oidc, service_token, linked_app_token, user_risk_score, cloudflare_account_member. | | `is_default` | boolean | No | Whether this is the default group | @@ -772,7 +770,6 @@ Creates a Cloudflare Access (Zero Trust) policy on an application, deciding who | `isolationRequired` | boolean | No | Whether the session must run in a remote isolated browser | | `purposeJustificationRequired` | boolean | No | Whether users must state a reason for access | | `purposeJustificationPrompt` | string | No | Prompt shown when a justification is required | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -1013,7 +1010,6 @@ Creates a Cloudflare Access (Zero Trust) service token so a machine can authenti | `accountId` | string | Yes | The Cloudflare account ID. Service tokens are account-scoped | | `name` | string | Yes | Name of the service token | | `duration` | string | No | How long the token stays valid before it expires, e.g. 8760h. Defaults to Cloudflare's standard lifetime | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -1055,7 +1051,6 @@ Creates a new DNS record for a zone. | `priority` | number | No | Record priority. Cloudflare accepts this top-level field for MX and URI records only; use the data object for structured records such as SRV \(priority, weight, port, target\) | | `comment` | string | No | Comment for the DNS record | | `tags` | string | No | Comma-separated tags for the DNS record | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -1143,7 +1138,6 @@ Creates an R2 object storage bucket in an account. The location hint and jurisdi | `locationHint` | string | No | Region hint for where the bucket should live: apac, eeur, enam, weur, wnam, or oc. Cannot be changed after creation | | `storageClass` | string | No | Default storage class for objects: Standard or InfrequentAccess | | `jurisdiction` | string | No | Data-residency jurisdiction to create the bucket in: default, eu, or fedramp. Cannot be changed after creation | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -1188,7 +1182,6 @@ Creates a rate limiting rule in the http_ratelimit phase entry point ruleset of | `requestsToOrigin` | boolean | No | When true, only requests that reach the origin are counted | | `description` | string | No | Human-readable description of the rule | | `enabled` | boolean | No | Whether the rule is enabled | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -1453,7 +1446,6 @@ Creates a zone ruleset for a phase, optionally seeded with its first rules. Use | `kind` | string | No | Ruleset kind: zone or custom. Use zone to create a phase entry point ruleset and custom for a ruleset an execute rule deploys. Defaults to zone. "root" is the account-level phase entry point and "managed" is Cloudflare-owned, so neither can be created on this zone-scoped endpoint | | `description` | string | No | Description of the ruleset | | `rules` | json | No | JSON array of rules to seed the ruleset with, in evaluation order. Each rule takes action, expression, and optionally description, enabled, action_parameters, and ratelimit | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -1725,7 +1717,6 @@ Adds a rule to a zone ruleset. Use "Get Phase Entry Point Ruleset" first to find | `ref` | string | No | Reference tag that stays stable across rule updates | | `actionParameters` | string | No | JSON object of action-specific parameters. For an "execute" rule this carries the managed ruleset id and any overrides, e.g. \{"id":"<MANAGED_RULESET_ID>","overrides":\{"action":"log"\}\} | | `position` | string | No | JSON object placing the rule within the ruleset. Exactly one of \{"before":"<RULE_ID>"\}, \{"after":"<RULE_ID>"\}, or \{"index":<1-based position>\} | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -1984,7 +1975,6 @@ Creates a remotely or locally managed Cloudflare Tunnel in an account. After cre | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | -| `apiKey` | string | Yes | Cloudflare API token | | `config_src` | string | No | Indicates if this is a locally or remotely configured tunnel. If `local`, manage the tunnel using a YAML file on the origin machine. If `cloudflare`, manage the tunnel on the Zero Trust dashboard. Allowed values: local, cloudflare. | | `name` | string | Yes | A user-friendly name for a tunnel. | | `tunnel_secret` | string | No | Sets the password required to run a locally-managed tunnel. Must be at least 32 bytes and encoded as a base64 string. | @@ -2028,7 +2018,6 @@ Creates a route that maps a URL pattern to a Worker. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | Zone id | -| `apiKey` | string | Yes | Cloudflare API token | | `pattern` | string | Yes | Pattern to match incoming requests against. \[Learn more\]\(https://developers.cloudflare.com/workers/configuration/routing/routes/#matching-behavior). | | `script` | string | No | Name of the script to run if the route matches. | @@ -2055,7 +2044,6 @@ Adds a new zone (domain) to the Cloudflare account. | `name` | string | Yes | The domain name to add \(e.g., "example.com"\) | | `accountId` | string | Yes | The Cloudflare account ID | | `type` | string | No | Zone type: "full" \(Cloudflare manages DNS\), "partial" \(CNAME setup\), "secondary" \(secondary DNS\), or "internal" \(internal DNS zone\) | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -2124,7 +2112,6 @@ Permanently deletes a Cloudflare Access (Zero Trust) application and every polic | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | The Cloudflare account ID. Access applications are account-scoped | | `appId` | string | Yes | The Access application ID to delete permanently | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -2146,7 +2133,6 @@ Deletes an Access group. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `groupId` | string | Yes | Group id | -| `apiKey` | string | Yes | Cloudflare API token | #### Output @@ -2169,7 +2155,6 @@ Permanently deletes a Cloudflare Access (Zero Trust) policy from an application. | `accountId` | string | Yes | The Cloudflare account ID. Access applications are account-scoped | | `appId` | string | Yes | The Access application ID that owns the policy | | `policyId` | string | Yes | The Access policy ID to delete permanently | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -2191,7 +2176,6 @@ Deletes a DNS record from a zone. | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | The zone ID containing the DNS record | | `recordId` | string | Yes | The DNS record ID to delete | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -2214,7 +2198,6 @@ Permanently deletes an R2 object storage bucket. Cloudflare only deletes an empt | `accountId` | string | Yes | The Cloudflare account ID. R2 buckets are account-scoped | | `bucketName` | string | Yes | The name of the bucket to delete permanently | | `jurisdiction` | string | No | Data-residency jurisdiction the bucket lives in: default, eu, or fedramp | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -2236,7 +2219,6 @@ Delete the CORS policy for a bucket. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `bucketName` | string | Yes | Bucket name | -| `apiKey` | string | Yes | Cloudflare API token | | `jurisdiction` | string | No | Jurisdiction where objects in this bucket are guaranteed to be stored. | #### Output @@ -2259,7 +2241,6 @@ Deletes all versions of an existing zone ruleset. | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | Zone id | | `rulesetId` | string | Yes | Ruleset id | -| `apiKey` | string | Yes | Cloudflare API token | | `dry_run` | boolean | No | Validates the request without persisting changes when set to `true`. Responses that normally return 200 return `result: null`; endpoints that normally return 204 continue to return 204. | #### Output @@ -2284,7 +2265,6 @@ Permanently deletes a rule from a zone ruleset. This takes effect immediately on | `zoneId` | string | Yes | The zone ID that owns the ruleset | | `rulesetId` | string | Yes | The ruleset ID containing the rule | | `ruleId` | string | Yes | The rule ID to delete permanently | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -2544,7 +2524,6 @@ Permanently deletes a Cloudflare Tunnel from an account. The tunnel must have no | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `tunnelId` | string | Yes | Tunnel id | -| `apiKey` | string | Yes | Cloudflare API token | #### Output @@ -2586,7 +2565,6 @@ Deletes a Worker route. | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | Zone id | | `routeId` | string | Yes | Route id | -| `apiKey` | string | Yes | Cloudflare API token | #### Output @@ -2608,7 +2586,6 @@ Delete your worker. This call has no response body on a successful delete. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `scriptName` | string | Yes | Script name | -| `apiKey` | string | Yes | Cloudflare API token | | `force` | boolean | No | If true, delete the Worker even when other Workers still reference it. Service bindings in those Workers may be left broken. Durable Object namespaces implemented by the deleted Worker are deleted even if other Workers reference them. | #### Output @@ -2630,7 +2607,6 @@ Deletes a zone (domain) from the Cloudflare account. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | The zone ID to delete | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -2658,7 +2634,6 @@ Gets DNS analytics report for a zone including query counts and trends. | `filters` | string | No | Filters to apply to the data \(e.g., "queryType==A"\) | | `sort` | string | No | Sort order for the result set. Fields must be included in metrics or dimensions \(e.g., "+queryCount" or "-responseTimeAvg"\) | | `limit` | number | No | Maximum number of results to return | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -2702,7 +2677,6 @@ Reads a single Cloudflare Access (Zero Trust) application, including its attache | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | The Cloudflare account ID. Access applications are account-scoped | | `appId` | string | Yes | The Access application ID \(or audience tag\) to read | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -3102,7 +3076,6 @@ Fetches a single Access group. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `groupId` | string | Yes | Group id | -| `apiKey` | string | Yes | Cloudflare API token | #### Output @@ -3380,7 +3353,6 @@ Fetches a single Access policy configured for an application. Returns both exclu | `accountId` | string | Yes | Account id | | `appId` | string | Yes | App id | | `policyId` | string | Yes | Policy id | -| `apiKey` | string | Yes | Cloudflare API token | #### Output @@ -3617,7 +3589,6 @@ Fetches a single service token. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `serviceTokenId` | string | Yes | Service token id | -| `apiKey` | string | Yes | Cloudflare API token | #### Output @@ -3650,7 +3621,6 @@ Retrieves details for a specific DNS record in the zone. | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | Zone id | | `recordId` | string | Yes | Dns record id | -| `apiKey` | string | Yes | Cloudflare API token | | `include_shadow_metadata` | boolean | No | Whether to include shadow metadata in the `meta` field of each record in the response. See \[Shadowed records\]\(https://developers.cloudflare.com/dns/manage-dns-records/reference/shadowed-records). | #### Output @@ -3737,7 +3707,6 @@ Reads the metadata of a single R2 object storage bucket. Requires an API token w | `accountId` | string | Yes | The Cloudflare account ID. R2 buckets are account-scoped | | `bucketName` | string | Yes | The name of the bucket to read | | `jurisdiction` | string | No | Data-residency jurisdiction the bucket lives in: default, eu, or fedramp | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -3763,7 +3732,6 @@ Get the CORS policy for a bucket. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `bucketName` | string | Yes | Bucket name | -| `apiKey` | string | Yes | Cloudflare API token | | `jurisdiction` | string | No | Jurisdiction where objects in this bucket are guaranteed to be stored. | #### Output @@ -3793,7 +3761,6 @@ Get object lifecycle rules for a bucket. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `bucketName` | string | Yes | Bucket name | -| `apiKey` | string | Yes | Cloudflare API token | | `jurisdiction` | string | No | Jurisdiction where objects in this bucket are guaranteed to be stored. | #### Output @@ -3835,7 +3802,6 @@ Reads a single zone ruleset including every rule it contains, in evaluation orde | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | The zone ID that owns the ruleset | | `rulesetId` | string | Yes | The ruleset ID to read | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -4095,7 +4061,6 @@ Reads the entry point ruleset for a phase on a zone, including all of its rules. | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | The zone ID to read the phase entry point for | | `phase` | string | Yes | The ruleset phase, e.g. http_request_firewall_custom, http_request_firewall_managed, http_ratelimit, http_request_transform, http_request_dynamic_redirect | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -4355,7 +4320,6 @@ Reads a single Cloudflare Tunnel (cloudflared), including its health status and | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | The Cloudflare account ID. Tunnels are account-scoped | | `tunnelId` | string | Yes | The tunnel ID to read | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -4397,7 +4361,6 @@ Reads the configuration of a remotely-managed Cloudflare Tunnel — its ingress | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | The Cloudflare account ID. Tunnels are account-scoped | | `tunnelId` | string | Yes | The tunnel ID to read the configuration for | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -4468,7 +4431,6 @@ Gets the connector token used to run a remotely managed Cloudflare Tunnel with c | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Cloudflare account ID | | `tunnelId` | string | Yes | Cloudflare Tunnel ID | -| `apiKey` | string | Yes | Cloudflare API token | #### Output @@ -4490,7 +4452,6 @@ Returns information about a Worker route, including URL pattern and Worker. | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | Zone id | | `routeId` | string | Yes | Route id | -| `apiKey` | string | Yes | Cloudflare API token | #### Output @@ -4514,7 +4475,6 @@ Reads the deployment settings of a single Workers script — bindings, compatibi | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | The Cloudflare account ID. Workers scripts are account-scoped | | `scriptName` | string | Yes | The name of the Workers script to read settings for | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -4685,7 +4645,6 @@ Gets details for a specific zone (domain) by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | The zone ID to retrieve details for | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -4754,7 +4713,6 @@ Reads zone settings such as SSL mode, minimum TLS version, security level, and c | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | The zone ID to get settings for | | `settingIds` | string | No | Comma-separated setting IDs to read, e.g. "ssl,min_tls_version,security_level". Leave blank to read the default set \(ssl, always_use_https, min_tls_version, tls_1_3, security_level, cache_level, browser_cache_ttl, development_mode, rocket_loader, email_obfuscation, hotlink_protection, ip_geolocation, http2, http3, websockets\). At most 40 settings per call. | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -4791,7 +4749,6 @@ Lists the Cloudflare Access (Zero Trust) applications protecting an account. Req | `exact` | boolean | No | Whether the name and domain filters must match exactly | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of applications per page | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -5206,7 +5163,6 @@ Lists the reusable Cloudflare Access (Zero Trust) groups in an account. Groups b | `search` | string | No | Free-text search across groups | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of groups per page | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -5497,7 +5453,6 @@ Lists the identity providers configured for Cloudflare Access (Zero Trust) in an | `page` | number | No | Page number of results. | | `per_page` | number | No | Number of results per page. | | `accountId` | string | Yes | The Cloudflare account ID. Identity providers are account-scoped | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -5591,7 +5546,6 @@ Lists the Cloudflare Access (Zero Trust) policies attached to an application, in | `appId` | string | Yes | The Access application ID whose policies should be listed | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of policies per page | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -5843,7 +5797,6 @@ Lists the Cloudflare Access (Zero Trust) service tokens in an account, which let | `search` | string | No | Free-text search across service tokens | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of service tokens per page | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -5891,7 +5844,6 @@ Lists SSL/TLS certificate packs for a zone. | `page` | number | No | Page number of paginated results \(default: 1\) | | `per_page` | number | No | Number of certificate packs per page \(default: 20, min: 5, max: 50\) | | `deploy` | string | No | Filter by deployment environment: "staging" or "production" | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -5998,7 +5950,6 @@ Lists DNS records for a specific zone. | `tag` | string | No | Filter by tag name, or an exact name:value pair \(e.g., team:DNS\) | | `tag_match` | string | No | Tag filter match logic: any or all. Only affects results when combined with multiple tag filter conditions; has no effect with the single exact-match Tag Filter above. | | `commentFilter` | string | No | Filter records by comment content \(substring match\) | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6094,7 +6045,6 @@ Lists the WAF managed rulesets deployed on a zone together with the overrides ap | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | The zone ID to read managed ruleset deployments and overrides for | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6130,7 +6080,6 @@ Lists the R2 object storage buckets in an account. Requires an API token with Ac | `direction` | string | No | Sort direction by bucket name: asc or desc | | `per_page` | number | No | Number of buckets per page | | `jurisdiction` | string | No | Data-residency jurisdiction to list within: default, eu, or fedramp | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6168,7 +6117,6 @@ Lists the rate limiting rules on a zone by reading the http_ratelimit phase entr | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | The zone ID to list rate limiting rules for | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6429,7 +6377,6 @@ Lists every ruleset defined on a zone across all phases (WAF custom rules, manag | `zoneId` | string | Yes | The zone ID to list rulesets for | | `per_page` | number | No | Number of rulesets to return per page | | `cursor` | string | No | Cursor for the next page, taken from the cursor output of a previous call. This endpoint paginates by cursor, not by page number | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6480,7 +6427,6 @@ Lists the Cloudflare Tunnels (cloudflared) in an account, with their health stat | `was_inactive_at` | string | No | Return tunnels that were inactive at this RFC 3339 timestamp | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of tunnels per page | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6533,7 +6479,6 @@ Lists the Workers routes on a zone, showing which URL patterns are handled by wh | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | The zone ID to list Workers routes for. Routes are zone-scoped, not account-scoped | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6569,7 +6514,6 @@ Lists the Workers scripts deployed in an account. Requires an API token with Acc | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | The Cloudflare account ID. Workers scripts are account-scoped | | `tags` | string | No | Filter scripts by tag. Cloudflare expects a comma-separated list of tag:allowed pairs where allowed is yes or no, e.g. team:core:yes,deprecated:no | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6671,7 +6615,6 @@ Lists one page of zones (domains) in the Cloudflare account. | `order` | string | No | Sort field \(name, status, account.id, account.name, plan.id\) | | `direction` | string | No | Sort direction \(asc, desc\) | | `match` | string | No | Match logic for filters \(any, all\). Default: all | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6756,7 +6699,6 @@ Purges cached content for a zone. Can purge everything or specific files/tags/ho | `tags` | string | No | Comma-separated list of cache tags to purge | | `hosts` | string | No | Comma-separated list of hostnames to purge | | `prefixes` | string | No | Comma-separated list of URL prefixes to purge | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6778,7 +6720,6 @@ Refreshes the expiration of a service token. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `serviceTokenId` | string | Yes | Service token id | -| `apiKey` | string | Yes | Cloudflare API token | #### Output @@ -6811,7 +6752,6 @@ Permanently deletes a Cloudflare Access (Zero Trust) service token, revoking it. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | The Cloudflare account ID. Service tokens are account-scoped | | `serviceTokenId` | string | Yes | The service token ID to revoke permanently | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -6844,7 +6784,6 @@ Generates a new Client Secret. The previous secret expires immediately unless pr | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `serviceTokenId` | string | Yes | Service token id | -| `apiKey` | string | Yes | Cloudflare API token | | `previous_client_secret_expires_at` | string | No | The expiration of the previous `client_secret`. If not provided, it defaults to the current timestamp in order to immediately expire the previous secret. If the service token was reported as leaked, this must not be in the future; the leaked secret is discarded immediately. | #### Output @@ -6917,7 +6856,6 @@ Updates a Cloudflare Access (Zero Trust) application. Cloudflare does not docume | `saasApp` | string | No | JSON SaaS configuration, required for the saas type and rejected on every other type. SAML, e.g. \{ "auth_type": "saml", "consumer_service_url": "https://example.com/acs", "sp_entity_id": "https://example.com" \}; OIDC, e.g. \{ "auth_type": "oidc", "client_id": "...", "redirect_uris": \[ "https://example.com/callback" \] \} | | `targetCriteria` | string | No | JSON array of infrastructure target criteria, required for the infrastructure and rdp types and rejected on every other type, e.g. \[\{"port":22,"protocol":"SSH","target_attributes":\{"hostname":\["production"\]\}\}\] | | `policies` | string | No | JSON array of policies to attach. Entries may be reusable policy IDs or inline policy objects | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -7317,7 +7255,6 @@ Updates a configured Access group. Replaces the configuration; resend all values | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `groupId` | string | Yes | Group id | -| `apiKey` | string | Yes | Cloudflare API token | | `exclude` | json | No | JSON array. Rules evaluated with a NOT logical operator. To match a policy, a user cannot meet any of the Exclude rules. Fields: group, any_valid_service_token, auth_context, auth_method, azureAD, certificate, common_name, geo, device_posture, email_domain, email_list, email, everyone, external_evaluation, github-organization, gsuite, login_method, ip_list, ip, okta, saml, oidc, service_token, linked_app_token, user_risk_score, cloudflare_account_member. | | `include` | json | Yes | JSON array. Rules evaluated with an OR logical operator. A user needs to meet only one of the Include rules. Fields: group, any_valid_service_token, auth_context, auth_method, azureAD, certificate, common_name, geo, device_posture, email_domain, email_list, email, everyone, external_evaluation, github-organization, gsuite, login_method, ip_list, ip, okta, saml, oidc, service_token, linked_app_token, user_risk_score, cloudflare_account_member. | | `is_default` | boolean | No | Whether this is the default group | @@ -7614,7 +7551,6 @@ Updates a Cloudflare Access (Zero Trust) policy on an application. Cloudflare do | `isolationRequired` | boolean | No | Whether the session must run in a remote isolated browser | | `purposeJustificationRequired` | boolean | No | Whether users must state a reason for access | | `purposeJustificationPrompt` | string | No | Prompt shown when a justification is required | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -7851,7 +7787,6 @@ Updates a configured service token. Replaces the configuration; resend all value | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `serviceTokenId` | string | Yes | Service token id | -| `apiKey` | string | Yes | Cloudflare API token | | `client_secret_version` | number | No | A version number identifying the current `client_secret` associated with the service token. Incrementing it triggers a rotation; the previous secret will still be accepted until the time indicated by `previous_client_secret_expires_at`. If the service token was reported as leaked, `previous_client_secret_expires_at` is not required, may not be in the future, and the previous secret is discarded immediately. | | `duration` | string | No | The duration for how long the service token will be valid. Must be in the format `300ms` or `2h45m`, or the special value `forever` for non-expiring tokens. Valid time units are: ns, us \(or µs\), ms, s, m, h. The default is 1 year in hours \(8760h\). | | `enabled` | boolean | No | Whether the service token is enabled. A disabled service token cannot be used to authenticate; both its current and previous `client_secret` stop being accepted, but the token itself is preserved and can be re-enabled. A service token reported as leaked is disabled automatically and cannot be re-enabled until its secret is rotated; attempting to do so returns a 409. Defaults to enabled when omitted on create. | @@ -7902,7 +7837,6 @@ Updates an existing DNS record for a zone. | `priority` | number | No | Record priority. Cloudflare accepts this top-level field for MX and URI records only; use the data object for structured records such as SRV \(priority, weight, port, target\) | | `comment` | string | No | Comment for the DNS record | | `tags` | string | No | Comma-separated tags for the DNS record | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -7987,7 +7921,6 @@ Replaces the Cross-Origin Resource Sharing (CORS) rules for an R2 bucket. Rules | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `bucketName` | string | Yes | Bucket name | -| `apiKey` | string | Yes | Cloudflare API token | | `rules` | json | No | JSON array. rules Fields: allowed, exposeHeaders, id, maxAgeSeconds. | | `jurisdiction` | string | No | Jurisdiction where objects in this bucket are guaranteed to be stored. | @@ -8011,7 +7944,6 @@ Replaces the object lifecycle rules for an R2 bucket. Rules match object-key pre | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `bucketName` | string | Yes | Bucket name | -| `apiKey` | string | Yes | Cloudflare API token | | `rules` | string | Yes | Complete JSON array of lifecycle rules. Read the current rules first and include every rule to retain. Use \[\] only to remove all lifecycle rules. Fields: abortMultipartUploadsTransition, conditions, deleteObjectsTransition, enabled, id, storageClassTransitions. | | `jurisdiction` | string | No | Jurisdiction where objects in this bucket are guaranteed to be stored. | @@ -8054,7 +7986,6 @@ Updates a rate limiting rule in the http_ratelimit phase entry point ruleset of | `ref` | string | No | Reference tag that stays stable across rule updates. Because the update replaces the rule, omitting it resets the tag to the rule ID and breaks anything matching on the old value | | `actionParameters` | string | No | JSON object of action-specific parameters for the mitigation action, e.g. \{"response":\{"status_code":429,"content":"\{\"error\":\"rate limited\"\}","content_type":"application/json"\}\} for a custom block response. Because the update replaces the rule, omitting it resets action_parameters to \{\} and the rule falls back to Cloudflare's default block page | | `logging` | string | No | JSON logging configuration to preserve, e.g. \{"enabled":true\}. Omitting it on a rule that had logging configured resets it to the default | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -8314,7 +8245,6 @@ Updates the description and rules of a zone ruleset, creating a new version. Rea | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | Zone id | | `rulesetId` | string | Yes | Ruleset id | -| `apiKey` | string | Yes | Cloudflare API token | | `description` | string | No | An informative description of the ruleset. | | `rules` | json | No | JSON array replacing the complete rule list. Read "Get Ruleset" first and include every rule to retain; omitted rules are removed, and \[\] removes all rules. Fields: action, action_parameters, categories, description, enabled, exposed_credential_check, expression, id, last_updated, logging, ratelimit, ref, version. | | `dry_run` | boolean | No | Validates the request without persisting changes when set to `true`. Responses that normally return 200 return `result: null`; endpoints that normally return 204 continue to return 204. | @@ -8589,7 +8519,6 @@ Updates a rule in a zone ruleset. Cloudflare replaces the rule definition rather | `actionParameters` | string | No | JSON object of action-specific parameters, e.g. \{"id":"<MANAGED_RULESET_ID>","overrides":\{"rules":\[\{"id":"<RULE_ID>","action":"log","enabled":true,"score_threshold":40\}\]\}\}. Required on an execute rule and must be sent on every update: the endpoint replaces the rule, so omitting it resets action_parameters to \{\} — which unbinds the managed ruleset the rule deploys and every override under it | | `ratelimit` | string | No | JSON rate limiting configuration to preserve on a rule in the http_ratelimit phase, e.g. \{"characteristics":\["cf.colo.id","ip.src"\],"period":60,"requests_per_period":100\}. Because the update replaces the rule, omitting this on a rate limiting rule stops it rate limiting | | `logging` | string | No | JSON logging configuration to preserve, e.g. \{"enabled":true\}. Omitting it on a rule that had logging configured resets it to the default | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output @@ -8849,7 +8778,6 @@ Updates the name or secret of an existing Cloudflare Tunnel. | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `tunnelId` | string | Yes | Tunnel id | -| `apiKey` | string | Yes | Cloudflare API token | | `name` | string | No | A user-friendly name for a tunnel. | | `tunnel_secret` | string | No | Sets the password required to run a locally-managed tunnel. Must be at least 32 bytes and encoded as a base64 string. | @@ -8893,7 +8821,6 @@ Replaces the configuration for a remotely managed Cloudflare Tunnel, including i | --------- | ---- | -------- | ----------- | | `accountId` | string | Yes | Account id | | `tunnelId` | string | Yes | Tunnel id | -| `apiKey` | string | Yes | Cloudflare API token | | `config` | json | No | JSON object. The tunnel configuration and ingress rules. Fields: ingress, originRequest, warp-routing. | #### Output @@ -8965,7 +8892,6 @@ Replaces the URL pattern or Worker associated with a Worker route. Replaces the | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | Zone id | | `routeId` | string | Yes | Route id | -| `apiKey` | string | Yes | Cloudflare API token | | `pattern` | string | Yes | Pattern to match incoming requests against. \[Learn more\]\(https://developers.cloudflare.com/workers/configuration/routing/routes/#matching-behavior). | | `script` | string | No | Name of the script to run if the route matches. | @@ -8990,7 +8916,6 @@ Edits a zone. Only one zone property can be changed at a time. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `zoneId` | string | Yes | Zone id | -| `apiKey` | string | Yes | Cloudflare API token | | `paused` | boolean | No | Indicates whether the zone is only using Cloudflare DNS services. A true value means the zone will not receive security or performance benefits. | | `plan` | json | No | JSON object. \(Deprecated\) Please use the `/zones/\{zone_id\}/subscription` API to update a zone's plan. Changing this value will create/cancel associated subscriptions. To view available plans for this zone, see Zone Plans. Fields: id. | | `type` | string | No | A full zone implies that DNS is hosted with Cloudflare. A partial zone is typically a partner-hosted zone or a CNAME setup. This parameter is only available to Enterprise customers or if it has been explicitly enabled on a zone. Allowed values: full, partial, secondary, internal. | @@ -9065,7 +8990,6 @@ Updates a specific zone setting such as SSL mode, security level, cache level, o | `zoneId` | string | Yes | The zone ID to update settings for | | `settingId` | string | Yes | Setting to update \(e.g., "ssl", "security_level", "cache_level", "always_use_https", "browser_cache_ttl", "http3", "min_tls_version", "ciphers"\) | | `value` | string | No | Required except for ssl_recommender. New value as a string, or a JSON string for complex values \(e.g., "full" for SSL, "medium" for security_level, "aggressive" for cache_level, \["ECDHE-RSA-AES128-GCM-SHA256"\] for ciphers\) | -| `apiKey` | string | Yes | Cloudflare API Token | #### Output diff --git a/apps/docs/content/docs/integrations/cloudformation.mdx b/apps/docs/content/docs/integrations/cloudformation.mdx index c540935d5bb..87814805465 100644 --- a/apps/docs/content/docs/integrations/cloudformation.mdx +++ b/apps/docs/content/docs/integrations/cloudformation.mdx @@ -2,7 +2,7 @@ title: CloudFormation description: Manage and inspect AWS CloudFormation stacks, resources, and drift integration: - type: "cloudformation" + type: "cloudformation_v2" color: "linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)" --- @@ -10,12 +10,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [AWS CloudFormation](https://aws.amazon.com/cloudformation/) to manage template-defined resource stacks. Create or update stacks, preview changes with change sets, inspect resources and events, and check drift or template validity from a workflow. +Use [AWS CloudFormation](https://aws.amazon.com/cloudformation/) to manage template-defined resource stacks. Create or update stacks, preview changes with change sets, inspect resources and events, and check drift or template validity from a workflow. Connect with a saved [AWS credential](/integrations/aws-credentials). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Create, update, and delete stacks, preview changes with change sets, describe stacks, list resources, detect drift, view stack events, and retrieve or validate templates. Requires AWS access key and secret access key. +Create, update, and delete stacks, preview changes with change sets, describe stacks, list resources, detect drift, view stack events, and retrieve or validate templates. ## Actions @@ -30,8 +30,6 @@ List and describe CloudFormation stacks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | No | Stack name or ID to describe \(omit to list all stacks\) | #### Output @@ -53,8 +51,6 @@ Create a new CloudFormation stack from a template | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Name for the new stack \(must be unique in the Region\) | | `templateBody` | string | Yes | The CloudFormation template body \(JSON or YAML\) | | `parameters` | json | No | Template input parameters \(e.g., \[\{"parameterKey": "InstanceType", "parameterValue": "t3.micro"\}\]\) | @@ -82,8 +78,6 @@ Update an existing CloudFormation stack with a new or previous template | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Name or ID of the stack to update | | `templateBody` | string | No | The new CloudFormation template body \(JSON or YAML\). Required unless usePreviousTemplate is true | | `usePreviousTemplate` | boolean | No | Reuse the template currently associated with the stack instead of providing templateBody | @@ -110,8 +104,6 @@ Delete a CloudFormation stack and its resources | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Name or ID of the stack to delete | | `retainResources` | string | No | Comma-separated logical resource IDs to retain instead of deleting \(only applies to stacks in DELETE_FAILED state\) | @@ -134,8 +126,6 @@ Cancel an in-progress stack update and roll back to the last known stable state | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Name or ID of the stack whose update should be cancelled | #### Output @@ -157,8 +147,6 @@ Preview the changes a stack create or update would make before applying them | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Name of the stack to create or update \(new name for CREATE type, existing name for UPDATE\) | | `changeSetName` | string | Yes | Name for the new change set | | `templateBody` | string | No | The CloudFormation template body \(JSON or YAML\). Required unless usePreviousTemplate is true | @@ -188,8 +176,6 @@ View the resource changes a change set would make and its execution status | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `changeSetName` | string | Yes | Name or ARN of the change set to describe | | `stackName` | string | No | Name or ID of the stack the change set belongs to \(required if changeSetName is not an ARN\) | @@ -222,8 +208,6 @@ Apply a previously created and reviewed change set to its stack | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `changeSetName` | string | Yes | Name or ARN of the change set to execute | | `stackName` | string | No | Name or ID of the stack the change set belongs to \(required if changeSetName is not an ARN\) | @@ -246,8 +230,6 @@ List all resources in a CloudFormation stack | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Stack name or ID | #### Output @@ -269,8 +251,6 @@ Initiate drift detection on a CloudFormation stack | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Stack name or ID to detect drift on | #### Output @@ -292,8 +272,6 @@ Check the status of a stack drift detection operation | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackDriftDetectionId` | string | Yes | The drift detection ID returned by Detect Stack Drift | #### Output @@ -321,8 +299,6 @@ Get the event history for a CloudFormation stack | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Stack name or ID | | `limit` | number | No | Maximum number of events to return \(default: 50\) | @@ -345,8 +321,6 @@ Retrieve the template body for a CloudFormation stack | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Stack name or ID | | `templateStage` | string | No | Which template version to retrieve: Processed \(default, with transforms applied\) or Original \(as submitted\) | @@ -370,8 +344,6 @@ Get a summary of a template or deployed stack: resource types, required capabili | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `templateBody` | string | No | The CloudFormation template body \(JSON or YAML\). Required if stackName is not provided | | `stackName` | string | No | Name or ID of a deployed stack to summarize instead of a template body | @@ -400,8 +372,6 @@ Validate a CloudFormation template for syntax and structural correctness | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `templateBody` | string | Yes | The CloudFormation template body \(JSON or YAML\) | #### Output diff --git a/apps/docs/content/docs/integrations/cloudtrail.mdx b/apps/docs/content/docs/integrations/cloudtrail.mdx index fde1ff1aea4..3cdabcb5a1a 100644 --- a/apps/docs/content/docs/integrations/cloudtrail.mdx +++ b/apps/docs/content/docs/integrations/cloudtrail.mdx @@ -2,7 +2,7 @@ title: CloudTrail description: Audit who did what in AWS with CloudTrail event history and Lake queries integration: - type: "cloudtrail" + type: "cloudtrail_v2" color: "linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -[AWS CloudTrail](https://aws.amazon.com/cloudtrail/) records API activity in AWS accounts. Use it to look up events, inspect trail configuration and logging status, or run CloudTrail Lake queries. +[AWS CloudTrail](https://aws.amazon.com/cloudtrail/) records API activity in AWS accounts. Use it to look up events, inspect trail configuration and logging status, or run CloudTrail Lake queries. Connect with a saved [AWS credential](/integrations/aws-credentials). Captured event types depend on configuration. Trails and event data stores log management events by default; data events, network activity events, and Insights events are captured only where you configure selectors for them. Read a trail's selectors before you treat its history as complete. @@ -60,7 +60,7 @@ CloudTrail Lake closed to new customers on May 31, 2026. Existing customers can ## Use the block [#usage-instructions] -Integrate AWS CloudTrail into workflows. Look up the last 90 days of management and Insights events by user, event name, resource, or access key; inspect trail configuration, logging status, and event selectors; and run SQL queries against CloudTrail Lake event data stores. This block never changes trail or event data store configuration, and never starts or stops logging. Starting and cancelling a Lake query are the only actions that are not reads, and AWS bills Lake queries on the data they scan. Requires AWS access key and secret access key. +Integrate AWS CloudTrail into workflows. Look up the last 90 days of management and Insights events by user, event name, resource, or access key; inspect trail configuration, logging status, and event selectors; and run SQL queries against CloudTrail Lake event data stores. This block never changes trail or event data store configuration, and never starts or stops logging. Starting and cancelling a Lake query are the only actions that are not reads, and AWS bills Lake queries on the data they scan. ## Actions @@ -75,8 +75,6 @@ Look up AWS CloudTrail management or Insights events from the last 90 days in a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `attributeKey` | string | No | Lookup attribute to filter on: AccessKeyId, EventId, EventName, EventSource, ReadOnly, ResourceName, ResourceType, or Username. Must be paired with attributeValue | | `attributeValue` | string | No | Value the lookup attribute must equal. Must be paired with attributeKey | | `startTime` | string | No | Only return events at or after this ISO 8601 timestamp | @@ -115,8 +113,6 @@ Retrieve the full configuration of one or more CloudTrail trails in the current | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `trailNameList` | string | No | Comma-separated trail names or ARNs. Leave empty to describe every trail in the Region. Trails in another Region must be given as ARNs | | `includeShadowTrails` | boolean | No | Include shadow trails \(replications of trails created in another Region, and organization trails in member accounts\). Defaults to true | @@ -155,8 +151,6 @@ Retrieve the settings of a single CloudTrail trail by name or ARN | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Trail name, or the trail ARN for a trail in another Region | #### Output @@ -193,8 +187,6 @@ Check whether a CloudTrail trail is logging and surface its most recent delivery | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Trail name, or the trail ARN. An organization trail read from a member account must be given as an ARN | #### Output @@ -226,8 +218,6 @@ List the ARN, name, and home Region of every CloudTrail trail visible to the acc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `nextToken` | string | No | Pagination token from a previous list request | #### Output @@ -253,8 +243,6 @@ Read which management, data, and network activity events a CloudTrail trail is c | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `trailName` | string | Yes | Trail name or trail ARN | #### Output @@ -284,8 +272,6 @@ Read which CloudTrail Insights types are enabled on a trail or event data store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `trailName` | string | No | Trail name or trail ARN. Cannot be combined with eventDataStore | | `eventDataStore` | string | No | Event data store ARN, or the ID suffix of that ARN. Cannot be combined with trailName | @@ -313,8 +299,6 @@ Start a CloudTrail Lake SQL query over an event data store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryStatement` | string | No | SQL query to run, up to 10,000 characters. The event data store ID is named in the FROM clause. Supply this or queryAlias, not both | | `queryAlias` | string | No | Alias of a query template used by CloudTrail Lake dashboards. Supply this or queryStatement, not both | | `queryParameters` | string | No | Comma-separated parameter values for the query alias, up to 10 values | @@ -341,8 +325,6 @@ Check the status, run time, and scan statistics of a CloudTrail Lake query | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryId` | string | No | ID of the query returned by Start Query. Supply this or queryAlias, not both | | `queryAlias` | string | No | Query template alias; returns the last run for that alias. Supply this or queryId, not both | | `refreshId` | string | No | Dashboard refresh ID, used together with queryAlias | @@ -379,8 +361,6 @@ Fetch a page of result rows from a finished CloudTrail Lake query | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryId` | string | Yes | ID of the query returned by Start Query | | `maxQueryResults` | number | No | Maximum rows to return on a single page, 1 to 1000 | | `nextToken` | string | No | Pagination token from a previous results request | @@ -411,8 +391,6 @@ Cancel a running CloudTrail Lake query | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryId` | string | Yes | ID of the query returned by Start Query | | `eventDataStoreOwnerAccountId` | string | No | Account ID of the event data store owner, for a shared event data store | @@ -437,8 +415,6 @@ List the CloudTrail Lake event data stores in the account for the current Region | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `maxResults` | number | No | Maximum event data stores to return on a single page, 1 to 1000 | | `nextToken` | string | No | Pagination token from a previous list request | @@ -472,8 +448,6 @@ Retrieve the configuration of a single CloudTrail Lake event data store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `eventDataStore` | string | Yes | Event data store ARN, or the ID suffix of that ARN | #### Output @@ -513,8 +487,6 @@ List the tags on CloudTrail trails, event data stores, dashboards, or channels | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `resourceIdList` | string | Yes | Comma-separated CloudTrail resource ARNs, up to 20 | | `nextToken` | string | No | Reserved for future use by AWS | diff --git a/apps/docs/content/docs/integrations/cloudwatch.mdx b/apps/docs/content/docs/integrations/cloudwatch.mdx index 380c6e46ef2..96f9907f869 100644 --- a/apps/docs/content/docs/integrations/cloudwatch.mdx +++ b/apps/docs/content/docs/integrations/cloudwatch.mdx @@ -2,7 +2,7 @@ title: CloudWatch description: Query and monitor AWS CloudWatch logs, metrics, and alarms integration: - type: "cloudwatch" + type: "cloudwatch_v2" color: "linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)" --- @@ -10,14 +10,14 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [AWS CloudWatch](https://aws.amazon.com/cloudwatch/) to query logs, retrieve log events, inspect log groups and streams, read metrics and alarms, and publish custom metric data. Connect with an AWS access key ID and secret access key. +Use [AWS CloudWatch](https://aws.amazon.com/cloudwatch/) to query logs, retrieve log events, inspect log groups and streams, read metrics and alarms, and publish custom metric data. Connect with a saved [AWS credential](/integrations/aws-credentials). Choose **Query Logs (Insights)** for a query across log groups, or **Get Log Events** for one stream. Metric statistics use a time range and period; alarm actions report the current state. Pass results to later steps for incident investigation or threshold-based notifications. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. Requires AWS access key and secret access key. +Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. ## Actions @@ -32,8 +32,6 @@ Run a CloudWatch Log Insights query against one or more log groups | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `logGroupNames` | array | Yes | Log group names to query | | `queryString` | string | Yes | CloudWatch Log Insights query string | | `startTime` | number | Yes | Start time as Unix epoch seconds | @@ -64,8 +62,6 @@ Search log events across all streams in a log group by filter pattern and time r | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `logGroupName` | string | Yes | CloudWatch log group name to search | | `filterPattern` | string | No | CloudWatch Logs filter pattern \(e.g., "ERROR", "?ERROR ?Exception"\). Matches all events if omitted. | | `logStreamNamePrefix` | string | No | Only search log streams whose name starts with this prefix | @@ -97,8 +93,6 @@ List available CloudWatch log groups | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `prefix` | string | No | Filter log groups by name prefix | | `limit` | number | No | Maximum number of log groups to return | @@ -126,8 +120,6 @@ Retrieve log events from a specific CloudWatch log stream | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `logGroupName` | string | Yes | CloudWatch log group name | | `logStreamName` | string | Yes | CloudWatch log stream name | | `startTime` | number | No | Start time as Unix epoch seconds | @@ -156,8 +148,6 @@ List log streams within a CloudWatch log group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `logGroupName` | string | Yes | CloudWatch log group name | | `prefix` | string | No | Filter log streams by name prefix | | `limit` | number | No | Maximum number of log streams to return | @@ -186,8 +176,6 @@ Set (or clear, for never-expire) the retention period for a CloudWatch log group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `logGroupName` | string | Yes | CloudWatch log group name | | `retentionInDays` | number | No | Days to retain log events \(one of 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653\). Omit to make events never expire. | @@ -212,8 +200,6 @@ List available CloudWatch metrics | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `namespace` | string | No | Filter by namespace \(e.g., AWS/EC2, AWS/Lambda\) | | `metricName` | string | No | Filter by metric name | | `recentlyActive` | boolean | No | Only show metrics active in the last 3 hours | @@ -241,8 +227,6 @@ Get statistics for a CloudWatch metric over a time range | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `namespace` | string | Yes | Metric namespace \(e.g., AWS/EC2, AWS/Lambda\) | | `metricName` | string | Yes | Metric name \(e.g., CPUUtilization, Invocations\) | | `startTime` | number | Yes | Start time as Unix epoch seconds | @@ -278,8 +262,6 @@ Publish a custom metric data point to CloudWatch | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `namespace` | string | Yes | Metric namespace \(e.g., Custom/MyApp\) | | `metricName` | string | Yes | Name of the metric | | `value` | number | Yes | Metric value to publish | @@ -310,8 +292,6 @@ List and filter CloudWatch alarms | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `alarmNamePrefix` | string | No | Filter alarms by name prefix | | `stateValue` | string | No | Filter by alarm state \(OK, ALARM, INSUFFICIENT_DATA\) | | `alarmType` | string | No | Filter by alarm type \(MetricAlarm, CompositeAlarm\) | @@ -344,8 +324,6 @@ Retrieve state-change and configuration history for CloudWatch alarms | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `alarmName` | string | No | Name of a specific alarm to retrieve history for. Omit for all alarms. | | `historyItemType` | string | No | Filter by history item type \(ConfigurationUpdate, StateUpdate, Action, AlarmContributorStateUpdate, AlarmContributorAction\) | | `startDate` | number | No | Start of the history window as Unix epoch seconds | @@ -377,8 +355,6 @@ Create a CloudWatch alarm mute rule that suppresses alarms for a fixed duration | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `muteRuleName` | string | Yes | Unique name for the mute rule \(used later to unmute\) | | `alarmNames` | array | Yes | Names of the CloudWatch alarms this mute rule targets | | `durationValue` | number | Yes | How long the mute lasts \(paired with durationUnit\) | @@ -409,8 +385,6 @@ Delete a CloudWatch alarm mute rule, restoring alarm notifications | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `muteRuleName` | string | Yes | Name of the mute rule to delete | #### Output diff --git a/apps/docs/content/docs/integrations/codepipeline.mdx b/apps/docs/content/docs/integrations/codepipeline.mdx index ccb81d700f0..63e4bddc64d 100644 --- a/apps/docs/content/docs/integrations/codepipeline.mdx +++ b/apps/docs/content/docs/integrations/codepipeline.mdx @@ -2,7 +2,7 @@ title: CodePipeline description: Run, monitor, and approve AWS CodePipeline pipelines integration: - type: "codepipeline" + type: "codepipeline_v2" color: "linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)" --- @@ -10,12 +10,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [AWS CodePipeline](https://aws.amazon.com/codepipeline/) to inspect release pipelines and execution history, start or stop runs, retry stages, and approve or reject manual approval actions. Stage transitions can be disabled or re-enabled to control pipeline flow. +Use [AWS CodePipeline](https://aws.amazon.com/codepipeline/) to inspect release pipelines and execution history, start or stop runs, retry stages, and approve or reject manual approval actions. Stage transitions can be disabled or re-enabled to control pipeline flow. Connect with a saved [AWS credential](/integrations/aws-credentials). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Start, stop, and monitor pipeline executions, retry failed stages, and approve or reject manual approval actions. Requires AWS access key and secret access key. +Start, stop, and monitor pipeline executions, retry failed stages, and approve or reject manual approval actions. ## Actions @@ -30,8 +30,6 @@ List all CodePipeline pipelines in an AWS account and region | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `maxResults` | number | No | Maximum number of pipelines to return \(1-1000\) | | `nextToken` | string | No | Pagination token from a previous call | @@ -61,8 +59,6 @@ Get the structure of a CodePipeline pipeline, including its stages, actions, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | | `version` | number | No | Pipeline version to retrieve \(defaults to the current version\) | @@ -101,8 +97,6 @@ Get the current state of a CodePipeline pipeline, including stage and action sta | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | #### Output @@ -133,8 +127,6 @@ Get details of a CodePipeline execution, including status, trigger, source revis | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | | `pipelineExecutionId` | string | Yes | ID of the pipeline execution | @@ -174,8 +166,6 @@ List recent executions of a CodePipeline pipeline with status and source revisio | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | | `maxResults` | number | No | Maximum number of executions to return \(1-100, default 100\) | | `nextToken` | string | No | Pagination token from a previous call | @@ -213,8 +203,6 @@ List action-level execution history for a CodePipeline pipeline, including per-a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | | `pipelineExecutionId` | string | No | Only return action executions for this pipeline execution | | `maxResults` | number | No | Maximum number of action executions to return \(1-100, default 100\) | @@ -254,8 +242,6 @@ Start a CodePipeline pipeline execution, optionally overriding pipeline variable | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline to start | | `clientRequestToken` | string | No | Idempotency token to identify a unique execution request | | `variables` | json | No | Pipeline variable overrides as an array of \{ name, value \} objects | @@ -279,8 +265,6 @@ Stop a CodePipeline pipeline execution, either finishing in-progress actions or | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | | `pipelineExecutionId` | string | Yes | ID of the pipeline execution to stop | | `abandon` | boolean | No | Abandon in-progress actions instead of letting them finish \(default false\) | @@ -305,8 +289,6 @@ Retry the failed actions (or all actions) of a failed CodePipeline stage | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | | `stageName` | string | Yes | Name of the failed stage to retry | | `pipelineExecutionId` | string | Yes | ID of the pipeline execution in the failed stage | @@ -331,8 +313,6 @@ Approve or reject a pending CodePipeline manual approval action. The approval to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | | `stageName` | string | Yes | Name of the stage containing the approval action | | `actionName` | string | Yes | Name of the manual approval action | @@ -360,8 +340,6 @@ Prevent artifacts from transitioning into or out of a CodePipeline stage, freezi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | | `stageName` | string | Yes | Name of the stage to disable the transition for | | `transitionType` | string | Yes | Inbound to block artifacts entering the stage, Outbound to block artifacts leaving it | @@ -388,8 +366,6 @@ Re-enable artifacts transitioning into or out of a CodePipeline stage after it w | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `pipelineName` | string | Yes | Name of the pipeline | | `stageName` | string | Yes | Name of the stage to enable the transition for | | `transitionType` | string | Yes | Inbound to allow artifacts entering the stage, Outbound to allow artifacts leaving it | diff --git a/apps/docs/content/docs/integrations/convex.mdx b/apps/docs/content/docs/integrations/convex.mdx index 52179097d79..bc7b74720ca 100644 --- a/apps/docs/content/docs/integrations/convex.mdx +++ b/apps/docs/content/docs/integrations/convex.mdx @@ -2,7 +2,7 @@ title: Convex description: Use Convex database integration: - type: "convex" + type: "convex_v2" color: "#FFFFFF" --- @@ -33,8 +33,6 @@ Run a Convex query function and return its result | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `deploymentUrl` | string | Yes | Convex deployment URL \(e.g., https://your-deployment.convex.cloud) | -| `deployKey` | string | Yes | Convex deploy key from the dashboard Settings page | | `functionPath` | string | Yes | Path to the query function \(e.g., messages:list or folder/file:myQuery\) | | `args` | json | No | Named arguments to pass to the function as a JSON object | @@ -57,8 +55,6 @@ Run a Convex mutation function to write data and return its result | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `deploymentUrl` | string | Yes | Convex deployment URL \(e.g., https://your-deployment.convex.cloud) | -| `deployKey` | string | Yes | Convex deploy key from the dashboard Settings page | | `functionPath` | string | Yes | Path to the mutation function \(e.g., messages:send or folder/file:myMutation\) | | `args` | json | No | Named arguments to pass to the function as a JSON object | @@ -81,8 +77,6 @@ Run a Convex action function and return its result | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `deploymentUrl` | string | Yes | Convex deployment URL \(e.g., https://your-deployment.convex.cloud) | -| `deployKey` | string | Yes | Convex deploy key from the dashboard Settings page | | `functionPath` | string | Yes | Path to the action function \(e.g., emails:send or folder/file:myAction\) | | `args` | json | No | Named arguments to pass to the function as a JSON object | @@ -105,8 +99,6 @@ Run any Convex function (query, mutation, or action) by path without specifying | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `deploymentUrl` | string | Yes | Convex deployment URL \(e.g., https://your-deployment.convex.cloud) | -| `deployKey` | string | Yes | Convex deploy key from the dashboard Settings page | | `functionPath` | string | Yes | Path to the function \(e.g., messages:list or folder/file:myFunction\) | | `args` | json | No | Named arguments to pass to the function as a JSON object | @@ -129,8 +121,6 @@ List all tables in a Convex deployment along with their JSON schemas. Requires s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `deploymentUrl` | string | Yes | Convex deployment URL \(e.g., https://your-deployment.convex.cloud) | -| `deployKey` | string | Yes | Convex deploy key from the dashboard Settings page | #### Output @@ -151,8 +141,6 @@ List documents from a Convex table via a paginated snapshot. Pass the returned s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `deploymentUrl` | string | Yes | Convex deployment URL \(e.g., https://your-deployment.convex.cloud) | -| `deployKey` | string | Yes | Convex deploy key from the dashboard Settings page | | `tableName` | string | No | Table to list documents from. Omit to list documents from all tables. | | `snapshot` | string | No | Snapshot timestamp from a previous page. Omit on the first request to start a new snapshot. | | `pageCursor` | string | No | Page cursor from a previous page of the same snapshot. Omit on the first request. | @@ -178,8 +166,6 @@ List documents that changed after a snapshot or previous delta cursor. Deleted d | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `deploymentUrl` | string | Yes | Convex deployment URL \(e.g., https://your-deployment.convex.cloud) | -| `deployKey` | string | Yes | Convex deploy key from the dashboard Settings page | | `cursor` | string | Yes | Timestamp cursor to read deltas after. Use the snapshot value from List Documents or the cursor from a previous Document Deltas page. | | `tableName` | string | No | Table to read deltas from. Omit to read deltas from all tables. | diff --git a/apps/docs/content/docs/integrations/crunchbase.mdx b/apps/docs/content/docs/integrations/crunchbase.mdx index d58c8ff64c7..b21451c8338 100644 --- a/apps/docs/content/docs/integrations/crunchbase.mdx +++ b/apps/docs/content/docs/integrations/crunchbase.mdx @@ -2,7 +2,7 @@ title: Crunchbase description: Search and look up companies, people, funding rounds, and acquisitions integration: - type: "crunchbase" + type: "crunchbase_v2" color: "#0287D1" --- @@ -39,7 +39,6 @@ Search Crunchbase companies, investors, and schools with filter predicates on fu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `query` | json | Yes | Filter predicates, combined with AND. Array of \{type:"predicate", field_id, operator_id, values\}. Operators: blank, eq, not_eq, gt, gte, lt, lte, starts, contains, not_contains, between, includes, not_includes, includes_all, not_includes_all, domain_eq, not_domain_eq, domain_blank, domain_includes, not_domain_includes. Max 25 predicates. Example: \[\{"type":"predicate","field_id":"categories","operator_id":"includes","values":\["biotechnology"\]\}\] | | `fieldIds` | json | No | Organization fields to return as columns, e.g. \["identifier","name","founded_on","categories"\]. Defaults to identifier, name, short_description, website_url, linkedin, location_identifiers, categories, founded_on, num_employees_enum, operating_status, rank_org, permalink. | | `order` | json | No | Sort clauses, e.g. \[\{"field_id":"rank_org","sort":"asc","nulls":"last"\}\]. Sort is "asc" or "desc". | @@ -67,7 +66,6 @@ Look up a single Crunchbase organization by permalink or UUID, returning the req | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `entityId` | string | Yes | Organization permalink \(e.g. "tesla-motors"\) or UUID | | `fieldIds` | json | No | Organization fields to return, e.g. \["identifier","name","founded_on","categories"\]. Defaults to identifier, name, short_description, website_url, linkedin, location_identifiers, categories, founded_on, num_employees_enum, operating_status, rank_org, permalink. | | `cardIds` | json | No | Related-entity cards to include, e.g. \["founders","headquarters_address"\]. Available on every license tier: child_organizations, child_ownerships, event_appearances, fields, founders, headquarters_address, parent_organization, parent_ownership. Richer packages add acquiree_acquisitions, acquirer_acquisitions, investors, ipos, jobs, participated_funding_rounds, participated_funds, participated_investments, press_references, raised_funding_rounds, raised_funds, raised_investments. A card returns at most 100 items. | @@ -94,7 +92,6 @@ Search Crunchbase people — founders, executives, and investors — with filter | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `query` | json | Yes | Filter predicates, combined with AND. Array of \{type:"predicate", field_id, operator_id, values\}. Operators: blank, eq, not_eq, gt, gte, lt, lte, starts, contains, not_contains, between, includes, not_includes, includes_all, not_includes_all, domain_eq, not_domain_eq, domain_blank, domain_includes, not_domain_includes. Max 25 predicates. Example: \[\{"type":"predicate","field_id":"primary_job_title","operator_id":"contains","values":\["Founder"\]\}\] | | `fieldIds` | json | No | Person fields to return as columns, e.g. \["identifier","name","primary_job_title","primary_organization"\]. Defaults to identifier, name, first_name, last_name, primary_job_title, primary_organization, short_description, location_identifiers, linkedin, rank_person, permalink. | | `order` | json | No | Sort clauses, e.g. \[\{"field_id":"rank_person","sort":"asc","nulls":"last"\}\]. Sort is "asc" or "desc". | @@ -122,7 +119,6 @@ Look up a single Crunchbase person by permalink or UUID, returning the requested | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `entityId` | string | Yes | Person permalink \(e.g. "elon-musk"\) or UUID | | `fieldIds` | json | No | Person fields to return, e.g. \["identifier","name","primary_job_title","primary_organization"\]. Defaults to identifier, name, first_name, last_name, primary_job_title, primary_organization, short_description, location_identifiers, linkedin, rank_person, permalink. | | `cardIds` | json | No | Related-entity cards to include, e.g. \["jobs","primary_organization"\]. Available on every license tier: degrees, event_appearances, fields, founded_organizations, jobs, primary_job, primary_organization. Advanced Financials adds participated_funding_rounds, participated_funds, participated_investments, partner_funding_rounds, partner_investments, press_references. A card returns at most 100 items. | @@ -149,7 +145,6 @@ Search Crunchbase funding rounds with filter predicates on announced date, inves | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `query` | json | Yes | Filter predicates, combined with AND. Array of \{type:"predicate", field_id, operator_id, values\}. Operators: blank, eq, not_eq, gt, gte, lt, lte, starts, contains, not_contains, between, includes, not_includes, includes_all, not_includes_all, domain_eq, not_domain_eq, domain_blank, domain_includes, not_domain_includes. Max 25 predicates. Example: \[\{"type":"predicate","field_id":"announced_on","operator_id":"gte","values":\["2026-01-01"\]\}\] | | `fieldIds` | json | No | Funding round fields to return as columns, e.g. \["identifier","announced_on","money_raised","investor_identifiers"\]. Defaults to identifier, announced_on, investment_type, investment_stage, money_raised, funded_organization_identifier, investor_identifiers, lead_investor_identifiers, num_investors, short_description, permalink. | | `order` | json | No | Sort clauses, e.g. \[\{"field_id":"announced_on","sort":"desc","nulls":"last"\}\]. Sort is "asc" or "desc". | @@ -177,7 +172,6 @@ Look up a single Crunchbase funding round by permalink or UUID, returning the re | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `entityId` | string | Yes | Funding round permalink \(e.g. "tesla-motors-series-c--12345678"\) or UUID | | `fieldIds` | json | No | Funding round fields to return, e.g. \["identifier","announced_on","money_raised","investor_identifiers"\]. Defaults to identifier, announced_on, investment_type, investment_stage, money_raised, funded_organization_identifier, investor_identifiers, lead_investor_identifiers, num_investors, short_description, permalink. | | `cardIds` | json | No | Related-entity cards to include, e.g. \["investors","organization"\]. Available: fields, investments, investors, lead_investors, organization, partners, press_references. A card returns at most 100 items. | @@ -204,7 +198,6 @@ Search Crunchbase acquisitions with filter predicates on announced date, price, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `query` | json | Yes | Filter predicates, combined with AND. Array of \{type:"predicate", field_id, operator_id, values\}. Operators: blank, eq, not_eq, gt, gte, lt, lte, starts, contains, not_contains, between, includes, not_includes, includes_all, not_includes_all, domain_eq, not_domain_eq, domain_blank, domain_includes, not_domain_includes. Max 25 predicates. Example: \[\{"type":"predicate","field_id":"announced_on","operator_id":"gte","values":\["2026-01-01"\]\}\] | | `fieldIds` | json | No | Acquisition fields to return as columns, e.g. \["identifier","acquiree_identifier","acquirer_identifier","price"\]. Defaults to identifier, acquiree_identifier, acquirer_identifier, announced_on, completed_on, price, acquisition_type, status, terms, short_description, permalink. | | `order` | json | No | Sort clauses, e.g. \[\{"field_id":"announced_on","sort":"desc","nulls":"last"\}\]. Sort is "asc" or "desc". | @@ -232,7 +225,6 @@ Look up a single Crunchbase acquisition by permalink or UUID, returning the requ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `entityId` | string | Yes | Acquisition permalink or UUID | | `fieldIds` | json | No | Acquisition fields to return, e.g. \["identifier","acquiree_identifier","acquirer_identifier","price"\]. Defaults to identifier, acquiree_identifier, acquirer_identifier, announced_on, completed_on, price, acquisition_type, status, terms, short_description, permalink. | | `cardIds` | json | No | Related-entity cards to include, e.g. \["acquiree_organization","acquirer_organization"\]. Available: acquiree_organization, acquirer_organization, fields, press_references. A card returns at most 100 items. | @@ -259,7 +251,6 @@ Search any Crunchbase collection — events, jobs, ipos, funds, investments, pre | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `collection` | string | Yes | Collection to search. One of: acquisition_predictions, acquisitions, addresses, awards, categories, category_groups, closure_predictions, current_valuation_estimates, degrees, diversity_spotlights, event_appearances, events, funding_predictions, funding_rounds, funds, growth_insights, growth_predictions, investments, investor_insights, investor_matches, ipo_predictions, ipos, jobs, key_employee_changes, layoff_predictions, layoffs, legal_proceedings, locations, market_insight_reasons, market_insights, micro_categories, org_similarities, organizations, ownerships, partnership_announcements, people, press_references, principals, product_launches, product_similarities, products, remain_private_predictions, research_insights. | | `query` | json | Yes | Filter predicates, combined with AND. Array of \{type:"predicate", field_id, operator_id, values\}. Operators: blank, eq, not_eq, gt, gte, lt, lte, starts, contains, not_contains, between, includes, not_includes, includes_all, not_includes_all, domain_eq, not_domain_eq, domain_blank, domain_includes, not_domain_includes. Max 25 predicates. | | `fieldIds` | json | Yes | Fields to return as columns for the chosen collection, e.g. \["identifier","short_description"\]. Required — the valid ids differ per collection; list them with the Get Fields Metadata operation. | @@ -288,7 +279,6 @@ Look up a single entity in any Crunchbase collection — events, jobs, ipos, fun | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `collection` | string | Yes | Collection the entity belongs to. One of: acquisition_predictions, acquisitions, addresses, awards, categories, category_groups, closure_predictions, current_valuation_estimates, degrees, diversity_spotlights, event_appearances, events, funding_predictions, funding_rounds, funds, growth_insights, growth_predictions, investments, investor_insights, investor_matches, ipo_predictions, ipos, jobs, key_employee_changes, layoff_predictions, layoffs, legal_proceedings, locations, market_insight_reasons, market_insights, micro_categories, org_similarities, organizations, ownerships, partnership_announcements, people, press_references, principals, product_launches, product_similarities, products, remain_private_predictions, research_insights. | | `entityId` | string | Yes | Entity permalink or UUID | | `fieldIds` | json | No | Fields to return for the chosen collection, e.g. \["identifier","short_description"\]. Leave empty to accept the default projection the API returns; list the valid ids with the Get Fields Metadata operation. | @@ -316,7 +306,6 @@ Page through one related-entity card of a Crunchbase entity — an investor's in | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `collection` | string | Yes | Collection the entity belongs to. One of: acquisitions, addresses, categories, category_groups, degrees, event_appearances, events, funding_rounds, funds, investments, ipos, jobs, market_insights, micro_categories, organizations, ownerships, people. | | `entityId` | string | Yes | Entity permalink or UUID | | `cardId` | string | Yes | Card to page through, e.g. "participated_investments" on a person, "founders" on an organization, or "investors" on a funding round. Valid ids differ per collection. | @@ -346,7 +335,6 @@ Suggest Crunchbase entities matching a typed query, returning the permalinks and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `query` | string | Yes | Text to autocomplete against, e.g. "airbnb" | | `collectionIds` | json | No | Collections to search, e.g. \["organizations","people"\]. One or more of: acquisition_predictions, acquisitions, addresses, awards, categories, category_groups, closure_predictions, current_valuation_estimates, degrees, diversity_spotlights, event_appearances, events, funding_predictions, funding_rounds, funds, growth_insights, growth_predictions, investments, investor_insights, investor_matches, ipo_predictions, ipos, jobs, key_employee_changes, layoff_predictions, layoffs, legal_proceedings, locations, market_insight_reasons, market_insights, micro_categories, org_similarities, organizations, ownerships, partnership_announcements, people, press_references, principals, product_launches, product_similarities, products, remain_private_predictions, research_insights. Which of them your key can read depends on your Crunchbase package. Defaults to every collection. | | `limit` | number | No | Suggestions to return, max 25 \(default 10\) | @@ -369,7 +357,6 @@ List entities Crunchbase has deleted, so a mirrored copy can be pruned in step w | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `collection` | string | No | Restrict the feed to a single collection: acquisitions, categories, event_appearances, events, funding_rounds, funds, investments, ipos, jobs, locations, organizations, ownerships, people, or press_references. Which of them your key can read depends on your Crunchbase package. Leave empty to read the feed across collections. | | `collectionIds` | json | No | Collections to include when reading the cross-collection feed, e.g. \["organizations","people"\]. One or more of: acquisitions, categories, event_appearances, events, funding_rounds, funds, investments, ipos, jobs, locations, organizations, ownerships, people, press_references. Ignored when a single collection is set. | | `deletedAtOrder` | string | No | Order by deletion time: "asc" \(default\) or "desc" | @@ -396,7 +383,6 @@ List the field ids, types, and descriptions each Crunchbase collection publishes | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Crunchbase API key, sent as the X-cb-user-key header | | `collectionIds` | json | No | Collections to describe, e.g. \["organizations","people"\]. One or more of: acquisition_predictions, acquisitions, addresses, awards, categories, category_groups, closure_predictions, current_valuation_estimates, degrees, diversity_spotlights, event_appearances, events, funding_predictions, funding_rounds, funds, growth_insights, growth_predictions, investments, investor_insights, investor_matches, ipo_predictions, ipos, jobs, key_employee_changes, layoff_predictions, layoffs, legal_proceedings, locations, market_insight_reasons, market_insights, micro_categories, org_similarities, organizations, ownerships, partnership_announcements, people, press_references, principals, product_launches, product_similarities, products, remain_private_predictions, research_insights. Which of them your key can read depends on your Crunchbase package. Defaults to every collection. | #### Output diff --git a/apps/docs/content/docs/integrations/cursor.mdx b/apps/docs/content/docs/integrations/cursor.mdx index e6855805da9..c50a8e37e64 100644 --- a/apps/docs/content/docs/integrations/cursor.mdx +++ b/apps/docs/content/docs/integrations/cursor.mdx @@ -2,7 +2,7 @@ title: Cursor description: Launch and manage Cursor cloud agents to work on GitHub repositories integration: - type: "cursor_v2" + type: "cursor_v3" color: "#1E1E1E" --- @@ -29,7 +29,6 @@ List all cloud agents for the authenticated user with optional pagination. Retur | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | | `limit` | number | No | Number of agents to return \(default: 20, max: 100\) | | `cursor` | string | No | Pagination cursor from previous response | | `prUrl` | string | No | Filter agents by pull request URL | @@ -53,7 +52,6 @@ Retrieve the current status and results of a cloud agent. Returns API-aligned fi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | | `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) | #### Output @@ -80,7 +78,6 @@ Retrieve the conversation history of a cloud agent, including all user prompts a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | | `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) | #### Output @@ -102,7 +99,6 @@ Start a new cloud agent to work on a GitHub repository with the given instructio | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | | `repository` | string | Yes | GitHub repository URL \(e.g., https://github.com/your-org/your-repo) | | `ref` | string | No | Branch, tag, or commit to work from \(defaults to default branch\) | | `promptText` | string | Yes | The instruction text for the agent | @@ -132,7 +128,6 @@ Add a follow-up instruction to an existing cloud agent. Returns API-aligned fiel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | | `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) | | `followupPromptText` | string | Yes | The follow-up instruction text for the agent | | `promptImages` | string | No | JSON array of image objects with base64 data and dimensions \(max 5\) | @@ -155,7 +150,6 @@ Stop a running cloud agent. Returns API-aligned fields only. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | | `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) | #### Output @@ -176,7 +170,6 @@ Permanently delete a cloud agent. Returns API-aligned fields only. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | | `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) | #### Output @@ -197,7 +190,6 @@ List generated artifact files for a cloud agent. Returns API-aligned fields only | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | | `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) | #### Output @@ -220,7 +212,6 @@ Download a generated artifact file from a cloud agent. Returns the file for exec | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | | `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) | | `path` | string | Yes | Absolute path of the artifact to download \(e.g., /src/index.ts\) | @@ -242,7 +233,6 @@ List the models available for launching cloud agents. Returns API-aligned fields | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | #### Output @@ -262,7 +252,6 @@ List the GitHub repositories accessible to the authenticated user. Returns API-a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | #### Output @@ -285,7 +274,6 @@ Retrieve details about the API key currently in use. Returns API-aligned fields | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Cursor API key | #### Output diff --git a/apps/docs/content/docs/integrations/databricks-credentials.mdx b/apps/docs/content/docs/integrations/databricks-credentials.mdx new file mode 100644 index 00000000000..0f332e9b62d --- /dev/null +++ b/apps/docs/content/docs/integrations/databricks-credentials.mdx @@ -0,0 +1,52 @@ +--- +title: Databricks connections +description: Save a Databricks workspace connection with a personal access token, a service principal OAuth secret, or a keyless service principal that trusts Sim through OpenID Connect +--- + +import { Callout } from '@/components/ui/callout' +import { Step, Steps } from '@/components/ui/steps' + +A Databricks connection holds a workspace host and how Sim authenticates to it. The Databricks block signs in with a saved connection, so the host and secret live in one place instead of in each block. + + +Databricks blocks added before saved connections, which take a host and personal access token typed into the block, are marked legacy and keep working exactly as they are. Upgrade one by replacing it with the current block and selecting a connection. + + +## Choose how Sim signs in + +| Option | What Sim stores | When to use it | +| --- | --- | --- | +| **Service principal (keyless, via Sim OIDC)** | Nothing secret: the host and the service principal's application ID | Recommended. No secret to rotate or leak. | +| **Service principal OAuth secret** | The application ID and an OAuth secret | Machine-to-machine access where federation isn't set up. Sim exchanges the secret for a short-lived token on each run. | +| **Personal access token** | A personal access token | The same behavior as a token typed into the block, shared across blocks. | + +Every option resolves on Sim's servers at run time; tokens never appear in block outputs, logs, or anything a model sees. + +## Set up a keyless service principal [#keyless-service-principal] + + + + In Sim, add a **Databricks connection**, choose **Service principal (keyless, via Sim OIDC)**, and enter the workspace host and the service principal's **application ID**. + + + A Databricks account admin runs the **Federation policy** command from the dialog with the service principal's numeric ID. First sign in to the account with `databricks auth login --host --account-id ` (not a workspace profile or the workspace web terminal). Find the numeric ID with `databricks account service-principals list --filter 'applicationId eq ""'`, or on the service principal's page in the account console. The policy trusts Sim's issuer, the Sim audience, and this connection's subject only. + + + Make sure the service principal can access the workspace and the resources your workflows use, then save. Sim verifies the connection by exchanging a token and reading the service principal's identity, and refuses a policy that would also trust a different subject, since every Sim workspace shares the issuer and audience. + + + +Reconnecting a keyless connection keeps its subject by default, so the federation policy keeps working. + +### From the API or CLI [#keyless-api-cli] + +Run `sim credentials keyless-subject databricks-service-account`, trust the printed subject as above, then add `host` and `clientId` to its `keylessConfig` and pass it to `sim credentials create databricks-service-account`. See [the CLI credentials reference](/cli/credentials) for the full two-step example; the proof expires after 24 hours and works only for the user who requested it. + +## Self-hosted deployments + +Keyless connections need `WORKLOAD_IDENTITY_SIGNING_KEY` set and an `https` issuer Databricks can reach over the public internet. See [AWS credentials](/integrations/aws-credentials#self-hosted-deployments) for the same settings and for rotating the signing key. + +## Troubleshooting + +- **"Databricks rejected the token request":** the federation policy's issuer, audience, or subject doesn't match the dialog, or the policy is on a different service principal than the application ID you entered. +- **"This service principal's federation policy also trusts other Sim credentials":** the policy's `subject_claim` isn't `sub`, so it matches more than this connection. Recreate it with the command from the dialog. diff --git a/apps/docs/content/docs/integrations/databricks.mdx b/apps/docs/content/docs/integrations/databricks.mdx index 991e2f0b68e..568bdfb7642 100644 --- a/apps/docs/content/docs/integrations/databricks.mdx +++ b/apps/docs/content/docs/integrations/databricks.mdx @@ -2,7 +2,7 @@ title: Databricks description: Run SQL, manage jobs, and ask Genie agents on Databricks integration: - type: "databricks" + type: "databricks_v2" color: "#F9F7F4" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Databricks](https://www.databricks.com/) to execute parameterized SQL, manage jobs and runs, control compute clusters and SQL warehouses, and work with Genie spaces. Connect with a personal access token and your Databricks workspace host URL. +Use [Databricks](https://www.databricks.com/) to execute parameterized SQL, manage jobs and runs, control compute clusters and SQL warehouses, and work with Genie spaces. Connect with a saved [Databricks connection](/integrations/databricks-credentials), which holds the workspace host and how Sim signs in. SQL operations return inline JSON rows, including SQL nulls. If a statement is still running, poll **Get Statement**. Follow `nextChunkIndex` with **Get Statement Chunk** to retrieve additional rows, or use **Cancel Statement** to stop execution. Configure `rowLimit` and `byteLimit` for predictable result sizes; external-link and Arrow/CSV downloads are outside these SQL operations. @@ -21,9 +21,11 @@ Cluster operations support creating, configuring, starting, restarting, resizing Genie operations create and update spaces, answer natural-language questions, manage conversation history, and retrieve generated SQL, results, and charts. Reuse the conversation ID for follow-ups. Use a space's `etag` when updating it to avoid overwriting concurrent changes. Agent mode supports research reports and response cancellation; it must be enabled in your workspace. {/* MANUAL-CONTENT-END */} +Connection guides: [Databricks connections](/integrations/databricks-credentials). + ## Use the block [#usage-instructions] -Execute parameterized SQL and retrieve result chunks, create and manage jobs, submit and repair runs, manage compute clusters and SQL warehouses, and create or update Genie spaces. Ask Genie questions or run agent research, inspect conversations, and retrieve query results and charts. Connect with a personal access token and workspace host URL. +Execute parameterized SQL and retrieve result chunks, create and manage jobs, submit and repair runs, manage compute clusters and SQL warehouses, and create or update Genie spaces. Ask Genie questions or run agent research, inspect conversations, and retrieve query results and charts. ## Actions @@ -37,8 +39,6 @@ Execute a SQL statement against a Databricks SQL warehouse and return results in | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `warehouseId` | string | Yes | The ID of the SQL warehouse to execute against | | `statement` | string | Yes | The SQL statement to execute \(max 16 MiB\) | | `catalog` | string | No | Unity Catalog name \(equivalent to USE CATALOG\) | @@ -81,8 +81,6 @@ Poll a SQL statement by its ID to retrieve status and results. Use this after Ex | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `warehouseId` | string | No | SQL warehouse identifier, when required by the statement endpoint | | `statementId` | string | Yes | The ID of the statement to fetch \(returned by Execute SQL\) | @@ -118,8 +116,6 @@ Retrieve one chunk of an INLINE SQL result using nextChunkIndex from Execute SQL | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `statementId` | string | Yes | SQL statement identifier | | `chunkIndex` | number | Yes | Zero-based result chunk index | | `warehouseId` | string | No | SQL warehouse identifier, when required by the statement endpoint | @@ -147,8 +143,6 @@ Request cancellation of an executing SQL statement; poll Get Statement to confir | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `statementId` | string | Yes | SQL statement identifier | | `warehouseId` | string | No | SQL warehouse identifier, when required by the statement endpoint | @@ -170,8 +164,6 @@ List all jobs in a Databricks workspace with optional filtering by name. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `limit` | number | No | Maximum number of jobs to return \(range 1-100, default 20\) | | `pageToken` | string | No | Token for the next or previous page; takes precedence over legacy offset | | `offset` | number | No | Legacy Jobs 2.1 offset; prefer pageToken | @@ -320,8 +312,6 @@ Get the full definition and settings of a single Databricks job by its job ID. | --------- | ---- | -------- | ----------- | | `includeTriggerState` | boolean | No | Include current job trigger state | | `pageToken` | string | No | Token for the next page of tasks and job clusters | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `jobId` | number | Yes | The canonical identifier of the job to retrieve | #### Output @@ -471,8 +461,6 @@ Create a Databricks job with tasks, scheduling, execution identity, and notifica | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `accessControlList` | json | No | Initial permissions as \[\{user_name, group_name, service_principal_name, permission_level\}\] | | `name` | string | No | Display name of the job | | `description` | string | No | Description of the job | @@ -522,8 +510,6 @@ Merge selected settings into a Databricks job and optionally remove settings or | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `jobId` | number | Yes | Job identifier | | `newSettings` | json | No | Job settings in Databricks API format; Update merges top-level fields and task keys, Reset replaces all settings | | `fieldsToRemove` | json | No | Settings to remove, such as \["schedule", "tasks/task_key"\] | @@ -546,8 +532,6 @@ Replace all settings of a Databricks job. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `jobId` | number | Yes | Job identifier | | `newSettings` | json | Yes | Job settings in Databricks API format; Update merges top-level fields and task keys, Reset replaces all settings | @@ -569,8 +553,6 @@ Delete a Databricks job and cancel its active runs. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `jobId` | number | Yes | Job identifier | #### Output @@ -595,8 +577,6 @@ Trigger an existing Databricks job to run immediately with optional job-level or | `queue` | json | No | Run queue settings as \{enabled: true\} or \{enabled: false\} | | `performanceTarget` | string | No | Serverless performance: STANDARD or PERFORMANCE_OPTIMIZED | | `pipelineParams` | json | No | Pipeline full_refresh and selective table or flow settings | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `jobId` | number | Yes | The ID of the job to trigger | | `jobParameters` | json | No | Job-level parameter overrides as a JSON object \(e.g., \{"key": "value"\}\) | | `notebookParams` | json | No | Notebook task parameters as a JSON object \(e.g., \{"param1": "value1"\}\) | @@ -621,8 +601,6 @@ Submit a one-time task run without creating a saved job. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `accessControlList` | json | No | Initial permissions as \[\{user_name, group_name, service_principal_name, permission_level\}\] | | `queue` | json | No | Queue behavior as \{enabled: true\} or \{enabled: false\} | | `runAs` | json | No | Execution identity as \{user_name\} or \{service_principal_name\} | @@ -658,8 +636,6 @@ List job runs in a Databricks workspace with optional filtering by job, status, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `jobId` | number | No | Filter runs by job ID. Omit to list runs across all jobs | | `activeOnly` | boolean | No | Only include active runs \(PENDING, RUNNING, or TERMINATING\) | | `completedOnly` | boolean | No | Only include completed runs | @@ -827,8 +803,6 @@ Get the status, timing, and details of a Databricks job run by its run ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `pageToken` | string | No | Token for the next page of tasks and job clusters | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `runId` | number | Yes | The canonical identifier of the run | | `includeHistory` | boolean | No | Include repair history in the response | | `includeResolvedValues` | boolean | No | Include resolved parameter values in the response | @@ -1004,8 +978,6 @@ Get the output of a completed Databricks job run, including notebook results, er | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `runId` | number | Yes | The run ID to get output for. For multi-task jobs, use the task run ID | #### Output @@ -1043,8 +1015,6 @@ Rerun failed or selected tasks within an existing Databricks run. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `runId` | number | Yes | Run identifier | | `latestRepairId` | number | No | Most recent repair ID; required after the first repair of a run | | `rerunTasks` | json | No | Task keys to repair; mutually exclusive with rerunAllFailedTasks | @@ -1072,8 +1042,6 @@ Cancel a running or pending Databricks job run. Cancellation is asynchronous; po | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `runId` | number | Yes | The canonical identifier of the run to cancel | #### Output @@ -1094,8 +1062,6 @@ Request cancellation of active runs of a job or queued runs in the workspace. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `jobId` | number | No | Job identifier; required unless allQueuedRuns is true | | `allQueuedRuns` | boolean | No | Cancel queued runs; without a job ID this applies to the workspace | @@ -1117,8 +1083,6 @@ Delete a completed Databricks job run. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `runId` | number | Yes | Run identifier | #### Output @@ -1141,8 +1105,6 @@ List a page of clusters in a Databricks workspace including their state, configu | --------- | ---- | -------- | ----------- | | `pageSize` | number | No | Resources per page \(default 20\) | | `pageToken` | string | No | Token returned by the previous list request | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | #### Output @@ -1185,8 +1147,6 @@ Get the state, configuration, and resource details of a single Databricks cluste | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `clusterId` | string | Yes | The ID of the cluster to retrieve | #### Output @@ -1228,8 +1188,6 @@ Create a compute cluster with runtime, sizing, policy, and cloud-specific settin | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `applyPolicyDefaultValues` | boolean | No | Fill unspecified settings from the compute policy defaults | | `cloneFrom` | json | No | Clone source as \{source_cluster_id, clone_source\} | | `numWorkers` | number | No | Fixed worker count; use this or autoscale | @@ -1285,8 +1243,6 @@ Replace a compute cluster configuration; a running cluster restarts to apply the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `clusterId` | string | Yes | Compute cluster identifier | | `applyPolicyDefaultValues` | boolean | No | Fill unspecified settings from the compute policy defaults | | `numWorkers` | number | No | Fixed worker count; use this or autoscale | @@ -1342,8 +1298,6 @@ Start a terminated all-purpose compute cluster. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `clusterId` | string | Yes | Compute cluster identifier | #### Output @@ -1364,8 +1318,6 @@ Restart a running compute cluster. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `clusterId` | string | Yes | Compute cluster identifier | | `restartUser` | string | No | User requesting the cluster restart | @@ -1387,8 +1339,6 @@ Change the fixed worker count or autoscaling range of a running cluster. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `clusterId` | string | Yes | Compute cluster identifier | | `numWorkers` | number | No | Fixed worker count; use this or autoscale | | `autoscale` | json | No | Worker range as \{min_workers, max_workers\}; use this or numWorkers | @@ -1411,8 +1361,6 @@ Terminate a compute cluster while retaining its configuration for a later restar | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `clusterId` | string | Yes | Compute cluster identifier | #### Output @@ -1433,8 +1381,6 @@ Permanently delete a compute cluster and its configuration. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `clusterId` | string | Yes | Compute cluster identifier | #### Output @@ -1457,8 +1403,6 @@ List a page of SQL warehouses in a Databricks workspace including their size, st | --------- | ---- | -------- | ----------- | | `pageSize` | number | No | Resources per page \(default 20\) | | `pageToken` | string | No | Token returned by the previous list request | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | #### Output @@ -1512,8 +1456,6 @@ Get the configuration, state, and scaling settings of a SQL warehouse. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `warehouseId` | string | Yes | SQL warehouse identifier | #### Output @@ -1567,8 +1509,6 @@ Create a SQL warehouse with sizing, autoscaling, and serverless settings. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `name` | string | No | Display name of the SQL warehouse | | `clusterSize` | string | No | SQL warehouse size, such as Small or Medium | | `minNumClusters` | number | No | Minimum number of SQL warehouse clusters | @@ -1601,8 +1541,6 @@ Update a SQL warehouse configuration. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `warehouseId` | string | Yes | SQL warehouse identifier | | `name` | string | No | Display name of the SQL warehouse | | `clusterSize` | string | No | SQL warehouse size, such as Small or Medium | @@ -1636,8 +1574,6 @@ Start a stopped SQL warehouse. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `warehouseId` | string | Yes | SQL warehouse identifier | #### Output @@ -1658,8 +1594,6 @@ Stop a running SQL warehouse. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `warehouseId` | string | Yes | SQL warehouse identifier | #### Output @@ -1680,8 +1614,6 @@ Delete a SQL warehouse. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `warehouseId` | string | Yes | SQL warehouse identifier | #### Output @@ -1702,8 +1634,6 @@ List the Genie spaces (Genie agents) you can access. Use this to find the space | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `pageSize` | number | No | Maximum number of spaces to return per page \(default 20, max 100\) | | `pageToken` | string | No | Pagination token from a previous response | @@ -1734,8 +1664,6 @@ Get a Genie space's title, description, and SQL warehouse, optionally with its f | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `includeSerializedSpace` | boolean | No | Include the space's serialized configuration export \(requires CAN EDIT on the space\) | @@ -1765,8 +1693,6 @@ Create a Genie space from its serialized configuration and a SQL warehouse. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `warehouseId` | string | Yes | SQL warehouse identifier | | `parentPath` | string | No | Parent workspace folder for the Genie space | | `serializedSpace` | string | Yes | Serialized Genie configuration JSON, including data_sources, instructions, config, and benchmarks; get an example with Get Genie Space | @@ -1799,8 +1725,6 @@ Update a Genie space, optionally protecting the change with its current ETag. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | Genie space identifier | | `serializedSpace` | string | No | Serialized Genie configuration JSON, including data_sources, instructions, config, and benchmarks; get an example with Get Genie Space | | `title` | string | No | Genie space title | @@ -1835,8 +1759,6 @@ Move a Genie space to the trash. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | Genie space identifier | #### Output @@ -1857,8 +1779,6 @@ Ask a Databricks Genie space a question in natural language and wait for the ans | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `content` | string | Yes | The question to ask Genie | | `conversationId` | string | No | Continue this conversation so Genie uses its earlier messages as context. Omit to start a new conversation. | @@ -1916,8 +1836,6 @@ Get a Genie message's status and answer, including the generated SQL, visualizat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `conversationId` | string | Yes | The ID of the Genie conversation | | `messageId` | string | Yes | The ID of the Genie message | @@ -1962,8 +1880,6 @@ List the messages in a Genie conversation with their answers and generated SQL, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `conversationId` | string | Yes | The ID of the Genie conversation | | `pageSize` | number | No | Maximum number of messages to return per page \(default 20, max 100\) | @@ -2011,8 +1927,6 @@ Get the rows returned by the SQL query Genie generated for a message. Available | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `conversationId` | string | Yes | The ID of the Genie conversation | | `messageId` | string | Yes | The ID of the Genie message | @@ -2050,8 +1964,6 @@ Re-run the SQL query Genie generated for a message. Use this when the message's | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `conversationId` | string | Yes | The ID of the Genie conversation | | `messageId` | string | Yes | The ID of the Genie message | @@ -2089,8 +2001,6 @@ Download a chart Genie generated as a PNG image, for example to post it to Slack | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `conversationId` | string | Yes | The ID of the Genie conversation | | `messageId` | string | Yes | The ID of the Genie message | @@ -2114,8 +2024,6 @@ Rate a Genie answer as positive or negative, with an optional comment. Space aut | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `conversationId` | string | Yes | The ID of the Genie conversation | | `messageId` | string | Yes | The ID of the Genie message | @@ -2140,8 +2048,6 @@ Delete a message from a Genie conversation. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `conversationId` | string | Yes | The ID of the Genie conversation | | `messageId` | string | Yes | The ID of the Genie message | @@ -2164,8 +2070,6 @@ List conversations in a Genie space, including whether each is a chat-mode or ag | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `includeAll` | boolean | No | Include every user's conversations in the space \(requires CAN MANAGE on the space\) | | `pageSize` | number | No | Maximum number of conversations to return per page \(default 20, max 100\) | @@ -2194,8 +2098,6 @@ Delete a Genie conversation you no longer need. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `conversationId` | string | Yes | The ID of the Genie conversation to delete | @@ -2217,8 +2119,6 @@ Ask a Genie agent a question in agent mode, where Genie plans multi-step researc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space \(the Genie agent ID\) | | `content` | string | Yes | The question to ask the Genie agent | | `conversationId` | string | No | Continue this agent-mode conversation. Omit to start a new conversation. | @@ -2261,8 +2161,6 @@ List an agent-mode conversation's full history in order: questions, reasoning, S | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space \(the Genie agent ID\) | | `conversationId` | string | Yes | The ID of the agent-mode conversation | | `limit` | number | No | Maximum number of items to return \(1-100, default 100\) | @@ -2300,8 +2198,6 @@ Cancel an in-flight Genie agent response using the response ID returned by the a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | -| `apiKey` | string | Yes | Databricks Personal Access Token | | `spaceId` | string | Yes | The ID of the Genie space | | `conversationId` | string | Yes | Agent conversation identifier | | `responseId` | string | Yes | Response ID from the response.created stream event | diff --git a/apps/docs/content/docs/integrations/daytona.mdx b/apps/docs/content/docs/integrations/daytona.mdx index 9026dc20019..bae9100557a 100644 --- a/apps/docs/content/docs/integrations/daytona.mdx +++ b/apps/docs/content/docs/integrations/daytona.mdx @@ -2,7 +2,7 @@ title: Daytona description: Run code and commands in secure cloud sandboxes integration: - type: "daytona" + type: "daytona_v2" color: "#FFFFFF" --- @@ -29,7 +29,6 @@ Create a new Daytona sandbox for running AI-generated code in isolation | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `snapshot` | string | No | ID or name of the snapshot to create the sandbox from \(uses default if empty\) | | `name` | string | No | Name for the sandbox \(defaults to the sandbox ID\) | | `target` | string | No | Region where the sandbox will be created \(e.g., us, eu\) | @@ -62,7 +61,6 @@ List Daytona sandboxes in the organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `limit` | number | No | Maximum number of sandboxes to return \(1-200\) | | `name` | string | No | Filter sandboxes by name prefix \(case-insensitive\) | | `labels` | json | No | Filter sandboxes by labels as key-value pairs | @@ -87,7 +85,6 @@ Get details of a Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID or name of the sandbox | #### Output @@ -108,7 +105,6 @@ Start a stopped Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID or name of the sandbox | #### Output @@ -129,7 +125,6 @@ Stop a running Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID or name of the sandbox | #### Output @@ -150,7 +145,6 @@ Delete a Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID or name of the sandbox | #### Output @@ -171,7 +165,6 @@ Execute a shell command inside a Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID of the sandbox to execute the command in | | `command` | string | Yes | Shell command to execute | | `cwd` | string | No | Working directory for the command \(defaults to the sandbox working directory\) | @@ -197,7 +190,6 @@ Run Python, JavaScript, or TypeScript code inside a Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID of the sandbox to run the code in | | `code` | string | Yes | Code to run | | `language` | string | Yes | Language of the code: python, javascript, or typescript | @@ -224,7 +216,6 @@ Upload a file to a Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID of the sandbox to upload the file to | | `destinationPath` | string | Yes | Destination path in the sandbox \(a trailing slash uploads into that directory using the file name\) | | `file` | file | No | The file to upload | @@ -250,7 +241,6 @@ Download a file from a Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID of the sandbox to download the file from | | `filePath` | string | Yes | Path of the file in the sandbox | @@ -275,7 +265,6 @@ List files in a directory of a Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID of the sandbox to list files in | | `path` | string | No | Directory path to list \(defaults to the sandbox working directory\) | @@ -305,7 +294,6 @@ Clone a Git repository into a Daytona sandbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Daytona API key | | `sandboxId` | string | Yes | ID of the sandbox to clone the repository into | | `url` | string | Yes | URL of the Git repository to clone | | `path` | string | Yes | Path in the sandbox to clone the repository into | diff --git a/apps/docs/content/docs/integrations/devin.mdx b/apps/docs/content/docs/integrations/devin.mdx index ed25ab6e30d..49ccec99c85 100644 --- a/apps/docs/content/docs/integrations/devin.mdx +++ b/apps/docs/content/docs/integrations/devin.mdx @@ -2,7 +2,7 @@ title: Devin description: Autonomous AI software engineer integration: - type: "devin" + type: "devin_v2" color: "#12141A" --- @@ -31,8 +31,6 @@ Create a new Devin session with a prompt. Devin will autonomously work on the ta | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `prompt` | string | Yes | The task prompt for Devin to work on | | `playbookId` | string | No | Optional playbook ID to guide the session | | `maxAcuLimit` | number | No | Maximum ACU limit for the session | @@ -68,8 +66,6 @@ Retrieve details of an existing Devin session including status, tags, pull reque | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `sessionId` | string | Yes | The session ID to retrieve | #### Output @@ -102,8 +98,6 @@ List Devin sessions in the organization. Returns up to 100 sessions by default. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `limit` | number | No | Maximum number of sessions to return \(1-200, default: 100\) | | `after` | string | No | Pagination cursor \(endCursor from a previous response\) to fetch the next page | @@ -140,8 +134,6 @@ Send a message to a Devin session. If the session is suspended, it will be autom | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `sessionId` | string | Yes | The session ID to send the message to | | `message` | string | Yes | The message to send to Devin | @@ -175,8 +167,6 @@ List the messages exchanged in a Devin session, including messages from both the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `sessionId` | string | Yes | The session ID to list messages for | | `limit` | number | No | Maximum number of messages to return \(1-200, default: 100\) | | `after` | string | No | Pagination cursor \(endCursor from a previous response\) to fetch the next page | @@ -206,8 +196,6 @@ List the files uploaded to or produced by a Devin session. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `sessionId` | string | Yes | The session ID to list attachments for | #### Output @@ -233,8 +221,6 @@ Retrieve the tags currently applied to a Devin session. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `sessionId` | string | Yes | The session ID to retrieve tags for | #### Output @@ -255,8 +241,6 @@ Add tags to a Devin session without removing existing tags (max 50 tags total). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `sessionId` | string | Yes | The session ID to add tags to | | `tags` | string | Yes | Tags to append to the session \(comma-separated string or array of strings\) | @@ -278,8 +262,6 @@ Replace all tags on a Devin session with a new set of tags (max 50 tags). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `sessionId` | string | Yes | The session ID to replace tags on | | `tags` | string | Yes | Tags that will overwrite the existing tags \(comma-separated string or array of strings\) | @@ -301,8 +283,6 @@ Archive a Devin session. Archived sessions can still be viewed but cannot be mod | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `sessionId` | string | Yes | The session ID to archive | #### Output @@ -335,8 +315,6 @@ Terminate a Devin session. Optionally archive the session instead of permanently | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | -| `orgId` | string | Yes | Devin organization ID \(prefixed with org-\) | | `sessionId` | string | Yes | The session ID to terminate | | `archive` | boolean | No | Archive the session instead of permanently terminating it \(default: false\) | diff --git a/apps/docs/content/docs/integrations/discord.mdx b/apps/docs/content/docs/integrations/discord.mdx index d236151b2e4..d63441d0f7a 100644 --- a/apps/docs/content/docs/integrations/discord.mdx +++ b/apps/docs/content/docs/integrations/discord.mdx @@ -2,7 +2,7 @@ title: Discord description: Interact with Discord integration: - type: "discord" + type: "discord_v2" color: "#5865F2" --- @@ -38,7 +38,6 @@ Send a message to a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to send the message to, e.g., 123456789012345678 | | `content` | string | No | The text content of the message | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -79,7 +78,6 @@ Retrieve messages from a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to retrieve messages from, e.g., 123456789012345678 | | `limit` | number | No | Maximum number of messages to retrieve \(default: 10, max: 100\) | @@ -119,7 +117,6 @@ Retrieve information about a Discord server (guild) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | #### Output @@ -149,7 +146,6 @@ Retrieve information about a Discord user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Discord bot token for authentication | | `userId` | string | Yes | The Discord user ID, e.g., 123456789012345678 | #### Output @@ -179,7 +175,6 @@ Edit an existing message in a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID containing the message, e.g., 123456789012345678 | | `messageId` | string | Yes | The ID of the message to edit, e.g., 123456789012345678 | | `content` | string | No | The new text content for the message | @@ -208,7 +203,6 @@ Delete a message from a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID containing the message, e.g., 123456789012345678 | | `messageId` | string | Yes | The ID of the message to delete, e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -231,7 +225,6 @@ Delete 2-100 messages from a Discord channel in a single request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to delete messages from, e.g., 123456789012345678 | | `messageIds` | json | Yes | Array of 2-100 message IDs to delete. Messages older than 2 weeks cannot be bulk deleted. | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -254,7 +247,6 @@ Add a reaction emoji to a Discord message | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID containing the message, e.g., 123456789012345678 | | `messageId` | string | Yes | The ID of the message to react to, e.g., 123456789012345678 | | `emoji` | string | Yes | The emoji to react with \(unicode emoji or custom emoji in name:id format\) | @@ -278,7 +270,6 @@ Remove a reaction from a Discord message | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID containing the message, e.g., 123456789012345678 | | `messageId` | string | Yes | The ID of the message with the reaction, e.g., 123456789012345678 | | `emoji` | string | Yes | The emoji to remove \(unicode emoji or custom emoji in name:id format\) | @@ -303,7 +294,6 @@ Pin a message in a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID containing the message, e.g., 123456789012345678 | | `messageId` | string | Yes | The ID of the message to pin, e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -326,7 +316,6 @@ Unpin a message in a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID containing the message, e.g., 123456789012345678 | | `messageId` | string | Yes | The ID of the message to unpin, e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -349,7 +338,6 @@ Retrieve all pinned messages in a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to retrieve pinned messages from, e.g., 123456789012345678 | | `limit` | number | No | Maximum number of pins to return per page \(1-50\). Defaults to 50. | | `before` | string | No | Return pins created before this ISO8601 timestamp, for paging past the first 50 results | @@ -382,7 +370,6 @@ Create a thread in a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to create the thread in, e.g., 123456789012345678 | | `name` | string | Yes | The name of the thread \(1-100 characters\) | | `messageId` | string | No | The message ID to create a thread from \(if creating from existing message\), e.g., 123456789012345678 | @@ -414,7 +401,6 @@ Join a thread in Discord | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `threadId` | string | Yes | The thread ID to join, e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -436,7 +422,6 @@ Leave a thread in Discord | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `threadId` | string | Yes | The thread ID to leave, e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -458,7 +443,6 @@ Archive or unarchive a thread in Discord | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `threadId` | string | Yes | The thread ID to archive/unarchive, e.g., 123456789012345678 | | `archived` | boolean | Yes | Whether to archive \(true\) or unarchive \(false\) the thread | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -484,7 +468,6 @@ Create a new channel in a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `name` | string | Yes | The name of the channel \(1-100 characters\) | | `type` | number | No | Channel type \(0=text, 2=voice, 4=category, 5=announcement, 13=stage\) | @@ -514,7 +497,6 @@ Update a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to update, e.g., 123456789012345678 | | `name` | string | No | The new name for the channel | | `topic` | string | No | The new topic for the channel | @@ -543,7 +525,6 @@ Delete a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to delete, e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -570,7 +551,6 @@ Get information about a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to retrieve, e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -598,7 +578,6 @@ List all channels in a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | #### Output @@ -626,7 +605,6 @@ Create a new role in a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `name` | string | Yes | The name of the role | | `color` | number | No | RGB color value as integer \(e.g., 0xFF0000 for red\) | @@ -657,7 +635,6 @@ Update a role in a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `roleId` | string | Yes | The role ID to update, e.g., 123456789012345678 | | `name` | string | No | The new name for the role | @@ -687,7 +664,6 @@ Delete a role from a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `roleId` | string | Yes | The role ID to delete, e.g., 123456789012345678 | @@ -709,7 +685,6 @@ Assign a role to a member in a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `userId` | string | Yes | The user ID to assign the role to, e.g., 123456789012345678 | | `roleId` | string | Yes | The role ID to assign, e.g., 123456789012345678 | @@ -732,7 +707,6 @@ Remove a role from a member in a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `userId` | string | Yes | The user ID to remove the role from, e.g., 123456789012345678 | | `roleId` | string | Yes | The role ID to remove, e.g., 123456789012345678 | @@ -755,7 +729,6 @@ List all roles in a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | #### Output @@ -783,7 +756,6 @@ Kick a member from a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `userId` | string | Yes | The user ID to kick, e.g., 123456789012345678 | | `reason` | string | No | Reason for kicking the member | @@ -806,7 +778,6 @@ Ban a member from a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `userId` | string | Yes | The user ID to ban, e.g., 123456789012345678 | | `reason` | string | No | Reason for banning the member | @@ -830,7 +801,6 @@ Unban a member from a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `userId` | string | Yes | The user ID to unban, e.g., 123456789012345678 | | `reason` | string | No | Reason for unbanning the member | @@ -853,7 +823,6 @@ Get information about a member in a Discord server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `userId` | string | Yes | The user ID to retrieve, e.g., 123456789012345678 | @@ -883,7 +852,6 @@ Update a member in a Discord server (e.g., change nickname) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `userId` | string | Yes | The user ID to update, e.g., 123456789012345678 | | `nick` | string | No | New nickname for the member \(null to remove\) | @@ -912,7 +880,6 @@ Create an invite link for a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to create an invite for, e.g., 123456789012345678 | | `maxAge` | number | No | Duration of invite in seconds \(0 = never expires, default 86400\) | | `maxUses` | number | No | Max number of uses \(0 = unlimited, default 0\) | @@ -943,7 +910,6 @@ Get information about a Discord invite | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `inviteCode` | string | Yes | The invite code to retrieve | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -971,7 +937,6 @@ Delete a Discord invite | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `inviteCode` | string | Yes | The invite code to delete | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -993,7 +958,6 @@ Create a webhook in a Discord channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `channelId` | string | Yes | The Discord channel ID to create the webhook in, e.g., 123456789012345678 | | `name` | string | Yes | Name of the webhook \(1-80 characters\) | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -1051,7 +1015,6 @@ Get information about a Discord webhook | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `webhookId` | string | Yes | The webhook ID to retrieve, e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | @@ -1079,7 +1042,6 @@ Delete a Discord webhook | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | The bot token for authentication | | `webhookId` | string | Yes | The webhook ID to delete, e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | diff --git a/apps/docs/content/docs/integrations/docusign-service-account.mdx b/apps/docs/content/docs/integrations/docusign-service-account.mdx new file mode 100644 index 00000000000..9abaab3b28e --- /dev/null +++ b/apps/docs/content/docs/integrations/docusign-service-account.mdx @@ -0,0 +1,88 @@ +--- +title: DocuSign JWT Grant +description: Connect DocuSign to Sim with an integration key and RSA keypair that acts as one DocuSign user, instead of a personal OAuth connection +--- + +import { Callout } from '@/components/ui/callout' +import { Step, Steps } from '@/components/ui/steps' + +Connect DocuSign with JWT Grant when a workflow should keep working without anyone signing in again. Sim signs a short-lived assertion with your app's RSA private key, exchanges it for an access token that acts as one DocuSign user, and repeats that every hour. DocuSign issues no refresh token for JWT Grant, so nothing expires as long as the key and the user's consent stay in place. + +Every envelope, template, and download runs as the impersonated user, with that user's DocuSign permissions. Use a dedicated user when you can. + +## Prerequisites + +- A DocuSign account in the environment you want: a free developer account (demo) or a production account +- Admin access to **Apps and Keys** in that account, to create the integration key + +## Create the integration key [#creating-the-integration-key] + + + + In DocuSign, open the **Apps and Keys** page and add an app. Give it a name (e.g. `Sim workflows`) and click **ADD** + + + Copy the integration key DocuSign assigns (a GUID) + + + Under **RSA key pair**, click **GENERATE RSA**. Copy the private key before you close the dialog, including its `BEGIN` and `END` lines. DocuSign masks it afterwards and can't show it again + + + Under **Redirect URI**, click **ADD URI** and enter the address you open Sim at (for example `https://sim.ai`). DocuSign requires a redirect URI for the consent step; Sim never uses what it receives there + + + Leave **User Application** set to **Yes** (the default) and click **SAVE** + + + + +The private key lets anyone holding it act as any user who has consented to this integration key. Sim encrypts it at rest and never shows it again. Treat it like a password. + + +## Find the user ID [#finding-the-user-id] + +The **Apps and Keys** page shows your **User ID** and **API Account ID**, both GUIDs. To act as a different user, use that user's ID instead. Use the user ID, not an email address. + +The account ID is optional. Sim uses the user's default account unless you enter one, so you only need it when the user belongs to more than one account. + +## Add the credential to Sim [#adding-the-credential-to-sim] + + + + Open **Integrations** from your workspace sidebar + + + Search for "DocuSign" and open it, then click **Add to Sim** and choose **Add JWT Grant app** + + + Choose the **Environment** the integration key was created in: **Developer (demo)** or **Production**. A key that hasn't gone live only works in the developer environment + + + Paste the integration key, the impersonated user ID, the RSA private key, and optionally the account ID + + + Click **Add JWT Grant app**. Sim requests a token and reads the user's accounts once to check everything matches + + + +## Grant consent [#granting-consent] + +DocuSign won't issue tokens for a user until that user (or an organization admin) consents to the integration key. The first time you add the credential, Sim shows an error with the exact consent link for your integration key and environment. Open the link signed in as the impersonated user and click **Accept**. DocuSign then redirects to the redirect URI you added; the page it lands on doesn't matter, so close it and add the credential again. + +You only need to consent once per user and integration key, unless the user revokes it. + +## Use the credential in workflows [#using-the-credential-in-workflows] + +Add a DocuSign block and select the saved credential in the account dropdown. Every DocuSign operation works with it the same way as with a connected account. Sim calls the eSignature REST API for the account it found when it last requested a token, in the environment stored on the credential. + +Knowledge base syncs from DocuSign can use the credential too. They read the same account in the same environment, and envelope links open in that environment's DocuSign web app. + +## Fix connection errors + +| Error | What to check | +| ----- | ------------- | +| Needs the impersonated user's consent | Open the consent link from the error signed in as that user, accept, and add the credential again. Consent can also be revoked later, which stops runs until it is granted again | +| Couldn't authenticate | The integration key, user ID, and private key must belong to the same app, in the environment you chose. A key created in the developer environment isn't valid in production until it goes live. A private key from a deleted keypair no longer works | +| User isn't a member of the account | The account ID you entered isn't one the user belongs to. Check it, or leave it blank | + +To rotate the key, generate a new RSA keypair on the integration key, reconnect the credential in Sim with the new private key, test a workflow, then delete the old keypair. DocuSign allows up to five keypairs per integration key. See [DocuSign's JWT Grant documentation](https://developers.docusign.com/platform/auth/jwt/jwt-get-token/). diff --git a/apps/docs/content/docs/integrations/docusign.mdx b/apps/docs/content/docs/integrations/docusign.mdx index b86be40bc45..b17fd5ce500 100644 --- a/apps/docs/content/docs/integrations/docusign.mdx +++ b/apps/docs/content/docs/integrations/docusign.mdx @@ -11,8 +11,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} Use [DocuSign](https://www.docusign.com) to send envelopes for e-signature, create them from templates, inspect recipients and signing status, and download completed documents. Pending envelopes can be voided with a reason. + +Connect a DocuSign account through OAuth, or an integration key that acts as one user through [JWT Grant](/integrations/docusign-service-account). {/* MANUAL-CONTENT-END */} +Connection guides: [JWT Grant](/integrations/docusign-service-account). + ## OAuth scopes [#oauth-scopes] Sim requests these scopes when someone connects a DocuSign account. For a self-hosted deployment, register your own app with the provider using the settings below. See [Integrations and OAuth](/platform/self-hosting/integrations-oauth) for setup. diff --git a/apps/docs/content/docs/integrations/dub.mdx b/apps/docs/content/docs/integrations/dub.mdx index d4faacd9040..e519ef39f57 100644 --- a/apps/docs/content/docs/integrations/dub.mdx +++ b/apps/docs/content/docs/integrations/dub.mdx @@ -2,7 +2,7 @@ title: Dub description: Link management with Dub integration: - type: "dub_v2" + type: "dub_v3" color: "#181C1E" --- @@ -31,7 +31,6 @@ Create a new short link with Dub. Supports custom domains, slugs, UTM parameters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `url` | string | Yes | The destination URL of the short link | | `domain` | string | No | Custom domain for the short link \(defaults to dub.sh\) | | `key` | string | No | Custom slug for the short link \(randomly generated if not provided\) | @@ -97,7 +96,6 @@ Create or update a short link by its URL. If a link with the same URL already ex | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `url` | string | Yes | The destination URL of the short link | | `domain` | string | No | Custom domain for the short link \(defaults to dub.sh\) | | `key` | string | No | Custom slug for the short link \(randomly generated if not provided\) | @@ -163,7 +161,6 @@ Retrieve information about a short link by its link ID, external ID, or domain + | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `linkId` | string | No | The unique ID of the short link | | `externalId` | string | No | The external ID of the link in your database | | `domain` | string | No | The domain of the link \(use with key\) | @@ -213,7 +210,6 @@ Update an existing short link's destination URL, domain, slug, link-preview titl | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `linkId` | string | Yes | The link ID or external ID prefixed with ext_ | | `url` | string | No | New destination URL | | `domain` | string | No | New custom domain | @@ -280,7 +276,6 @@ Delete a short link by its link ID or external ID (prefixed with ext_). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `linkId` | string | Yes | The link ID or external ID prefixed with ext_ | #### Output @@ -301,7 +296,6 @@ Retrieve a paginated list of short links for the authenticated workspace. Suppor | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `domain` | string | No | Filter by domain | | `search` | string | No | Search query matched against the short link slug and destination URL | | `tagIds` | string | No | Comma-separated tag IDs to filter by | @@ -332,7 +326,6 @@ Retrieve the number of short links for the authenticated workspace, optionally f | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `domain` | string | No | Filter by domain | | `search` | string | No | Search query matched against the short link slug and destination URL | | `tagIds` | string | No | Comma-separated tag IDs to filter by | @@ -360,7 +353,6 @@ Create up to 100 short links in a single request. Returns the created links alon | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `links` | json | Yes | JSON array of link objects to create. Each object requires a "url" and may include domain, key, tagIds, and other link fields \(max 100\). | #### Output @@ -383,7 +375,6 @@ Apply the same set of field updates to up to 100 links at once, selected by link | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `linkIds` | string | No | Comma-separated link IDs to update \(max 100, takes precedence over externalIds\) | | `externalIds` | string | No | Comma-separated external IDs to update \(max 100\) | | `data` | json | Yes | JSON object of fields to apply to every selected link \(e.g. \{ "archived": true, "tagIds": \["..."\] \}\) | @@ -407,7 +398,6 @@ Delete up to 100 short links in a single request by their link IDs. Non-existing | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `linkIds` | string | Yes | Comma-separated link IDs to delete \(max 100\) | #### Output @@ -428,7 +418,6 @@ Retrieve analytics for links including clicks, leads, and sales. Supports filter | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `event` | string | No | Event type: clicks \(default\), leads, sales, or composite | | `groupBy` | string | No | Group results by: count \(default\), timeseries, countries, cities, devices, browsers, os, referers, top_links, top_urls | | `linkId` | string | No | Filter by link ID | @@ -462,7 +451,6 @@ Retrieve a paginated stream of individual click, lead, and sale events for links | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `event` | string | No | Event type: clicks \(default\), leads, or sales | | `linkId` | string | No | Filter by link ID | | `externalId` | string | No | Filter by external ID \(prefix with ext_\) | @@ -495,7 +483,6 @@ Generate a customizable QR code (PNG) for a short link, with control over size, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `url` | string | Yes | The short link URL to encode in the QR code | | `logo` | string | No | URL of a custom logo to embed in the QR code \(requires a paid Dub plan\) | | `size` | number | No | QR code size in pixels \(default: 600\) | @@ -523,7 +510,6 @@ Retrieve the custom domains registered in the workspace, so links can be created | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `archived` | boolean | No | Whether to include archived domains \(defaults to false\) | | `search` | string | No | Search by domain name | | `page` | number | No | Page number \(default: 1\) | @@ -548,7 +534,6 @@ Retrieve the tags defined in the workspace, so the right tag IDs can be assigned | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `search` | string | No | Search by tag name | | `sortBy` | string | No | Field to sort by: name \(default\) or createdAt | | `sortOrder` | string | No | Sort order: asc \(default\) or desc | @@ -574,7 +559,6 @@ Create a new tag in the workspace for organizing and filtering short links. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `name` | string | Yes | The name of the tag to create \(1-50 characters\) | | `color` | string | No | Tag color: red, yellow, green, blue, purple, brown, gray, or pink \(random if omitted\) | @@ -598,7 +582,6 @@ Retrieve the folders defined in the workspace, so the right folder ID can be use | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Dub API key | | `search` | string | No | Search by folder name | | `page` | number | No | Page number \(default: 1\) | | `pageSize` | number | No | Number of folders per page \(default: 50, max: 50\) | diff --git a/apps/docs/content/docs/integrations/dynamodb.mdx b/apps/docs/content/docs/integrations/dynamodb.mdx index 443df5f95bd..0aaae67b69a 100644 --- a/apps/docs/content/docs/integrations/dynamodb.mdx +++ b/apps/docs/content/docs/integrations/dynamodb.mdx @@ -2,7 +2,7 @@ title: Amazon DynamoDB description: Get, put, query, scan, update, and delete items in Amazon DynamoDB tables integration: - type: "dynamodb" + type: "dynamodb_v2" color: "linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -[Amazon DynamoDB](https://aws.amazon.com/dynamodb/) stores items in tables identified by primary keys. Use Get to read an item, Put to insert or replace one, Update to change attributes, and Delete to remove it. Query a table or index by key conditions; use Scan when you need to inspect items across the table. The integration also reads table metadata and uses AWS access-key credentials. +[Amazon DynamoDB](https://aws.amazon.com/dynamodb/) stores items in tables identified by primary keys. Use Get to read an item, Put to insert or replace one, Update to change attributes, and Delete to remove it. Query a table or index by key conditions; use Scan when you need to inspect items across the table. The integration also reads table metadata and signs in with a saved [AWS credential](/integrations/aws-credentials). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -30,8 +30,6 @@ Get an item from a DynamoDB table by primary key | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) | | `key` | json | Yes | Primary key of the item to retrieve \(e.g., \{"pk": "USER#123"\} or \{"pk": "ORDER#456", "sk": "ITEM#789"\}\) | | `consistentRead` | boolean | No | Use strongly consistent read | @@ -56,8 +54,6 @@ Put an item into a DynamoDB table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) | | `item` | json | Yes | Item to put into the table \(e.g., \{"pk": "USER#123", "name": "John", "email": "john@example.com"\}\) | | `conditionExpression` | string | No | Condition that must be met for the put to succeed \(e.g., "attribute_not_exists\(pk\)" to prevent overwrites\) | @@ -84,8 +80,6 @@ Query items from a DynamoDB table using key conditions | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) | | `keyConditionExpression` | string | Yes | Key condition expression \(e.g., "pk = :pk" or "pk = :pk AND sk BEGINS_WITH :prefix"\) | | `filterExpression` | string | No | Filter expression for results \(e.g., "age > :minAge AND #status = :status"\) | @@ -118,8 +112,6 @@ Scan all items in a DynamoDB table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) | | `filterExpression` | string | No | Filter expression for results \(e.g., "age > :minAge AND #status = :status"\) | | `projectionExpression` | string | No | Attributes to retrieve \(e.g., "pk, sk, #name, email"\) | @@ -150,8 +142,6 @@ Update an item in a DynamoDB table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) | | `key` | json | Yes | Primary key of the item to update \(e.g., \{"pk": "USER#123"\} or \{"pk": "ORDER#456", "sk": "ITEM#789"\}\) | | `updateExpression` | string | Yes | Update expression \(e.g., "SET #name = :name, age = :age" or "SET #count = #count + :inc"\) | @@ -179,8 +169,6 @@ Delete an item from a DynamoDB table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) | | `key` | json | Yes | Primary key of the item to delete \(e.g., \{"pk": "USER#123"\} or \{"pk": "ORDER#456", "sk": "ITEM#789"\}\) | | `conditionExpression` | string | No | Condition that must be met for the delete to succeed \(e.g., "attribute_exists\(pk\)"\) | @@ -206,8 +194,6 @@ Introspect DynamoDB to list tables or get detailed schema information for a spec | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `tableName` | string | No | Optional table name to get detailed schema \(e.g., "Users", "Orders"\). If not provided, lists all tables. | #### Output diff --git a/apps/docs/content/docs/integrations/dynatrace.mdx b/apps/docs/content/docs/integrations/dynatrace.mdx index 83bf8472778..a73b08f0671 100644 --- a/apps/docs/content/docs/integrations/dynatrace.mdx +++ b/apps/docs/content/docs/integrations/dynatrace.mdx @@ -2,7 +2,7 @@ title: Dynatrace description: Manage Dynatrace problems, metrics, entities, logs, SLOs, settings, and security integration: - type: "dynatrace" + type: "dynatrace_v2" color: "#FFFFFF" --- @@ -37,7 +37,7 @@ The environment URL is the base address of your Dynatrace environment, without t | SaaS | `https://abc12345.live.dynatrace.com` | | Managed / environment ActiveGate | `https://your-activegate:9999/e/abc12345` | -Create the token under **Access tokens** in Dynatrace, and grant only the scopes for the operations you plan to call. Each action's `apiToken` description names the scope it needs: +Create the token under **Access tokens** in Dynatrace, and grant only the scopes for the operations you plan to call. The token and environment URL are saved together on the Dynatrace credential. The scope each action needs: | Area | Read | Write | | --- | --- | --- | @@ -93,8 +93,6 @@ List Davis-detected problems in a Dynatrace environment, filtered by timeframe, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.read scope | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1d. Defaults to now-2h | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | | `problemSelector` | string | No | Problem selector, e.g. status\("open"\),severityLevel\("AVAILABILITY"\),impactLevel\("SERVICES"\) | @@ -122,8 +120,6 @@ Get the full details of a single Dynatrace problem, including root cause, affect | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.read scope | | `problemId` | string | Yes | ID of the problem \(e.g., -1234567890123456789_1700000000000V2\) | | `fields` | string | No | Comma-separated optional properties to include. Defaults to all of them: evidenceDetails, impactAnalysis, recentComments | @@ -145,8 +141,6 @@ Close a Dynatrace problem and record the closing comment. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.write scope | | `problemId` | string | Yes | ID of the problem to close | | `message` | string | Yes | Text of the closing comment | @@ -171,8 +165,6 @@ List the comments recorded on a Dynatrace problem. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.read scope | | `problemId` | string | Yes | ID of the problem whose comments should be listed | | `pageSize` | number | No | Comments per page \(max 500, default 10\) | | `nextPageKey` | string | No | Cursor for the next page. Page size is ignored when it is set | @@ -195,8 +187,6 @@ Add a comment to a Dynatrace problem. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.write scope | | `problemId` | string | Yes | ID of the problem to comment on | | `message` | string | Yes | Text of the comment | | `context` | string | No | Context of the comment, shown alongside the author \(e.g., the source system\) | @@ -221,8 +211,6 @@ Read metric data points from Dynatrace using a metric selector, with optional en | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the metrics.read scope | | `metricSelector` | string | Yes | Metric selector, up to 10 metrics comma-separated, with optional transformations after a colon \(e.g. builtin:host.cpu.usage:avg:names\) | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-2h. Defaults to now-2h | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | @@ -262,8 +250,6 @@ Discover the metrics available in a Dynatrace environment, filtered by metric se | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the metrics.read scope | | `metricSelector` | string | No | Metric selector, supporting wildcards \(e.g. builtin:host.cpu.*\) | | `text` | string | No | Free-text search across metric display names and descriptions | | `fields` | string | No | Comma-separated descriptor properties. Prefix with + to add a non-default property and - to drop a default one; metricId is always returned \(e.g. +aggregationTypes,-description\) | @@ -291,8 +277,6 @@ Get the descriptor of a single Dynatrace metric — its unit, dimensions, suppor | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the metrics.read scope | | `metricKey` | string | Yes | Metric key, optionally followed by transformation operators separated by a colon \(e.g. builtin:host.cpu.usage\) | #### Output @@ -313,8 +297,6 @@ Push custom metric data points into Dynatrace using the metric line protocol, on | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the metrics.ingest scope | | `payload` | string | Yes | Metric line protocol payload, one data point per line, max 1 MB \(e.g. cpu.temperature,dt.entity.host=HOST-06F288EE2A930951,cpu=1 55\) | #### Output @@ -347,8 +329,6 @@ List monitored entities — hosts, services, applications, Kubernetes workloads | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the entities.read scope | | `entitySelector` | string | Yes | Entity selector defining the scope, e.g. type\("HOST"\),tag\("env:prod"\) or entityId\("HOST-06F288EE2A930951"\) | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-3d. Defaults to now-3d | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | @@ -375,8 +355,6 @@ Get the properties, tags, management zones, and relationships of a single monito | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the entities.read scope | | `entityId` | string | Yes | ID of the entity \(e.g., HOST-06F288EE2A930951\) | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-3d. Defaults to now-3d | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | @@ -400,8 +378,6 @@ List the monitored entity types available in the environment, with the propertie | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the entities.read scope | | `pageSize` | number | No | Entity types per page \(max 500, default 50\) | | `nextPageKey` | string | No | Cursor for the next page. Page size is ignored when it is set | @@ -433,8 +409,6 @@ List events — deployments, availability changes, alerts, custom annotations | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the events.read scope | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1d. Defaults to now-2h | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | | `eventSelector` | string | No | Event selector, e.g. eventType\("CUSTOM_DEPLOYMENT"\),status\("OPEN"\),correlationId\("build-42"\) | @@ -460,8 +434,6 @@ Get the full details of a single Dynatrace event. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the events.read scope | | `eventId` | string | Yes | ID of the event | #### Output @@ -482,8 +454,6 @@ Push an event into Dynatrace — a deployment marker, custom annotation, or cust | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the events.ingest scope | | `eventType` | string | Yes | One of AVAILABILITY_EVENT, CUSTOM_ALERT, CUSTOM_ANNOTATION, CUSTOM_CONFIGURATION, CUSTOM_DEPLOYMENT, CUSTOM_INFO, ERROR_EVENT, MARKED_FOR_TERMINATION, PERFORMANCE_EVENT, RESOURCE_CONTENTION_EVENT, WARNING | | `title` | string | Yes | Title of the event | | `entitySelector` | string | No | Entity selector for the entities to attach the event to. Defaults to the environment entity | @@ -513,8 +483,6 @@ Search log records in Dynatrace by query and timeframe, for troubleshooting and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the logs.read scope, or storage:logs:read and storage:buckets:read on Grail | | `query` | string | No | Log search query, e.g. status="ERROR" AND dt.entity.host="HOST-06F288EE2A930951" | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1h. Defaults to now-2w | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | @@ -548,8 +516,6 @@ Push log events into Dynatrace. Accepts a single log event object or an array of | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the logs.ingest scope | | `logs` | json | Yes | A log event object, or an array of them. Recognized keys are content, timestamp, and severity; any other key becomes a custom attribute \(e.g. \[\{"content":"Deploy finished","severity":"info","service":"checkout"\}\]\) | #### Output @@ -572,8 +538,6 @@ List service-level objectives with their current attainment, error budget, and b | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the slo.read scope | | `sloSelector` | string | No | SLO selector, e.g. healthState\("WARNING"\),text\("checkout"\),problems\("true"\). Combine criteria with commas | | `from` | string | No | Start of the evaluation timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d | | `to` | string | No | End of the evaluation timeframe in the same formats as From. Defaults to now | @@ -603,8 +567,6 @@ Get a single service-level objective with its evaluated attainment, error budget | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the slo.read scope | | `sloId` | string | Yes | ID of the SLO | | `from` | string | No | Start of the evaluation timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d. Defaults to now-2w | | `to` | string | No | End of the evaluation timeframe in the same formats as From. Defaults to now | @@ -628,8 +590,6 @@ List vulnerabilities detected by Dynatrace Application Security, filtered by ris | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.read scope | | `securityProblemSelector` | string | No | Security problem selector, e.g. status\("OPEN"\),riskLevel\("CRITICAL"\),technology\("JAVA"\) | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d. Defaults to now-30d | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | @@ -656,8 +616,6 @@ Get a single vulnerability with its description, remediation guidance, affected | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.read scope | | `securityProblemId` | string | Yes | ID of the security problem | | `fields` | string | No | Comma-separated optional properties to include, each prefixed with +. Defaults to every detail property: +riskAssessment, +managementZones, +codeLevelVulnerabilityDetails, +globalCounts, +filteredCounts, +description, +remediationDescription, +events, +vulnerableComponents, +affectedEntities, +exposedEntities, +reachableDataAssets, +relatedEntities, +relatedContainerImages, +relatedAttacks, +entryPoints | | `managementZoneFilter` | string | No | Restrict the counts to management zones, e.g. names\("Production"\) | @@ -681,8 +639,6 @@ Read the Dynatrace audit log — who changed which configuration, when, and whet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the auditLogs.read scope | | `filter` | string | No | Audit log filter, e.g. eventType\("UPDATE"\),user\("someone@example.com"\),category\("CONFIG"\) | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1d. Defaults to now-2w | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | @@ -726,8 +682,6 @@ Mute a single Dynatrace vulnerability with a reason, for triaging false positive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.write scope | | `securityProblemId` | string | Yes | ID of the security problem to mute | | `reason` | string | Yes | One of CONFIGURATION_NOT_AFFECTED, FALSE_POSITIVE, IGNORE, OTHER, VULNERABLE_CODE_NOT_IN_USE | | `comment` | string | No | Explanation recorded alongside the mute | @@ -753,8 +707,6 @@ Unmute a single Dynatrace vulnerability, returning it to the active triage queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.write scope | | `securityProblemId` | string | Yes | ID of the security problem to unmute | | `reason` | string | Yes | Reason for unmuting. AFFECTED is the only value the API accepts | | `comment` | string | No | Explanation recorded alongside the unmute | @@ -780,8 +732,6 @@ Mute several Dynatrace vulnerabilities at once with a shared reason, for bulk tr | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.write scope | | `securityProblemIds` | string | Yes | Security problem IDs to mute, comma-separated or as a JSON array | | `reason` | string | Yes | One of CONFIGURATION_NOT_AFFECTED, FALSE_POSITIVE, IGNORE, OTHER, VULNERABLE_CODE_NOT_IN_USE | | `comment` | string | No | Explanation recorded against every muted problem | @@ -804,8 +754,6 @@ Unmute several Dynatrace vulnerabilities at once, returning them to active triag | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.write scope | | `securityProblemIds` | string | Yes | Security problem IDs to unmute, comma-separated or as a JSON array | | `reason` | string | Yes | Reason for unmuting. AFFECTED is the only value the API accepts | | `comment` | string | No | Explanation recorded against every unmuted problem | @@ -828,8 +776,6 @@ List the remediation items of a third-party vulnerability — the components to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.read scope | | `securityProblemId` | string | Yes | ID of the third-party security problem | | `remediationItemSelector` | string | No | Remediation item selector, e.g. vulnerabilityState\("VULNERABLE"\),muted\("false"\),exposure\("PUBLIC_NETWORK"\) | @@ -851,8 +797,6 @@ List runtime attacks Dynatrace Application Protection detected — injection att | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the attacks.read scope | | `attackSelector` | string | No | Attack selector, e.g. state\("EXPLOITED"\),attackType\("SQL_INJECTION"\),technology\("JAVA"\) | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d. Defaults to now-30d | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | @@ -879,8 +823,6 @@ Get a single attack with its entry point, payload, attacker, and the vulnerabili | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the attacks.read scope | | `attackId` | string | Yes | ID of the attack | | `fields` | string | No | Comma-separated optional properties to include, each prefixed with +. Defaults to all of them: +attackTarget, +request, +entrypoint, +vulnerability, +securityProblem, +attacker, +managementZones | @@ -902,8 +844,6 @@ List the custom tags applied to the monitored entities an entity selector matche | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the entities.read scope | | `entitySelector` | string | Yes | Entity selector for the entities to read tags from, e.g. type\("HOST"\) | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression. Defaults to now-24h | | `to` | string | No | End of the timeframe in the same formats as From. Defaults to now | @@ -926,8 +866,6 @@ Add custom tags to every monitored entity an entity selector matches. Tags drive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the entities.write scope | | `entitySelector` | string | Yes | Entity selector for the entities to tag, e.g. type\("HOST"\),tag\("env:staging"\) | | `tags` | json | Yes | Tags to add, as an array of objects with a key and an optional value \(e.g. \[\{"key":"owner","value":"platform"\},\{"key":"reviewed"\}\]\) | | `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression. Defaults to now-24h | @@ -952,8 +890,6 @@ Remove a custom tag from every monitored entity an entity selector matches. Dele | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the entities.write scope | | `entitySelector` | string | Yes | Entity selector for the entities to untag, e.g. type\("HOST"\),tag\("owner:old"\) | | `key` | string | Yes | Key of the tag to delete | | `value` | string | No | Value of the tag to delete. Omit it and set Delete All With Key to remove every value of the key | @@ -979,8 +915,6 @@ List the settings schemas available in the environment. Use it to find the schem | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the settings.read scope | | `fields` | string | No | Comma-separated fields to include: schemaId, displayName, maturity, latestSchemaVersion, multiObject, ordered, ownerBasedAccessControl | #### Output @@ -1001,8 +935,6 @@ List settings objects — the configuration behind maintenance windows, alerting | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the settings.read scope | | `schemaIds` | string | No | Comma-separated schema IDs, e.g. builtin:alerting.profile | | `scopes` | string | No | Comma-separated scopes, e.g. environment or HOST-06F288EE2A930951 | | `externalIds` | string | No | Comma-separated external IDs | @@ -1030,8 +962,6 @@ Get a single settings object with its value and its update token. Read it before | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the settings.read scope | | `objectId` | string | Yes | ID of the settings object | #### Output @@ -1052,8 +982,6 @@ Create a settings object — a maintenance window, alerting profile, management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the settings.write scope | | `schemaId` | string | Yes | Schema of the object to create, e.g. builtin:alerting.maintenance-window. List Settings Schemas returns the available IDs | | `scope` | string | Yes | Scope the object applies to, e.g. environment or an entity ID such as HOST-06F288EE2A930951 | | `value` | json | Yes | The configuration itself. Its shape is defined by the schema — read an existing object of the same schema to see the expected fields | @@ -1091,8 +1019,6 @@ Update an existing settings object. Pass the update token from Get Settings Obje | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the settings.write scope | | `objectId` | string | Yes | ID of the settings object to update | | `value` | json | Yes | The full replacement configuration. Its shape is defined by the object schema — this replaces the value rather than merging into it | | `schemaVersion` | string | No | Schema version to validate against | @@ -1118,8 +1044,6 @@ Delete a settings object. Dynatrace cannot undo this. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the settings.write scope | | `objectId` | string | Yes | ID of the settings object to delete | | `updateToken` | string | No | Token from Get Settings Object. When set, the delete fails if the object changed in the meantime | @@ -1142,8 +1066,6 @@ List synthetic monitors and their IDs. Use it to find the monitor IDs to feed in | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with one of ReadSyntheticData, DataExport, or ExternalSyntheticIntegration | | `type` | string | No | Filter by monitor type: BROWSER or HTTP | | `enabled` | boolean | No | Filter to enabled \(true\) or disabled \(false\) monitors | | `location` | string | No | Filter to monitors assigned to a synthetic location | @@ -1168,8 +1090,6 @@ Trigger an on-demand batch execution of synthetic monitors, for gating a deploy | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with syntheticExecutions.write or ExternalSyntheticIntegration | | `monitors` | json | Yes | Monitors to run, as an array of objects with monitorId and optional locations and executionCount \(e.g. \[\{"monitorId":"SYNTHETIC_TEST-123","executionCount":1\}\]\). Execution count caps at 10 | | `processingMode` | string | No | STANDARD, DISABLE_PROBLEM_DETECTION, or EXECUTIONS_DETAILS_ONLY | | `failOnPerformanceIssue` | boolean | No | Treat a performance threshold breach as a failure | @@ -1208,8 +1128,6 @@ Get the status and failures of an on-demand synthetic batch execution. Poll it a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with syntheticExecutions.read, ReadSyntheticData, or ExternalSyntheticIntegration | | `batchId` | string | Yes | Batch ID returned by Execute Synthetic Monitors | #### Output @@ -1260,8 +1178,6 @@ Get a single comment on a Dynatrace problem. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.read scope | | `problemId` | string | Yes | ID of the problem the comment belongs to | | `commentId` | string | Yes | ID of the comment | @@ -1283,8 +1199,6 @@ Replace the text of an existing comment on a Dynatrace problem. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.write scope | | `problemId` | string | Yes | ID of the problem the comment belongs to | | `commentId` | string | Yes | ID of the comment to update | | `message` | string | Yes | Replacement text of the comment | @@ -1311,8 +1225,6 @@ Delete a comment from a Dynatrace problem. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.write scope | | `problemId` | string | Yes | ID of the problem the comment belongs to | | `commentId` | string | Yes | ID of the comment to delete | @@ -1336,8 +1248,6 @@ Create a service-level objective from a metric expression, target, and evaluatio | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the slo.write scope | | `name` | string | Yes | Name of the SLO | | `target` | number | Yes | Target success rate as a percentage, e.g. 99.5 | | `warning` | number | Yes | Warning threshold as a percentage. Must sit above the target, e.g. 99.8 | @@ -1370,8 +1280,6 @@ Update an existing service-level objective. Every field is replaced, so send the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the slo.write scope | | `sloId` | string | Yes | ID of the SLO to update | | `name` | string | Yes | Name of the SLO | | `target` | number | Yes | Target success rate as a percentage, e.g. 99.5 | @@ -1405,8 +1313,6 @@ Delete a service-level objective. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) | -| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the slo.write scope | | `sloId` | string | Yes | ID of the SLO to delete | #### Output diff --git a/apps/docs/content/docs/integrations/ec2.mdx b/apps/docs/content/docs/integrations/ec2.mdx index d41a80954c8..1065a8b4368 100644 --- a/apps/docs/content/docs/integrations/ec2.mdx +++ b/apps/docs/content/docs/integrations/ec2.mdx @@ -2,7 +2,7 @@ title: Amazon EC2 description: Manage EC2 instances, Elastic IPs, VPC networking, and EBS integration: - type: "ec2" + type: "ec2_v2" color: "linear-gradient(45deg, #C8511B 0%, #FF9900 100%)" --- @@ -24,7 +24,7 @@ With the Amazon EC2 block, you can: ## Credentials and permissions -The block authenticates with an AWS access key ID and secret access key and runs in the region you set. Grant the IAM principal only the EC2 actions the operations you use require. Each operation maps to the IAM action of the same name, for example `ec2:DescribeAddresses`, `ec2:DescribeNatGateways`, and `ec2:DescribeRouteTables` for a read-only egress audit, or `ec2:StopInstances` and `ec2:StartInstances` for scheduling. +The block signs in with a saved [AWS credential](/integrations/aws-credentials) and runs in the region you set. Grant the IAM principal only the EC2 actions the operations you use require. Each operation maps to the IAM action of the same name, for example `ec2:DescribeAddresses`, `ec2:DescribeNatGateways`, and `ec2:DescribeRouteTables` for a read-only egress audit, or `ec2:StopInstances` and `ec2:StartInstances` for scheduling. List operations accept EC2 filters such as `[{"Name": "vpc-id", "Values": ["vpc-0abc123"]}]`. Without IDs they return up to 100 results per page, with a `nextToken` for the next page. @@ -33,7 +33,7 @@ In Sim, the Amazon EC2 block lets your agents map which public IPs your subnets ## Use the block [#usage-instructions] -Integrate Amazon EC2 into workflows. Describe and control instances, manage Elastic IP addresses, inspect VPCs, subnets, route tables, NAT and internet gateways, security groups, and network interfaces, add and revoke security group ingress rules, inspect network ACLs, VPC endpoints, and peering connections, work with EBS volumes and snapshots, list AMIs and key pairs, and tag resources. Requires an AWS access key with the matching ec2:* IAM permissions. +Integrate Amazon EC2 into workflows. Describe and control instances, manage Elastic IP addresses, inspect VPCs, subnets, route tables, NAT and internet gateways, security groups, and network interfaces, add and revoke security group ingress rules, inspect network ACLs, VPC endpoints, and peering connections, work with EBS volumes and snapshots, list AMIs and key pairs, and tag resources. The AWS credential’s identity needs the matching ec2:* IAM permissions. ## Actions @@ -48,8 +48,6 @@ List EC2 instances with their state, type, networking, and tags | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `instanceIds` | string | No | Comma-separated instance IDs \(e.g., i-0abc123,i-0def456\). Leave empty to list all | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"instance-state-name","Values":\["running"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -96,8 +94,6 @@ Get status checks and scheduled events for EC2 instances | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `instanceIds` | string | No | Comma-separated instance IDs \(max 100\). Leave empty to describe all | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"instance-status.status","Values":\["impaired"\]\}\] | | `includeAllInstances` | boolean | No | Include instances that are not running \(by default only running instances are returned\) | @@ -135,8 +131,6 @@ Start one or more stopped EC2 instances | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `instanceIds` | string | Yes | Comma-separated instance IDs to start | #### Output @@ -161,8 +155,6 @@ Stop one or more running EC2 instances | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `instanceIds` | string | Yes | Comma-separated instance IDs to stop | | `hibernate` | boolean | No | Hibernate the instances instead of stopping them \(hibernation must be enabled on the instance\) | | `force` | boolean | No | Force the instances to stop without flushing file system caches | @@ -189,8 +181,6 @@ Request a reboot of one or more EC2 instances | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `instanceIds` | string | Yes | Comma-separated instance IDs to reboot | #### Output @@ -213,8 +203,6 @@ Permanently terminate one or more EC2 instances | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `instanceIds` | string | Yes | Comma-separated instance IDs to terminate | #### Output @@ -239,8 +227,6 @@ List Elastic IP addresses and what they are associated with | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `publicIps` | string | No | Comma-separated Elastic IP addresses to describe | | `allocationIds` | string | No | Comma-separated allocation IDs \(e.g., eipalloc-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"domain","Values":\["vpc"\]\}\] | @@ -276,8 +262,6 @@ Allocate a new Elastic IP address for use in a VPC | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `networkBorderGroup` | string | No | Network border group to allocate from \(defaults to the region\) | | `publicIpv4Pool` | string | No | BYOIP address pool to allocate from \(defaults to the Amazon pool\) | | `tags` | json | No | Tags to apply as a JSON object, e.g. \{"Name":"web-eip"\} | @@ -305,8 +289,6 @@ Associate an Elastic IP address with an instance or network interface | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `allocationId` | string | Yes | Allocation ID of the Elastic IP \(e.g., eipalloc-0abc123\) | | `instanceId` | string | No | Instance ID to associate with \(provide this or networkInterfaceId\) | | `networkInterfaceId` | string | No | Network interface ID to associate with \(provide this or instanceId\) | @@ -332,8 +314,6 @@ Disassociate an Elastic IP address from its instance or network interface | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `associationId` | string | Yes | Association ID of the Elastic IP \(e.g., eipassoc-0abc123\) | #### Output @@ -356,8 +336,6 @@ Release an Elastic IP address back to AWS | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `allocationId` | string | Yes | Allocation ID of the Elastic IP to release | | `networkBorderGroup` | string | No | Network border group the address was allocated from | @@ -381,8 +359,6 @@ List NAT gateways with their state, subnet, and IP addresses | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `natGatewayIds` | string | No | Comma-separated NAT gateway IDs \(e.g., nat-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"vpc-id","Values":\["vpc-0abc123"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -426,8 +402,6 @@ List VPC route tables with their routes and subnet associations | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `routeTableIds` | string | No | Comma-separated route table IDs \(e.g., rtb-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"vpc-id","Values":\["vpc-0abc123"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-100, default 100\) | @@ -476,8 +450,6 @@ List VPCs with their CIDR blocks and state | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `vpcIds` | string | No | Comma-separated VPC IDs \(e.g., vpc-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"is-default","Values":\["false"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -513,8 +485,6 @@ List subnets with their CIDR, Availability Zone, and free IP count | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `subnetIds` | string | No | Comma-separated subnet IDs \(e.g., subnet-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"vpc-id","Values":\["vpc-0abc123"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -553,8 +523,6 @@ List security groups with their inbound and outbound rules | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `groupIds` | string | No | Comma-separated security group IDs \(e.g., sg-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"vpc-id","Values":\["vpc-0abc123"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -616,8 +584,6 @@ List internet gateways and the VPCs they are attached to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `internetGatewayIds` | string | No | Comma-separated internet gateway IDs \(e.g., igw-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"attachment.vpc-id","Values":\["vpc-0abc123"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -649,8 +615,6 @@ List elastic network interfaces with their IPs, attachment, and security groups | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `networkInterfaceIds` | string | No | Comma-separated network interface IDs \(e.g., eni-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"vpc-id","Values":\["vpc-0abc123"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -698,8 +662,6 @@ List EBS volumes with their size, type, state, and attachments | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `volumeIds` | string | No | Comma-separated volume IDs \(e.g., vol-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"status","Values":\["available"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -744,8 +706,6 @@ List EBS snapshots owned by the account (or by the owners you specify) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `snapshotIds` | string | No | Comma-separated snapshot IDs \(e.g., snap-0abc123\) | | `ownerIds` | string | No | Comma-separated owners: AWS account IDs, self, or amazon \(defaults to self\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"volume-id","Values":\["vol-0abc123"\]\}\] | @@ -784,8 +744,6 @@ Create a point-in-time snapshot of an EBS volume | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `volumeId` | string | Yes | ID of the EBS volume to snapshot \(e.g., vol-0abc123\) | | `description` | string | No | Description for the snapshot | | `tags` | json | No | Tags to apply as a JSON object, e.g. \{"Name":"nightly-backup"\} | @@ -817,8 +775,6 @@ Add or overwrite tags on one or more EC2 resources | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `resourceIds` | string | Yes | Comma-separated resource IDs to tag \(e.g., i-0abc123,vol-0def456\) | | `tags` | json | Yes | Tags as a JSON object, e.g. \{"Environment":"prod","Owner":"platform"\} | @@ -842,8 +798,6 @@ Remove tags from one or more EC2 resources | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `resourceIds` | string | Yes | Comma-separated resource IDs to untag | | `tagKeys` | string | Yes | Comma-separated tag keys to remove | @@ -867,8 +821,6 @@ List AWS Regions available to the account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `regionNames` | string | No | Comma-separated region names to describe | | `allRegions` | boolean | No | Include Regions that are not enabled for the account | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"opt-in-status","Values":\["opted-in"\]\}\] | @@ -895,8 +847,6 @@ List Availability Zones, Local Zones, and Wavelength Zones in the region | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `zoneNames` | string | No | Comma-separated zone names \(e.g., us-east-1a\) | | `allAvailabilityZones` | boolean | No | Include zones that are not opted in | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"state","Values":\["available"\]\}\] | @@ -928,8 +878,6 @@ Add an inbound rule to a security group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `groupId` | string | Yes | Security group ID \(e.g., sg-0abc123\) | | `ipProtocol` | string | Yes | IP protocol: tcp, udp, icmp, icmpv6, or -1 for all traffic | | `fromPort` | number | No | Start of the port range \(ICMP type for icmp; omit for -1\) | @@ -968,8 +916,6 @@ Remove inbound rules from a security group by rule ID or by matching protocol, p | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `groupId` | string | Yes | Security group ID \(e.g., sg-0abc123\) | | `ipProtocol` | string | No | IP protocol: tcp, udp, icmp, icmpv6, or -1 for all traffic | | `fromPort` | number | No | Start of the port range \(ICMP type for icmp; omit for -1\) | @@ -1008,8 +954,6 @@ List network ACLs with their inbound and outbound rules and subnet associations | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `networkAclIds` | string | No | Comma-separated network ACL IDs \(e.g., acl-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"vpc-id","Values":\["vpc-0abc123"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -1054,8 +998,6 @@ List VPC endpoints with their service, type, state, and network placement | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `vpcEndpointIds` | string | No | Comma-separated VPC endpoint IDs \(e.g., vpce-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"vpc-id","Values":\["vpc-0abc123"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -1095,8 +1037,6 @@ List VPC peering connections with their status and both VPCs | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `vpcPeeringConnectionIds` | string | No | Comma-separated VPC peering connection IDs \(e.g., pcx-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"status-code","Values":\["active"\]\}\] | | `maxResults` | number | No | Maximum results per page when no IDs are given \(5-1000, default 100\) | @@ -1137,8 +1077,6 @@ Permanently delete an EBS volume that is not attached to an instance | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `volumeId` | string | Yes | ID of the EBS volume to delete \(e.g., vol-0abc123\) | #### Output @@ -1161,8 +1099,6 @@ Permanently delete an EBS snapshot | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `snapshotId` | string | Yes | ID of the EBS snapshot to delete \(e.g., snap-0abc123\) | #### Output @@ -1185,8 +1121,6 @@ List AMIs owned by the account (or by the owners you specify) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `imageIds` | string | No | Comma-separated AMI IDs \(e.g., ami-0abc123\) | | `owners` | string | No | Comma-separated owners: AWS account IDs, self, amazon, or aws-marketplace \(defaults to self\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"name","Values":\["my-app-*"\]\}\] | @@ -1228,8 +1162,6 @@ List EC2 key pairs with their type, fingerprint, and creation time | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `keyNames` | string | No | Comma-separated key pair names | | `keyPairIds` | string | No | Comma-separated key pair IDs \(e.g., key-0abc123\) | | `filters` | json | No | EC2 filters as a JSON array, e.g. \[\{"Name":"key-name","Values":\["deploy-*"\]\}\] | diff --git a/apps/docs/content/docs/integrations/elevenlabs.mdx b/apps/docs/content/docs/integrations/elevenlabs.mdx index 2065aced76a..d35cf68d0f6 100644 --- a/apps/docs/content/docs/integrations/elevenlabs.mdx +++ b/apps/docs/content/docs/integrations/elevenlabs.mdx @@ -2,7 +2,7 @@ title: ElevenLabs description: Generate and transform audio with ElevenLabs integration: - type: "elevenlabs" + type: "elevenlabs_v2" color: "#181C1E" --- @@ -34,7 +34,6 @@ Convert text to speech using ElevenLabs voices | `modelId` | string | No | The ID of the model to use \(e.g., "eleven_multilingual_v2", "eleven_turbo_v2"\). Defaults to eleven_monolingual_v1 | | `stability` | number | No | Voice stability setting from 0.0 to 1.0 \(e.g., 0.5 for balanced, 0.75 for more stable\). Higher values produce more consistent output | | `similarityBoost` | number | No | Similarity boost setting from 0.0 to 1.0 \(e.g., 0.75 for natural, 1.0 for maximum similarity\). Higher values make the voice more similar to the original | -| `apiKey` | string | Yes | Your ElevenLabs API key | #### Output @@ -55,7 +54,6 @@ Generate a sound effect from a text prompt using ElevenLabs | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Your ElevenLabs API key | | `text` | string | Yes | The prompt describing the sound effect \(e.g., "thunder rumbling in the distance"\) | | `modelId` | string | No | The model to use \(defaults to eleven_text_to_sound_v2\) | | `durationSeconds` | number | No | Length of the sound in seconds \(0.5-30\). Omit to auto-determine | @@ -81,7 +79,6 @@ Convert audio into a chosen ElevenLabs voice while preserving content and emotio | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Your ElevenLabs API key | | `voiceId` | string | Yes | The ID of the target voice to convert the audio into | | `audioFile` | file | Yes | The source audio file to convert \(e.g., MP3, WAV, M4A\) | | `modelId` | string | No | The model to use \(defaults to eleven_english_sts_v2\) | @@ -106,7 +103,6 @@ Remove background noise from an audio file, isolating the speech using ElevenLab | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Your ElevenLabs API key | | `audioFile` | file | Yes | The audio file to isolate speech from \(e.g., MP3, WAV, M4A\) | #### Output @@ -128,7 +124,6 @@ List the voices available in your ElevenLabs account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Your ElevenLabs API key | | `search` | string | No | Search term to filter voices by name, description, labels, or category | | `category` | string | No | Filter by category: premade, cloned, generated, or professional | | `pageSize` | number | No | Number of voices to return \(1-100, default 10\) | @@ -162,7 +157,6 @@ Get metadata and settings for a specific ElevenLabs voice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Your ElevenLabs API key | | `voiceId` | string | Yes | The ID of the voice to retrieve \(e.g., "21m00Tcm4TlvDq8ikWAM"\) | #### Output @@ -192,7 +186,6 @@ Get the configured settings for a specific ElevenLabs voice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Your ElevenLabs API key | | `voiceId` | string | Yes | The ID of the voice whose settings to retrieve | #### Output @@ -217,7 +210,6 @@ Update the settings for a specific ElevenLabs voice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Your ElevenLabs API key | | `voiceId` | string | Yes | The ID of the voice to update | | `stability` | number | No | Voice stability from 0.0 to 1.0 \(default 0.5\) | | `similarityBoost` | number | No | Similarity boost from 0.0 to 1.0 \(default 0.75\) | @@ -243,7 +235,6 @@ List the models available in ElevenLabs | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Your ElevenLabs API key | #### Output @@ -273,7 +264,6 @@ Get account and subscription information for the ElevenLabs user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Your ElevenLabs API key | #### Output diff --git a/apps/docs/content/docs/integrations/emailbison.mdx b/apps/docs/content/docs/integrations/emailbison.mdx index da5b8bf350a..f0687c0e4cf 100644 --- a/apps/docs/content/docs/integrations/emailbison.mdx +++ b/apps/docs/content/docs/integrations/emailbison.mdx @@ -2,7 +2,7 @@ title: Email Bison description: Manage Email Bison leads, campaigns, replies, and tags integration: - type: "emailbison" + type: "emailbison_v2" color: "#FB7A22" --- @@ -31,8 +31,6 @@ Retrieves leads from Email Bison with optional search and tag filters. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `search` | string | No | Search term for filtering leads | | `campaignStatus` | string | No | Lead campaign status filter: in_sequence, sequence_finished, sequence_stopped, never_contacted, or replied | | `tagIds` | array | No | Tag IDs to include | @@ -70,8 +68,6 @@ Retrieves a lead by Email Bison lead ID or email address. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `leadId` | string | Yes | Lead ID or email address | #### Output @@ -105,8 +101,6 @@ Creates a single lead in Email Bison. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `firstName` | string | Yes | Lead first name | | `lastName` | string | Yes | Lead last name | | `email` | string | Yes | Lead email address | @@ -146,8 +140,6 @@ Updates an existing Email Bison lead. Fields omitted from a PUT update may be cl | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `leadId` | string | Yes | Lead ID or email address | | `firstName` | string | Yes | Lead first name | | `lastName` | string | Yes | Lead last name | @@ -188,8 +180,6 @@ Retrieves Email Bison campaigns. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | #### Output @@ -222,8 +212,6 @@ Creates a new Email Bison campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `name` | string | Yes | Campaign name | | `campaignType` | string | No | Campaign type: outbound or reply_followup | @@ -258,8 +246,6 @@ Updates Email Bison campaign settings. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `campaignId` | number | Yes | Campaign ID | | `name` | string | No | Campaign name | | `maxEmailsPerDay` | number | No | Maximum emails per day | @@ -302,8 +288,6 @@ Pauses, resumes, or archives an Email Bison campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `campaignId` | number | Yes | Campaign ID | | `action` | string | Yes | Status action: pause, resume, or archive | @@ -338,8 +322,6 @@ Adds existing Email Bison leads to a campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `campaignId` | number | Yes | Campaign ID | | `leadIds` | array | Yes | Lead IDs to add to the campaign | | `allowParallelSending` | boolean | No | Force add leads already in sequence in other campaigns | @@ -375,8 +357,6 @@ Retrieves Email Bison replies with optional status, folder, campaign, sender, le | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `search` | string | No | Search term for replies | | `status` | string | No | Reply status: interested, automated_reply, or not_automated_reply | | `folder` | string | No | Reply folder: inbox, sent, spam, bounced, or all | @@ -417,8 +397,6 @@ Retrieves all Email Bison tags for the authenticated workspace. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | #### Output @@ -451,8 +429,6 @@ Creates a new Email Bison tag. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `name` | string | Yes | Tag name | #### Output @@ -486,8 +462,6 @@ Attaches Email Bison tags to one or more leads. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Email Bison API token | -| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token | | `tagIds` | array | Yes | Tag IDs to attach | | `leadIds` | array | Yes | Lead IDs to tag | | `skipWebhooks` | boolean | No | Skip Email Bison webhooks for this action | diff --git a/apps/docs/content/docs/integrations/enrich.mdx b/apps/docs/content/docs/integrations/enrich.mdx index 99aa6d47d4b..ac35946ada4 100644 --- a/apps/docs/content/docs/integrations/enrich.mdx +++ b/apps/docs/content/docs/integrations/enrich.mdx @@ -2,7 +2,7 @@ title: Enrich description: B2B data enrichment and LinkedIn intelligence with Enrich.so integration: - type: "enrich" + type: "enrich_v2" color: "#E5E5E6" --- @@ -29,7 +29,6 @@ Check your Enrich API credit usage and remaining balance. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | #### Output @@ -51,7 +50,6 @@ Retrieve detailed LinkedIn profile information using an email address including | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `email` | string | Yes | Email address to look up \(e.g., john.doe@company.com\) | | `inRealtime` | boolean | No | Set to true to retrieve fresh data, bypassing cached information | @@ -104,7 +102,6 @@ Retrieve basic LinkedIn profile information from an email address. A lighter ver | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `email` | string | Yes | Email address to look up \(e.g., john.doe@company.com\) | #### Output @@ -144,7 +141,6 @@ Enrich a LinkedIn profile URL with detailed information including positions, edu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `url` | string | Yes | LinkedIn profile URL \(e.g., linkedin.com/in/williamhgates\) | #### Output @@ -190,7 +186,6 @@ Find a person's work email address using their full name and company domain. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `fullName` | string | Yes | Person's full name \(e.g., John Doe\) | | `companyDomain` | string | Yes | Company domain \(e.g., example.com\) | @@ -217,7 +212,6 @@ Find a work email address from a LinkedIn profile URL. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `linkedinProfile` | string | Yes | LinkedIn profile URL \(e.g., https://www.linkedin.com/in/williamhgates) | #### Output @@ -240,7 +234,6 @@ Find personal email address from a LinkedIn profile URL. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `linkedinProfile` | string | Yes | LinkedIn profile URL \(e.g., linkedin.com/in/username\) | #### Output @@ -263,7 +256,6 @@ Find a phone number from a LinkedIn profile URL. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `linkedinProfile` | string | Yes | LinkedIn profile URL \(e.g., linkedin.com/in/williamhgates\) | #### Output @@ -287,7 +279,6 @@ Find a phone number associated with an email address. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `email` | string | Yes | Email address to look up \(e.g., john.doe@example.com\) | #### Output @@ -311,7 +302,6 @@ Verify an email address for deliverability, including catch-all detection and pr | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `email` | string | Yes | Email address to verify \(e.g., john.doe@example.com\) | #### Output @@ -339,7 +329,6 @@ Check if an email address is from a disposable or temporary email provider. Retu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `email` | string | Yes | Email address to check \(e.g., john.doe@example.com\) | #### Output @@ -368,7 +357,6 @@ Discover an IP address associated with an email address. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `email` | string | Yes | Email address to look up \(e.g., john.doe@example.com\) | #### Output @@ -391,7 +379,6 @@ Identify a company from an IP address with detailed firmographic information. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `ip` | string | Yes | IP address to look up \(e.g., 86.92.60.221\) | #### Output @@ -428,7 +415,6 @@ Look up company information by name or domain, including funding, location, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `name` | string | No | Company name \(e.g., Google\) | | `domain` | string | No | Company domain \(e.g., google.com\) | @@ -474,7 +460,6 @@ Retrieve company funding history, traffic metrics, and executive information by | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `domain` | string | Yes | Company domain \(e.g., example.com\) | #### Output @@ -510,7 +495,6 @@ Retrieve company revenue data, CEO information, and competitive analysis by doma | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `domain` | string | Yes | Company domain \(e.g., clay.io\) | #### Output @@ -556,7 +540,6 @@ Search for professionals by various criteria including name, title, skills, educ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `firstName` | string | No | First name | | `lastName` | string | No | Last name | | `summary` | string | No | Professional summary keywords | @@ -610,7 +593,6 @@ Search for companies by various criteria including name, industry, location, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `name` | string | No | Company name | | `website` | string | No | Company website URL | | `tagline` | string | No | Company tagline | @@ -657,7 +639,6 @@ Search for employees within specific companies by location and job title. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `companyIds` | json | No | Array of company IDs to search within | | `country` | string | No | Country filter \(e.g., United States\) | | `city` | string | No | City filter \(e.g., San Francisco\) | @@ -696,7 +677,6 @@ Find companies similar to a given company by LinkedIn URL with filters for locat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `url` | string | Yes | LinkedIn company URL \(e.g., linkedin.com/company/google\) | | `accountLocation` | json | No | Filter by locations \(array of country names\) | | `employeeSizeType` | string | No | Employee size filter type \(e.g., RANGE\) | @@ -735,7 +715,6 @@ Advanced people search with complex filters for location, company size, seniorit | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `page` | number | Yes | Page number \(starts at 1\) | | `filters` | json | Yes | Array of filter objects. Each filter has type \(e.g., POSTAL_CODE, COMPANY_HEADCOUNT\), values \(array with id, text, selectionType: INCLUDED/EXCLUDED\), and optional selectedSubFilter | @@ -769,7 +748,6 @@ Search LinkedIn job postings by keywords with filters for location, job type, wo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `keywords` | string | Yes | Search keywords \(e.g., "software engineer"\) | | `location` | string | No | Location filter \(e.g., London\) | | `jobTypes` | string | No | Comma-separated job types \(e.g., "full time, part time"\) | @@ -808,7 +786,6 @@ Search LinkedIn posts by keywords with date filtering. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `keywords` | string | Yes | Search keywords \(e.g., "AI automation"\) | | `datePosted` | string | No | Time filter \(e.g., past_week, past_month\) | | `page` | number | No | Page number \(default: 1\) | @@ -845,7 +822,6 @@ Get detailed information about a LinkedIn post by URL. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `url` | string | Yes | LinkedIn post URL | #### Output @@ -877,7 +853,6 @@ Get reactions on a LinkedIn post with filtering by reaction type. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `postUrn` | string | Yes | LinkedIn activity URN \(e.g., urn:li:activity:7231931952839196672\) | | `reactionType` | string | Yes | Reaction type filter: all, like, love, celebrate, insightful, or funny \(default: all\) | | `page` | number | Yes | Page number \(starts at 1\) | @@ -910,7 +885,6 @@ Get reactions on a LinkedIn post by its URL, filtered by reaction type. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `postUrl` | string | Yes | LinkedIn post URL \(e.g., https://www.linkedin.com/posts/...) | | `reactionType` | string | Yes | Reaction type filter: all, like, love, celebrate, insightful, or funny \(default: all\) | | `page` | number | Yes | Page number \(starts at 1\) | @@ -943,7 +917,6 @@ Get comments on a LinkedIn post. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `postUrn` | string | Yes | LinkedIn activity URN \(e.g., urn:li:activity:7191163324208705536\) | | `page` | number | No | Page number \(starts at 1, default: 1\) | @@ -985,7 +958,6 @@ Get comments on a LinkedIn post by its URL. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `postUrl` | string | Yes | LinkedIn post URL \(e.g., https://www.linkedin.com/posts/...) | | `page` | number | No | Page number \(starts at 1, default: 1\) | @@ -1027,7 +999,6 @@ Get a person's LinkedIn activities (posts, comments, or articles) by profile ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `profileId` | string | Yes | LinkedIn profile ID | | `activityType` | string | Yes | Activity type: posts, comments, or articles | | `paginationToken` | string | No | Pagination token for next page of results | @@ -1066,7 +1037,6 @@ Get a company's LinkedIn activities (posts, comments, or articles) by company ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `companyId` | string | Yes | LinkedIn company ID | | `activityType` | string | Yes | Activity type: posts, comments, or articles | | `paginationToken` | string | No | Pagination token for next page of results | @@ -1106,7 +1076,6 @@ Convert an MD5 email hash back to the original email address and display name. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `hash` | string | Yes | MD5 hash value to look up | #### Output @@ -1130,7 +1099,6 @@ Get a company logo image URL by domain. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Enrich API key | | `url` | string | Yes | Company domain \(e.g., google.com\) | #### Output diff --git a/apps/docs/content/docs/integrations/extend.mdx b/apps/docs/content/docs/integrations/extend.mdx index cb54644bdcb..a32e17b4c72 100644 --- a/apps/docs/content/docs/integrations/extend.mdx +++ b/apps/docs/content/docs/integrations/extend.mdx @@ -2,7 +2,7 @@ title: Extend description: Parse and extract content from documents integration: - type: "extend_v2" + type: "extend_v3" color: "#000000" --- @@ -33,7 +33,6 @@ Parse and extract content from documents using Extend AI | `outputFormat` | string | No | Target output format \(markdown or spatial\). Defaults to markdown. | | `chunking` | string | No | Chunking strategy \(page, document, or section\). Defaults to page. | | `engine` | string | No | Parsing engine \(parse_performance or parse_light\). Defaults to parse_performance. | -| `apiKey` | string | Yes | Extend API key | #### Output diff --git a/apps/docs/content/docs/integrations/fathom.mdx b/apps/docs/content/docs/integrations/fathom.mdx index 2e18443faa4..92ef1a1073f 100644 --- a/apps/docs/content/docs/integrations/fathom.mdx +++ b/apps/docs/content/docs/integrations/fathom.mdx @@ -2,7 +2,7 @@ title: Fathom description: Access meeting recordings, transcripts, and summaries integration: - type: "fathom" + type: "fathom_v2" color: "#181C1E" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Fathom](https://fathom.video/) to retrieve meeting summaries and transcripts and inspect teams and team members. Provide a Fathom API key and the selected operation's required fields, such as a recording ID. Triggers can start a workflow when new meeting content is ready. +Use [Fathom](https://fathom.video/) to retrieve meeting summaries and transcripts and inspect teams and team members. Connect a saved Fathom credential (an API key) and fill in the selected operation's required fields, such as a recording ID. Triggers can start a workflow when new meeting content is ready. Pass returned meeting data to later steps, such as sending a summary to Slack or extracting action items. {/* MANUAL-CONTENT-END */} @@ -31,7 +31,6 @@ List recent meetings recorded by the user or shared to their team. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fathom API Key | | `includeSummary` | string | No | Include meeting summary \(true/false\) | | `includeTranscript` | string | No | Include meeting transcript \(true/false\) | | `includeActionItems` | string | No | Include action items \(true/false\) | @@ -129,7 +128,6 @@ List meeting types configured in your Fathom organization. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fathom API Key | | `cursor` | string | No | Pagination cursor from a previous response | #### Output @@ -154,7 +152,6 @@ Get the call summary for a specific meeting recording. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fathom API Key | | `recordingId` | string | Yes | The recording ID of the meeting | #### Output @@ -176,7 +173,6 @@ Get the full transcript for a specific meeting recording. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fathom API Key | | `recordingId` | string | Yes | The recording ID of the meeting | #### Output @@ -202,7 +198,6 @@ List team members in your Fathom organization. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fathom API Key | | `teams` | string | No | Team name to filter by | | `cursor` | string | No | Pagination cursor from a previous response | @@ -228,7 +223,6 @@ List teams in your Fathom organization. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fathom API Key | | `cursor` | string | No | Pagination cursor from a previous response | #### Output diff --git a/apps/docs/content/docs/integrations/firecrawl.mdx b/apps/docs/content/docs/integrations/firecrawl.mdx index cbd6dd32e40..f1d4a66d830 100644 --- a/apps/docs/content/docs/integrations/firecrawl.mdx +++ b/apps/docs/content/docs/integrations/firecrawl.mdx @@ -184,7 +184,7 @@ Crawl entire websites and extract structured content from all accessible pages | --------- | ---- | -------- | ----------- | | `url` | string | Yes | The website URL to crawl \(e.g., "https://example.com" or "https://docs.example.com/guide") | | `limit` | number | No | Maximum number of pages to crawl \(e.g., 50, 100, 500\). Default: 100 | -| `maxDepth` | number | No | Maximum depth to crawl from the starting URL \(e.g., 1, 2, 3\). Controls how many levels deep to follow links | +| `maxDepth` | number | No | Maximum URL path depth below the starting URL \(e.g., from /docs, 1 allows /docs/guide\). Independent of link discovery depth | | `formats` | json | No | Output formats for scraped content \(e.g., \["markdown"\], \["markdown", "html"\], \["markdown", "links"\]\) | | `prompt` | string | No | Natural-language crawl guidance supplied by existing configurations | | `excludePaths` | json | No | URL paths to exclude from crawling \(e.g., \["/blog/*", "/admin/*", "/*.pdf"\]\) | diff --git a/apps/docs/content/docs/integrations/fireflies.mdx b/apps/docs/content/docs/integrations/fireflies.mdx index 9540c7072c6..27cf424f2d7 100644 --- a/apps/docs/content/docs/integrations/fireflies.mdx +++ b/apps/docs/content/docs/integrations/fireflies.mdx @@ -2,7 +2,7 @@ title: Fireflies description: Interact with Fireflies.ai meeting transcripts and recordings integration: - type: "fireflies_v2" + type: "fireflies_v3" color: "#100730" --- @@ -31,7 +31,6 @@ List meeting transcripts from Fireflies.ai with optional filtering | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | | `keyword` | string | No | Search keyword in meeting title or transcript \(e.g., "quarterly review"\) | | `fromDate` | string | No | Filter transcripts from this date \(ISO 8601 format\) | | `toDate` | string | No | Filter transcripts until this date \(ISO 8601 format\) | @@ -59,7 +58,6 @@ Get a single transcript with full details including summary, action items, and a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | | `transcriptId` | string | Yes | The transcript ID to retrieve \(e.g., "abc123def456"\) | #### Output @@ -92,7 +90,6 @@ Get user information from Fireflies.ai. Returns current user if no ID specified. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | | `userId` | string | No | User ID to retrieve \(e.g., "user_abc123", defaults to API key owner\) | #### Output @@ -122,7 +119,6 @@ List all users within your Fireflies.ai team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | #### Output @@ -142,7 +138,6 @@ Upload an audio file URL to Fireflies.ai for transcription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | | `audioFile` | file | No | Audio/video file to upload for transcription | | `audioUrl` | string | No | Public HTTPS URL of the audio/video file \(MP3, MP4, WAV, M4A, OGG\) | | `title` | string | No | Title for the meeting/transcript | @@ -171,7 +166,6 @@ Delete a transcript from Fireflies.ai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | | `transcriptId` | string | Yes | The transcript ID to delete \(e.g., "abc123def456"\) | #### Output @@ -199,7 +193,6 @@ Add the Fireflies.ai bot to an ongoing meeting to record and transcribe | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | | `meetingLink` | string | Yes | Valid meeting URL \(Zoom, Google Meet, Microsoft Teams, etc.\) | | `title` | string | No | Title for the meeting \(max 256 characters\) | | `meetingPassword` | string | No | Password for the meeting if required \(max 32 characters\) | @@ -224,7 +217,6 @@ Create a soundbite/highlight from a specific time range in a transcript | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | | `transcriptId` | string | Yes | ID of the transcript to create the bite from \(e.g., "abc123def456"\) | | `startTime` | number | Yes | Start time of the bite in seconds | | `endTime` | number | Yes | End time of the bite in seconds | @@ -253,7 +245,6 @@ List soundbites/highlights from Fireflies.ai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | | `transcriptId` | string | No | Filter bites for a specific transcript \(e.g., "abc123def456"\) | | `mine` | boolean | No | Only return bites owned by the API key owner \(default: true\) | | `limit` | number | No | Maximum number of bites to return \(e.g., 10, max 50\) | @@ -277,7 +268,6 @@ List all contacts from your Fireflies.ai meetings | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Fireflies API key | #### Output diff --git a/apps/docs/content/docs/integrations/flint.mdx b/apps/docs/content/docs/integrations/flint.mdx index 48f581b7553..fabb4fc33c4 100644 --- a/apps/docs/content/docs/integrations/flint.mdx +++ b/apps/docs/content/docs/integrations/flint.mdx @@ -2,7 +2,7 @@ title: Flint description: Run background agent tasks on your Flint sites integration: - type: "flint" + type: "flint_v2" color: "#F6F54F" --- @@ -29,7 +29,6 @@ Start a background Flint agent task that modifies a site from a natural-language | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Flint API key \(found in Flint team settings, starts with ak_\) | | `siteId` | string | Yes | ID of the Flint site the agent should modify | | `prompt` | string | Yes | Natural-language instructions for the agent \(e.g., "Add a new About page with a team section"\) | | `callbackUrl` | string | No | HTTPS webhook URL that Flint will POST to when the task completes or fails | @@ -55,7 +54,6 @@ Start a background Flint agent task that generates up to 10 pages from a templat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Flint API key \(found in Flint team settings, starts with ak_\) | | `siteId` | string | Yes | ID of the Flint site the agent should modify | | `templatePageSlug` | string | Yes | Slug of the existing template page to generate from \(e.g., /case-studies/template\) | | `items` | array | Yes | JSON array of 1-10 pages to generate. Each item requires targetPageSlug \(slug for the new page\) and context \(content details the agent should use\). | @@ -82,7 +80,6 @@ Get the status and results of a background Flint agent task. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Flint API key \(found in Flint team settings, starts with ak_\) | | `taskId` | string | Yes | Identifier of the task returned when it was created \(e.g., bg-...\) | #### Output diff --git a/apps/docs/content/docs/integrations/gamma.mdx b/apps/docs/content/docs/integrations/gamma.mdx index e47cbfaa8b4..c261b7db2e3 100644 --- a/apps/docs/content/docs/integrations/gamma.mdx +++ b/apps/docs/content/docs/integrations/gamma.mdx @@ -2,7 +2,7 @@ title: Gamma description: Generate presentations, documents, and webpages with AI integration: - type: "gamma" + type: "gamma_v2" color: "#002253" --- @@ -29,7 +29,6 @@ Generate a new Gamma presentation, document, webpage, or social post from text i | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Gamma API key | | `inputText` | string | Yes | Text and image URLs used to generate your gamma \(1-100,000 tokens\) | | `textMode` | string | Yes | How to handle input text: generate \(AI expands\), condense \(AI summarizes\), or preserve \(keep as-is\) | | `format` | string | No | Output format: presentation, document, webpage, or social \(default: presentation\) | @@ -66,7 +65,6 @@ Generate a new Gamma by adapting an existing template with a prompt. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Gamma API key | | `gammaId` | string | Yes | The ID of the template gamma to adapt | | `prompt` | string | Yes | Instructions for how to adapt the template \(1-100,000 tokens\) | | `themeId` | string | No | Custom Gamma workspace theme ID to apply | @@ -93,7 +91,6 @@ Check the status of a Gamma generation job. Returns the gamma URL when completed | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Gamma API key | | `generationId` | string | Yes | The generation ID returned by the Generate or Generate from Template tool | #### Output @@ -122,7 +119,6 @@ List available themes in your Gamma workspace. Returns theme IDs, names, and key | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Gamma API key | | `query` | string | No | Search query to filter themes by name \(case-insensitive\) | | `limit` | number | No | Maximum number of themes to return per page \(max 50\) | | `after` | string | No | Pagination cursor from a previous response \(nextCursor\) to fetch the next page | @@ -152,7 +148,6 @@ List available folders in your Gamma workspace. Returns folder IDs and names for | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Gamma API key | | `query` | string | No | Search query to filter folders by name \(case-sensitive\) | | `limit` | number | No | Maximum number of folders to return per page \(max 50\) | | `after` | string | No | Pagination cursor from a previous response \(nextCursor\) to fetch the next page | diff --git a/apps/docs/content/docs/integrations/github.mdx b/apps/docs/content/docs/integrations/github.mdx index bb9c0cc5b8d..6d53cce14ca 100644 --- a/apps/docs/content/docs/integrations/github.mdx +++ b/apps/docs/content/docs/integrations/github.mdx @@ -2,7 +2,7 @@ title: GitHub description: Interact with GitHub or trigger workflows from GitHub events integration: - type: "github_v2" + type: "github_v3" color: "#181C1E" --- @@ -11,6 +11,8 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} [GitHub](https://github.com/) hosts repositories and their pull requests, issues, and commit history. Use this integration to read repository content, automate code-review and issue workflows, and respond to GitHub events. Select an action below for its required repository identifiers, permissions, and inputs. + +Connect GitHub with a saved personal access token: in **Integrations**, open GitHub, click **Add to Sim**, and choose **Add personal access token**. Prefer a [fine-grained token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) limited to the repositories and permissions the workflow needs; a classic token with the `repo` scope also works. Sim verifies the token against `api.github.com/user`. GitHub answers **404 Not Found** or **403 Forbidden** when a token cannot reach a repository (a fine-grained token sees only the repositories it was given, plus public ones), and an organization that requires approval leaves a new fine-grained token read-only on public repositories until an owner approves it. GitHub Enterprise Server is not supported. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -32,7 +34,6 @@ Fetch PR details including diff and files changed | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | | `pullNumber` | number | Yes | Pull request number | -| `apiKey` | string | Yes | GitHub API token | | `includeFiles` | boolean | No | Whether to fetch changed-file details from the separate files endpoint | #### Output @@ -112,7 +113,6 @@ Create comments on GitHub PRs | `path` | string | No | File path for review comment | | `commentType` | string | No | Type of comment \(pr_comment or file_comment\) | | `line` | number | No | Line number for review comment | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -148,7 +148,6 @@ Read GitHub repository metadata, including stars, forks, issues, and primary lan | --------- | ---- | -------- | ----------- | | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -204,7 +203,6 @@ Retrieve the latest commit from a GitHub repository | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `branch` | string | No | Branch name \(defaults to the repository's default branch\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -267,7 +265,6 @@ Create a comment on a GitHub issue | `repo` | string | Yes | Repository name | | `issue_number` | number | Yes | Issue number | | `body` | string | Yes | Comment content | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -307,7 +304,6 @@ List all comments on a GitHub issue | `since` | string | No | Only show comments updated after this ISO 8601 timestamp | | `per_page` | number | No | Number of results per page \(max 100\) | | `page` | number | No | Page number | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -347,7 +343,6 @@ Update an existing comment on a GitHub issue or pull request | `repo` | string | Yes | Repository name | | `comment_id` | number | Yes | Comment ID | | `body` | string | Yes | Updated comment content | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -384,7 +379,6 @@ Delete a comment on a GitHub issue or pull request | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | | `comment_id` | number | Yes | Comment ID | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -413,7 +407,6 @@ List all review comments on a GitHub pull request | `since` | string | No | Only show comments updated after this ISO 8601 timestamp | | `per_page` | number | No | Number of results per page \(max 100\) | | `page` | number | No | Page number | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -460,7 +453,6 @@ Create a new pull request in a GitHub repository | `base` | string | Yes | The name of the branch you want the changes pulled into | | `body` | string | No | Pull request description \(Markdown\) | | `draft` | boolean | No | Create as draft pull request | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -500,7 +492,6 @@ Update an existing pull request in a GitHub repository | `body` | string | No | New pull request description \(Markdown\) | | `state` | string | No | New state \(open or closed\) | | `base` | string | No | New base branch name | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -538,7 +529,6 @@ Merge a pull request in a GitHub repository | `commit_title` | string | No | Title for the merge commit | | `commit_message` | string | No | Extra detail to append to merge commit message | | `merge_method` | string | No | Merge method: merge, squash, or rebase | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -569,7 +559,6 @@ List pull requests in a GitHub repository | `direction` | string | No | Sort direction: asc or desc | | `per_page` | number | No | Results per page \(max 100\) | | `page` | number | No | Page number | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -623,7 +612,6 @@ Get the list of files changed in a pull request | `pullNumber` | number | Yes | Pull request number | | `per_page` | number | No | Results per page \(max 100\) | | `page` | number | No | Page number | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -658,7 +646,6 @@ Close a pull request in a GitHub repository | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | | `pullNumber` | number | Yes | Pull request number | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -696,7 +683,6 @@ Request reviewers for a pull request | `pullNumber` | number | Yes | Pull request number | | `reviewers` | string | No | Comma-separated list of user logins to request reviews from \(at least one of reviewers or team_reviewers is required\) | | `team_reviewers` | string | No | Comma-separated list of team slugs to request reviews from | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -728,7 +714,6 @@ Submit a review for a pull request. Use APPROVE, REQUEST_CHANGES, or COMMENT. A | `body` | string | No | The body text of the review \(required for REQUEST_CHANGES and COMMENT\) | | `commit_id` | string | No | The SHA of the commit that needs a review \(required when posting inline comments; defaults to the most recent commit otherwise\) | | `comments` | array | No | Optional inline comments with required path, body, line, and side fields | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -764,7 +749,6 @@ Get the content of a file from a GitHub repository. Supports files up to 1MB. Co | `repo` | string | Yes | Repository name | | `path` | string | Yes | Path to the file in the repository \(e.g., "src/index.ts"\) | | `ref` | string | No | Branch name, tag, or commit SHA \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -801,7 +785,6 @@ Create a new file in a GitHub repository. The file content will be automatically | `message` | string | Yes | Commit message for this file creation | | `content` | string | Yes | File content \(plain text, will be Base64 encoded automatically\) | | `branch` | string | No | Branch to create the file in \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -829,7 +812,6 @@ Update an existing file in a GitHub repository. Requires the file SHA. Content w | `content` | string | Yes | New file content \(plain text, will be Base64 encoded automatically\) | | `sha` | string | Yes | The blob SHA of the file being replaced \(get from github_get_file_content\) | | `branch` | string | No | Branch to update the file in \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -856,7 +838,6 @@ Delete a file from a GitHub repository. Requires the file SHA. This operation ca | `message` | string | Yes | Commit message for this file deletion | | `sha` | string | Yes | The blob SHA of the file being deleted \(get from github_get_file_content\) | | `branch` | string | No | Branch to delete the file from \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -881,7 +862,6 @@ Get the contents of a directory in a GitHub repository. Returns a list of files | `repo` | string | Yes | Repository name | | `path` | string | No | Directory path \(e.g., "src/components"\). Leave empty for root directory. | | `ref` | string | No | Branch name, tag, or commit SHA \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -915,7 +895,6 @@ Get the preferred README for a GitHub repository, with its content decoded to pl | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `ref` | string | No | The name of the commit/branch/tag to read the README from \(defaults to the repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -946,7 +925,6 @@ List tags for a GitHub repository. Returns tag names with their commit SHA and d | `repo` | string | Yes | Repository name | | `per_page` | number | No | Number of results per page \(max 100\) | | `page` | number | No | Page number of the results to fetch | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -979,7 +957,6 @@ List all branches in a GitHub repository. Optionally filter by protected status | `protected` | boolean | No | Filter branches by protection status | | `per_page` | number | No | Number of results per page \(max 100, default 30\) | | `page` | number | No | Page number for pagination \(default 1\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1008,7 +985,6 @@ Get detailed information about a specific branch in a GitHub repository, includi | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `branch` | string | Yes | Branch name | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1038,7 +1014,6 @@ Create a new branch in a GitHub repository by creating a git reference pointing | `repo` | string | Yes | Repository name | | `branch` | string | Yes | Name of the branch to create | | `sha` | string | Yes | Commit SHA to point the branch to | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1064,7 +1039,6 @@ Delete a branch from a GitHub repository by removing its git reference. Protecte | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `branch` | string | Yes | Name of the branch to delete | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1088,7 +1062,6 @@ Get the branch protection rules for a specific branch, including status checks, | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `branch` | string | Yes | Branch name | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1125,7 +1098,6 @@ Update branch protection rules for a specific branch, including status checks, r | `enforce_admins` | boolean | No | Whether to enforce restrictions for administrators. Omit to disable admin enforcement — GitHub receives an explicit null. | | `required_pull_request_reviews` | object | No | PR review requirements. Object with optional required_approving_review_count, dismiss_stale_reviews, require_code_owner_reviews. Omit to disable review requirements — GitHub receives an explicit null. | | `restrictions` | object | No | Push restrictions, available only for organization-owned repositories. Object with users \(string array\), teams \(string array\) and optional apps \(string array\). Omit to disable push restrictions — GitHub receives an explicit null. | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1162,7 +1134,6 @@ Create a new issue in a GitHub repository | `assignees` | string | No | Comma-separated list of usernames to assign to this issue | | `labels` | string | No | Comma-separated list of label names to add to this issue | | `milestone` | number | No | Milestone number to associate with this issue | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -1235,7 +1206,6 @@ Update an existing issue in a GitHub repository | `state` | string | No | Issue state \(open or closed\) | | `labels` | array | No | Array of label names \(replaces all existing labels\) | | `assignees` | array | No | Array of usernames \(replaces all existing assignees\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -1310,7 +1280,6 @@ List issues in a GitHub repository. Note: This includes pull requests as PRs are | `direction` | string | No | Sort direction: asc or desc \(default: desc\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -1341,7 +1310,6 @@ Get detailed information about a specific issue in a GitHub repository | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | | `issue_number` | number | Yes | Issue number | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -1416,7 +1384,6 @@ Close an issue in a GitHub repository | `repo` | string | Yes | Repository name | | `issue_number` | number | Yes | Issue number | | `state_reason` | string | No | Reason for closing: completed or not_planned | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -1469,7 +1436,6 @@ Add labels to an issue in a GitHub repository | `repo` | string | Yes | Repository name | | `issue_number` | number | Yes | Issue number | | `labels` | string | Yes | Comma-separated list of label names to add to the issue | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -1498,7 +1464,6 @@ Remove a label from an issue in a GitHub repository | `repo` | string | Yes | Repository name | | `issue_number` | number | Yes | Issue number | | `name` | string | Yes | Label name to remove | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -1527,7 +1492,6 @@ Add assignees to an issue in a GitHub repository | `repo` | string | Yes | Repository name | | `issue_number` | number | Yes | Issue number | | `assignees` | string | Yes | Comma-separated list of usernames to assign to the issue | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -1565,7 +1529,6 @@ Create a new release for a GitHub repository. Specify tag name, target commit, t | `body` | string | No | Text describing the contents of the release \(markdown supported\) | | `draft` | boolean | No | true to create a draft \(unpublished\) release, false to create a published one | | `prerelease` | boolean | No | true to identify the release as a prerelease, false to identify as a full release | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1630,7 +1593,6 @@ Update an existing GitHub release. Modify tag name, target commit, title, descri | `body` | string | No | Text describing the contents of the release \(markdown supported\) | | `draft` | boolean | No | true to set as draft, false to publish | | `prerelease` | boolean | No | true to identify as a prerelease, false for a full release | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1690,7 +1652,6 @@ List all releases for a GitHub repository. Returns release information including | `repo` | string | Yes | Repository name | | `per_page` | number | No | Number of results per page \(max 100\) | | `page` | number | No | Page number of the results to fetch | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1751,7 +1712,6 @@ Get detailed information about a specific GitHub release by ID. Returns release | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `release_id` | number | Yes | The unique identifier of the release | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1809,7 +1769,6 @@ Get the latest published, non-draft, non-prerelease release for a GitHub reposit | --------- | ---- | -------- | ----------- | | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1868,7 +1827,6 @@ Delete a GitHub release by ID. This permanently removes the release but does not | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `release_id` | number | Yes | The unique identifier of the release to delete | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1893,7 +1851,6 @@ List all workflows in a GitHub repository. Returns workflow details including ID | `repo` | string | Yes | Repository name | | `per_page` | number | No | Number of results per page \(default: 30, max: 100\) | | `page` | number | No | Page number of results to fetch \(default: 1\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1928,7 +1885,6 @@ Get details of a specific GitHub Actions workflow by ID or filename. Returns wor | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `workflow_id` | string | Yes | Workflow ID \(number\) or workflow filename \(e.g., "main.yaml"\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1963,7 +1919,6 @@ Trigger a workflow dispatch event for a GitHub Actions workflow. The workflow mu | `workflow_id` | string | Yes | Workflow ID \(number\) or workflow filename \(e.g., "main.yaml"\) | | `ref` | string | Yes | Git reference \(branch or tag name\) to run the workflow on | | `inputs` | object | No | Input keys and values configured in the workflow file | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -1994,7 +1949,6 @@ List workflow runs for a repository, or for a single workflow when a workflow ID | `status` | string | No | Filter by status \(queued, in_progress, completed, waiting, requested, pending\) | | `per_page` | number | No | Number of results per page \(default: 30, max: 100\) | | `page` | number | No | Page number of results to fetch \(default: 1\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -2021,7 +1975,6 @@ Get detailed information about a specific workflow run by ID. Returns status, co | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `run_id` | number | Yes | Workflow run ID | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -2079,7 +2032,6 @@ Cancel a workflow run. Returns 202 Accepted if cancellation is initiated, or 409 | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | | `run_id` | number | Yes | Workflow run ID to cancel | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -2104,7 +2056,6 @@ Rerun a workflow run. Optionally enable debug logging for the rerun. Returns 201 | `repo` | string | Yes | Repository name | | `run_id` | number | Yes | Workflow run ID to rerun | | `enable_debug_logging` | boolean | No | Enable debug logging for the rerun \(default: false\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | #### Output @@ -2127,7 +2078,6 @@ List GitHub Projects V2 for an organization or user. Returns up to 20 projects w | --------- | ---- | -------- | ----------- | | `owner_type` | string | Yes | Owner type: "org" for organization or "user" for user | | `owner_login` | string | Yes | Organization or user login name | -| `apiKey` | string | Yes | GitHub Personal Access Token with project read permissions | #### Output @@ -2158,7 +2108,6 @@ Get detailed information about a specific GitHub Project V2 by its number. Retur | `owner_type` | string | Yes | Owner type: "org" for organization or "user" for user | | `owner_login` | string | Yes | Organization or user login name | | `project_number` | number | Yes | Project number | -| `apiKey` | string | Yes | GitHub Personal Access Token with project read permissions | #### Output @@ -2189,7 +2138,6 @@ Create a new GitHub Project V2. Requires the owner Node ID (not login name). Ret | --------- | ---- | -------- | ----------- | | `owner_id` | string | Yes | Node ID of the owning user or organization, not a project ID or login name. Retrieve the owner id through the GitHub GraphQL API. | | `title` | string | Yes | Project title | -| `apiKey` | string | Yes | GitHub Personal Access Token with project write permissions | #### Output @@ -2220,7 +2168,6 @@ Update an existing GitHub Project V2. Can update title, description, visibility | `shortDescription` | string | No | New project short description | | `project_public` | boolean | No | Set project visibility \(true = public, false = private\) | | `closed` | boolean | No | Set project status \(true = closed, false = open\) | -| `apiKey` | string | Yes | GitHub Personal Access Token with project write permissions | #### Output @@ -2247,7 +2194,6 @@ Delete a GitHub Project V2. This action is permanent and cannot be undone. Requi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `project_id` | string | Yes | Project Node ID \(format: PVT_...\) | -| `apiKey` | string | Yes | GitHub Personal Access Token with project admin permissions | #### Output @@ -2275,7 +2221,6 @@ Search for code across GitHub repositories. Use qualifiers like repo:owner/name, | `order` | string | No | Sort order: asc or desc \(default: desc\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -2336,7 +2281,6 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us | `order` | string | No | Sort order: asc or desc \(default: desc\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -2423,7 +2367,6 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne | `order` | string | No | Sort order: asc or desc \(default: desc\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -2515,7 +2458,6 @@ Search for repositories across GitHub. Use qualifiers like language:python, star | `order` | string | No | Sort order: asc or desc \(default: desc\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -2579,7 +2521,6 @@ Search for users and organizations on GitHub. Use qualifiers like type:user, typ | `order` | string | No | Sort order: asc or desc \(default: desc\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -2627,7 +2568,6 @@ List commits in a repository with optional filtering by SHA, path, author, commi | `until` | string | No | Only commits before this date \(ISO 8601 format\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -2698,7 +2638,6 @@ Get detailed information about a specific commit including files changed and sta | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | | `ref` | string | Yes | Commit SHA, branch name, or tag name | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -2786,7 +2725,6 @@ Compare two commits or branches to see the diff, commits between them, and chang | `head` | string | Yes | Head branch/tag/SHA for comparison | | `per_page` | number | No | Results per page for files \(max 100, default: 30\) | | `page` | number | No | Page number for files \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -2914,7 +2852,6 @@ Create a new gist with one or more files | `description` | string | No | Description of the gist | | `files` | json | Yes | JSON object with filenames as keys and content as values. Example: \{"file.txt": \{"content": "Hello"\}\} | | `public` | boolean | No | Whether the gist is public \(default: false\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -2959,7 +2896,6 @@ Get a gist by ID including its file contents | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `gist_id` | string | Yes | The gist ID | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3007,7 +2943,6 @@ List gists for a user or the authenticated user | `since` | string | No | Only gists updated after this time \(ISO 8601\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3056,7 +2991,6 @@ Update a gist description or files. To delete a file, set its value to null in f | `gist_id` | string | Yes | The gist ID to update | | `description` | string | No | New description for the gist | | `files` | json | No | JSON object with filenames as keys. Set to null to delete, or provide content to update/add | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3101,7 +3035,6 @@ Delete a gist by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `gist_id` | string | Yes | The gist ID to delete | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3123,7 +3056,6 @@ Fork a gist to create your own copy | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `gist_id` | string | Yes | The gist ID to fork | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3150,7 +3082,6 @@ Star a gist | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `gist_id` | string | Yes | The gist ID to star | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3172,7 +3103,6 @@ Unstar a gist | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `gist_id` | string | Yes | The gist ID to unstar | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3198,7 +3128,6 @@ Fork a repository to your account or an organization | `organization` | string | No | Organization to fork into \(omit to fork to your account\) | | `name` | string | No | Custom name for the forked repository | | `default_branch_only` | boolean | No | Only fork the default branch \(default: false\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3254,7 +3183,6 @@ List forks of a repository | `sort` | string | No | Sort by: newest, oldest, stargazers, watchers \(default: newest\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3312,7 +3240,6 @@ Create a milestone in a repository | `state` | string | No | State: open or closed \(default: open\) | | `description` | string | No | Milestone description | | `due_on` | string | No | Due date \(ISO 8601 format, e.g., 2024-12-31T23:59:59Z\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3358,7 +3285,6 @@ Get a specific milestone by number | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | | `milestone_number` | number | Yes | Milestone number | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3408,7 +3334,6 @@ List milestones in a repository | `direction` | string | No | Sort direction: asc or desc \(default: asc\) | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3460,7 +3385,6 @@ Update a milestone in a repository | `state` | string | No | New state: open or closed | | `description` | string | No | New description | | `due_on` | string | No | New due date \(ISO 8601 format\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3506,7 +3430,6 @@ Delete a milestone from a repository | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | | `milestone_number` | number | Yes | Milestone number to delete | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3531,7 +3454,6 @@ Add a reaction to an issue | `repo` | string | Yes | Repository name | | `issue_number` | number | Yes | Issue number | | `content` | string | Yes | Reaction type: +1 \(thumbs up\), -1 \(thumbs down\), laugh, confused, heart, hooray, rocket, eyes | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3564,7 +3486,6 @@ Remove a reaction from an issue | `repo` | string | Yes | Repository name | | `issue_number` | number | Yes | Issue number | | `reaction_id` | number | Yes | Reaction ID to delete | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3589,7 +3510,6 @@ Add a reaction to an issue comment | `repo` | string | Yes | Repository name | | `comment_id` | number | Yes | Comment ID | | `content` | string | Yes | Reaction type: +1 \(thumbs up\), -1 \(thumbs down\), laugh, confused, heart, hooray, rocket, eyes | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3622,7 +3542,6 @@ Remove a reaction from an issue comment | `repo` | string | Yes | Repository name | | `comment_id` | number | Yes | Comment ID | | `reaction_id` | number | Yes | Reaction ID to delete | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3645,7 +3564,6 @@ Star a repository | --------- | ---- | -------- | ----------- | | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3669,7 +3587,6 @@ Remove star from a repository | --------- | ---- | -------- | ----------- | | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3693,7 +3610,6 @@ Check if you have starred a repository | --------- | ---- | -------- | ----------- | | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3719,7 +3635,6 @@ List users who have starred a repository | `repo` | string | Yes | Repository name | | `per_page` | number | No | Results per page \(max 100, default: 30\) | | `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | #### Output @@ -3760,7 +3675,6 @@ List one page of a pull request's review threads with their comments, plus the n | `threadsPerPage` | number | No | Review threads to fetch in this page \(1-100\) | | `commentsPerThread` | number | No | Comments to fetch per thread \(1-100\) | | `cursor` | string | No | Cursor from a previous page \(endCursor\) to continue from | -| `apiKey` | string | Yes | GitHub API token with pull request read access | #### Output diff --git a/apps/docs/content/docs/integrations/gitlab.mdx b/apps/docs/content/docs/integrations/gitlab.mdx index 7f88acc2e1b..9256c2ac0e5 100644 --- a/apps/docs/content/docs/integrations/gitlab.mdx +++ b/apps/docs/content/docs/integrations/gitlab.mdx @@ -2,7 +2,7 @@ title: GitLab description: Interact with GitLab projects, issues, merge requests, and pipelines integration: - type: "gitlab" + type: "gitlab_v2" color: "#FFFFFF" --- @@ -29,7 +29,6 @@ List GitLab projects accessible to the authenticated user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `owned` | boolean | No | Limit to projects owned by the current user | | `membership` | boolean | No | Limit to projects the current user is a member of | | `search` | string | No | Search projects by name | @@ -58,7 +57,6 @@ Get details of a specific GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) \(e.g., "namespace/project"\) | #### Output @@ -79,7 +77,6 @@ List GitLab groups accessible to the authenticated user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `owned` | boolean | No | Limit to groups owned by the current user | | `search` | string | No | Search groups by name or path | | `topLevelOnly` | boolean | No | Limit to top-level groups, excluding subgroups | @@ -110,7 +107,6 @@ Get details of a specific GitLab group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `groupId` | string | Yes | Group ID or path \(e.g. mygroup or parent/subgroup\) | #### Output @@ -131,7 +127,6 @@ List a user's project and group memberships. Requires an administrator access to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | string | Yes | The ID of the user whose memberships to list | | `membershipType` | string | No | Filter by source: 'Project' or 'Namespace' \(group\). Omit for all memberships. | | `perPage` | number | No | Number of results per page \(default 20, max 100\) | @@ -156,7 +151,6 @@ List issues in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `state` | string | No | Filter by state \(opened, closed, all\) | | `labels` | string | No | Comma-separated list of label names | @@ -187,7 +181,6 @@ Get details of a specific GitLab issue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `issueIid` | number | Yes | Issue number within the project \(the # shown in GitLab UI\) | @@ -209,7 +202,6 @@ Create a new issue in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `title` | string | Yes | Issue title | | `description` | string | No | Issue description \(Markdown supported\) | @@ -237,7 +229,6 @@ Update an existing issue in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `issueIid` | number | Yes | Issue internal ID \(IID\) | | `title` | string | No | New issue title | @@ -267,7 +258,6 @@ Delete an issue from a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `issueIid` | number | Yes | Issue internal ID \(IID\) | @@ -289,7 +279,6 @@ Add a comment to a GitLab issue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `issueIid` | number | Yes | Issue internal ID \(IID\) | | `body` | string | Yes | Comment body \(Markdown supported\) | @@ -313,7 +302,6 @@ List merge requests in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `state` | string | No | Filter by state \(opened, closed, locked, merged, all\) | | `labels` | string | No | Comma-separated list of label names | @@ -343,7 +331,6 @@ Get details of a specific GitLab merge request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | @@ -365,7 +352,6 @@ Create a new merge request in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `sourceBranch` | string | Yes | Source branch name | | `targetBranch` | string | Yes | Target branch name | @@ -396,7 +382,6 @@ Update an existing merge request in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | | `title` | string | No | New merge request title | @@ -428,7 +413,6 @@ Merge a merge request in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | | `mergeCommitMessage` | string | No | Custom merge commit message | @@ -455,7 +439,6 @@ Add a comment to a GitLab merge request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | | `body` | string | Yes | Comment body \(Markdown supported\) | @@ -479,7 +462,6 @@ List pipelines in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `ref` | string | No | Filter by ref \(branch or tag\) | | `status` | string | No | Filter by status \(created, waiting_for_resource, preparing, pending, running, success, failed, canceling, canceled, skipped, manual, scheduled, waiting_for_callback\) | @@ -507,7 +489,6 @@ Get details of a specific GitLab pipeline | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `pipelineId` | number | Yes | Pipeline ID | @@ -529,7 +510,6 @@ Trigger a new pipeline in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `ref` | string | Yes | Branch or tag to run the pipeline on | | `variables` | array | No | Array of variables for the pipeline \(each with key, value, and optional variable_type\) | @@ -553,7 +533,6 @@ Retry a failed GitLab pipeline | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `pipelineId` | number | Yes | Pipeline ID | @@ -575,7 +554,6 @@ Cancel a running GitLab pipeline | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `pipelineId` | number | Yes | Pipeline ID | @@ -597,7 +575,6 @@ List files and directories in a GitLab project repository | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `path` | string | No | Path inside the repository to list | | `ref` | string | No | Branch, tag, or commit SHA to list from | @@ -624,7 +601,6 @@ Get the contents of a file from a GitLab project repository | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `filePath` | string | Yes | Path to the file in the repository | | `ref` | string | Yes | Branch, tag, or commit SHA | @@ -654,7 +630,6 @@ Create a new file in a GitLab project repository | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `filePath` | string | Yes | Path to the file in the repository | | `branch` | string | Yes | Branch to commit the new file to | @@ -684,7 +659,6 @@ Update an existing file in a GitLab project repository | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `filePath` | string | Yes | Path to the file in the repository | | `branch` | string | Yes | Branch to commit the update to | @@ -715,7 +689,6 @@ Create a new branch in a GitLab project repository | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `branch` | string | Yes | Name of the new branch | | `ref` | string | Yes | Source branch/tag/SHA | @@ -741,7 +714,6 @@ Delete a branch from a GitLab project repository | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `branch` | string | Yes | Name of the branch to delete | @@ -763,7 +735,6 @@ Compare two branches, tags, or commits in a GitLab project repository | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `from` | string | Yes | Commit SHA or branch/tag name to compare from | | `to` | string | Yes | Commit SHA or branch/tag name to compare to | @@ -794,7 +765,6 @@ List branches in a GitLab project repository | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `search` | string | No | Filter branches by name | | `perPage` | number | No | Number of results per page \(default 20, max 100\) | @@ -819,7 +789,6 @@ List commits in a GitLab project repository | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `refName` | string | No | Branch, tag, or revision range to list commits from | | `since` | string | No | Only commits after this ISO 8601 date | @@ -848,7 +817,6 @@ Get the file changes (diffs) of a GitLab merge request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | @@ -873,7 +841,6 @@ Approve a GitLab merge request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | | `sha` | string | No | HEAD SHA of the merge request to approve | @@ -898,7 +865,6 @@ List jobs for a GitLab pipeline | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `pipelineId` | number | Yes | Pipeline ID | | `scope` | string | No | Filter jobs by scope \(e.g. created, running, success, failed\) | @@ -925,7 +891,6 @@ Get the log (trace) of a GitLab job | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `jobId` | number | Yes | Job ID | @@ -948,7 +913,6 @@ Trigger (play) a manual GitLab job | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `jobId` | number | Yes | Job ID | | `jobVariables` | array | No | Variables for the manual job \(array of objects with key and value\) | @@ -974,7 +938,6 @@ List releases in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `orderBy` | string | No | Order by field \(released_at, created_at\) | | `sort` | string | No | Sort direction \(asc, desc\) | @@ -1000,7 +963,6 @@ Create a new release in a GitLab project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or path \(e.g. mygroup/myproject\) | | `tagName` | string | Yes | The Git tag for the release | | `name` | string | No | The release name | @@ -1029,7 +991,6 @@ List members of a GitLab project or group. Includes members inherited from ances | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `directOnly` | boolean | No | When true, returns only direct members. Defaults to false, which also returns members inherited from ancestor groups. | @@ -1059,7 +1020,6 @@ Add an existing GitLab user to a project or group at a given access level | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `userId` | number | No | The ID of the user to add. Provide either userId or username. | @@ -1087,7 +1047,6 @@ Update a member's access level in a GitLab project or group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `userId` | number | Yes | The ID of the member to update | @@ -1113,7 +1072,6 @@ Remove a member from a GitLab project or group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `userId` | number | Yes | The ID of the member to remove | @@ -1138,7 +1096,6 @@ Invite a person to a GitLab project or group by email address | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `email` | string | Yes | Email address to invite \(comma-separated for multiple\) | @@ -1166,7 +1123,6 @@ List pending email invitations for a GitLab project or group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `query` | string | No | Filter invitations by invited email | @@ -1192,7 +1148,6 @@ Update a pending invitation to a GitLab project or group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `email` | string | Yes | Email address of the invitation to update | @@ -1217,7 +1172,6 @@ Revoke a pending email invitation to a GitLab project or group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `email` | string | Yes | Email address of the invitation to revoke | @@ -1240,7 +1194,6 @@ List pending access requests for a GitLab project or group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `perPage` | number | No | Number of results per page \(default 20, max 100\) | @@ -1265,7 +1218,6 @@ Approve a pending access request for a GitLab project or group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `userId` | number | Yes | The user ID of the access requester | @@ -1289,7 +1241,6 @@ Deny (delete) a pending access request for a GitLab project or group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `resourceType` | string | Yes | Whether the resource is a 'project' or a 'group' | | `resourceId` | string | Yes | Project or group ID or path \(e.g. mygroup/myproject\) | | `userId` | number | Yes | The user ID of the access requester | @@ -1312,7 +1263,6 @@ List SAML group links for a GitLab group. Use this to detect whether a group is | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `groupId` | string | Yes | Group ID or path \(e.g. my-org/my-group\) | #### Output @@ -1334,7 +1284,6 @@ Search for GitLab users by name, username, or email. Email matches must be exact | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `search` | string | Yes | Name, username, or email to search for | | `perPage` | number | No | Number of results per page \(default 20, max 100\) | | `page` | number | No | Page number for pagination | @@ -1358,7 +1307,6 @@ Create a new GitLab user. Requires an administrator token with admin_mode on the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `email` | string | Yes | The user's email address | | `username` | string | Yes | The user's username | | `name` | string | Yes | The user's display name | @@ -1386,7 +1334,6 @@ Modify an existing GitLab user. Requires an administrator token with admin_mode | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to modify | | `email` | string | No | The user's new email address \(GitLab only allows changing to one of the user's existing verified secondary emails\) | | `username` | string | No | The user's new username | @@ -1411,7 +1358,6 @@ Delete a GitLab user. Requires an administrator token with admin_mode on the ins | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to delete | | `hardDelete` | boolean | No | When true, contributions, personal projects, AND groups owned solely by this user are deleted rather than moved to a Ghost User | @@ -1433,7 +1379,6 @@ Block a GitLab user, preventing them from signing in or accessing the instance | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to act on | #### Output @@ -1511,7 +1456,6 @@ Unblock a previously blocked GitLab user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to act on | #### Output @@ -1589,7 +1533,6 @@ Deactivate a dormant GitLab user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to act on | #### Output @@ -1667,7 +1610,6 @@ Reactivate a deactivated GitLab user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to act on | #### Output @@ -1745,7 +1687,6 @@ Ban a GitLab user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to act on | #### Output @@ -1823,7 +1764,6 @@ Unban a previously banned GitLab user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to act on | #### Output @@ -1901,7 +1841,6 @@ Approve a GitLab user whose signup is pending administrator approval | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to act on | #### Output @@ -1979,7 +1918,6 @@ Reject a GitLab user whose signup is pending administrator approval | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user to act on | #### Output @@ -2057,7 +1995,6 @@ Delete a user's authentication identity (e.g. SAML or LDAP). Requires an adminis | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `userId` | number | Yes | The ID of the user | | `provider` | string | Yes | The external identity provider name \(e.g. saml, ldapmain\) | @@ -2079,7 +2016,6 @@ Add a SAML group link that maps an identity-provider group to a GitLab group at | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `groupId` | string | Yes | Group ID or path \(e.g. my-org/my-group\) | | `samlGroupName` | string | Yes | The name of the SAML group as sent by the identity provider | | `accessLevel` | number | Yes | Access level granted to members of the SAML group: 10 \(Guest\), 15 \(Planner\), 20 \(Reporter\), 25 \(Security Manager\), 30 \(Developer\), 40 \(Maintainer\), 50 \(Owner\) | @@ -2104,7 +2040,6 @@ Delete a SAML group link from a GitLab group (GitLab Premium/Ultimate) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `groupId` | string | Yes | Group ID or path \(e.g. my-org/my-group\) | | `samlGroupName` | string | Yes | The name of the SAML group link to delete | | `provider` | string | No | Provider name of the link to delete. Required when multiple links share the same SAML group name. | diff --git a/apps/docs/content/docs/integrations/google-service-account.mdx b/apps/docs/content/docs/integrations/google-service-account.mdx index 5e31e7f8678..5a2e8d86514 100644 --- a/apps/docs/content/docs/integrations/google-service-account.mdx +++ b/apps/docs/content/docs/integrations/google-service-account.mdx @@ -11,6 +11,8 @@ Google service accounts can access shared resources directly or impersonate a Go For example, you could build a workflow that iterates through a list of employees, impersonates each one to read their Google Docs, and uploads the contents to a shared knowledge base — all without requiring any of those users to sign in. +For Google Cloud APIs such as BigQuery and Vertex AI, you can skip the JSON key entirely: see [Keyless Google Cloud access](#keyless-google-cloud-access). + ## Prerequisites Create the service account and enable its APIs in Google Cloud. Configure the Google Workspace Admin Console only when using domain-wide delegation. @@ -172,3 +174,71 @@ Leave **Impersonated Account** blank to authenticate as the service account itse Domain-wide delegation requires a Google Workspace super administrator. It authorizes impersonation within that Workspace domain, subject to the authorized scopes and the impersonated user’s access. Consumer Gmail accounts and users outside that domain cannot be impersonated. Access also depends on the license required by the target service. See [Google’s service-account authorization guide](https://developers.google.com/identity/protocols/oauth2/service-account). A credential can serve multiple supported Google services when their APIs and scopes are configured. To rotate its key, create a replacement JSON key, update the credential in Sim, verify access, then delete the old key in Google Cloud. + +## Keyless Google Cloud access [#keyless-google-cloud-access] + +A Google service account credential can also be **keyless**: Sim stores no key. Instead, your Google Cloud project trusts Sim through [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers). On each run, Sim signs a short-lived token for this credential's own subject, exchanges it at Google's Security Token Service for a federated token, and uses that to impersonate your service account. The service account's IAM roles decide what workflows can do, exactly as with a JSON key. + + +Keyless credentials work with Google Cloud APIs that the service account calls as itself: **BigQuery**, and **Vertex AI** wherever a Google service account credential is accepted. They can't use domain-wide delegation, so Gmail, Drive, Sheets, Calendar, and other Google Workspace blocks don't offer them, and a keyless credential refuses an **Impersonated Account**. Use a JSON key for those. + + + + + In the [Google Cloud Console](https://console.cloud.google.com/), enable the IAM, Resource Manager, Service Account Credentials, and Security Token Service APIs in the project that will hold the workload identity pool, and note its **project number** (on the project's Dashboard; it's numeric, not the project ID). + + + Create or pick a service account, and grant it the roles your workflows need (for example, BigQuery Data Viewer and BigQuery Job User, or Vertex AI User). No key is needed. + + + In Sim, open the **Google BigQuery** integration, choose **Add service account**, and set **Authentication** to **Workload Identity Federation (keyless, via Sim OIDC)**. Enter the project number, a pool ID and provider ID (4 to 32 lowercase letters, digits, or hyphens, not starting with `gcp-`), and the service account email (`name@project-id.iam.gserviceaccount.com`; user-managed service account only, so default accounts such as `…@developer.gserviceaccount.com` are refused). + + + The dialog shows the **Issuer**, **Audience**, and **Subject**, and three `gcloud` commands with your values filled in. Run them in that project. They run against the project number you entered and create the pool, an OIDC provider for Sim's issuer with the attribute mapping `google.subject=assertion.sub` and an attribute condition that accepts only this credential's subject, and grant that subject `roles/iam.workloadIdentityUser` on the service account. If the pool already exists, skip the first command: + + ```bash + gcloud iam workload-identity-pools create POOL_ID \ + --project="PROJECT_NUMBER" \ + --location="global" \ + --display-name="Sim" + + gcloud iam workload-identity-pools providers create-oidc PROVIDER_ID \ + --project="PROJECT_NUMBER" \ + --location="global" \ + --workload-identity-pool="POOL_ID" \ + --issuer-uri="ISSUER" \ + --attribute-mapping="google.subject=assertion.sub" \ + --attribute-condition="assertion.sub=='SUBJECT'" + + gcloud iam service-accounts add-iam-policy-binding SERVICE_ACCOUNT_EMAIL \ + --project="PROJECT_NUMBER" \ + --role="roles/iam.workloadIdentityUser" \ + --member="principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/subject/SUBJECT" + ``` + + The provider sets no allowed audiences, so it accepts its own resource name, which is the **Audience** the dialog shows. + + + Save in Sim. Sim exchanges a token at STS and impersonates the service account; a token from that step proves the setup works. It also checks that a subject no credential holds can't impersonate the service account, so only this credential can use it. New IAM bindings can take a few minutes to apply; if saving fails right after you run the commands, wait and try again. + + + + +Keep the attribute condition and bind the role to the exact `principal://…/subject/SUBJECT` member. Without the condition, the provider accepts a token for any Sim subject, and a role granted to a `principalSet` for the whole pool would let other Sim credentials act with it. Sim refuses to save a service account that another subject can impersonate, but it can't see roles you grant the pool directly on other resources. + + +Each keyless credential has its own subject; reconnecting keeps it by default so your provider and binding keep working. Use one provider per credential (a pool can hold many). Keyless credentials are available for workspace credentials, not organization credentials. Sim uses impersonation rather than direct federated access because some Google Cloud APIs don't accept federated tokens, and impersonation keeps the same identity a JSON key would use. + +Self-hosted deployments need `WORKLOAD_IDENTITY_SIGNING_KEY` set and an `https` issuer Google can reach over the public internet. See [AWS credentials](/integrations/aws-credentials#self-hosted-deployments). + +### From the API or CLI [#keyless-api-cli] + +Run `sim credentials keyless-subject google-service-account`, trust the printed subject as above, then add `projectNumber`, `poolId`, `providerId`, and `serviceAccountEmail` to its `keylessConfig` and pass it to `sim credentials create google-service-account`. See [the CLI credentials reference](/cli/credentials) for the full two-step example; the proof expires after 24 hours and works only for the user who requested it. As in the dialog, keyless works only for Google Cloud services such as BigQuery and Vertex AI, not Google Workspace blocks. + +### Troubleshooting keyless access + +- **"Google Cloud refused Sim's token for this workload identity provider":** the project number, pool ID, or provider ID doesn't match; the provider is disabled, has allowed audiences set, or uses a different issuer; or its attribute condition doesn't allow this credential's subject. +- **"Google Cloud could not fetch the signing keys from Sim's issuer":** the issuer isn't reachable over public HTTPS (self-hosted deployments). +- **"The service account … does not let this credential impersonate it":** run the `add-iam-policy-binding` command with the exact member shown, and enable the IAM Service Account Credentials API. +- **"This service account also trusts other Sim credentials":** remove any `principalSet` binding for the pool from the service account and keep the attribute condition. +- **A block fails with "This credential cannot be used here":** the block is a Google Workspace block or sets an **Impersonated Account**. Use a JSON key credential for it. diff --git a/apps/docs/content/docs/integrations/grafana.mdx b/apps/docs/content/docs/integrations/grafana.mdx index 9facac4c35e..797402593d4 100644 --- a/apps/docs/content/docs/integrations/grafana.mdx +++ b/apps/docs/content/docs/integrations/grafana.mdx @@ -2,7 +2,7 @@ title: Grafana description: Interact with Grafana dashboards, alerts, and annotations integration: - type: "grafana" + type: "grafana_v2" color: "#FFFFFF" --- @@ -29,8 +29,6 @@ Get a dashboard by its UID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `dashboardUid` | string | Yes | The UID of the dashboard to retrieve \(e.g., abc123def\) | @@ -53,8 +51,6 @@ Search and list all dashboards | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `query` | string | No | Search query to filter dashboards by title | | `tag` | string | No | Filter by tag \(comma-separated for multiple tags\) | @@ -88,8 +84,6 @@ Create a new dashboard | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `title` | string | Yes | The title of the new dashboard | | `folderUid` | string | No | The UID of the folder to create the dashboard in \(e.g., folder-abc123\) | @@ -123,8 +117,6 @@ Update an existing dashboard. Fetches the current dashboard and merges your chan | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `dashboardUid` | string | Yes | The UID of the dashboard to update \(e.g., abc123def\) | | `title` | string | No | New title for the dashboard | @@ -159,8 +151,6 @@ Delete a dashboard by its UID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `dashboardUid` | string | Yes | The UID of the dashboard to delete \(e.g., abc123def\) | @@ -184,8 +174,6 @@ List all alert rules in the Grafana instance | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | #### Output @@ -226,8 +214,6 @@ Get a specific alert rule by its UID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `alertRuleUid` | string | Yes | The UID of the alert rule to retrieve | @@ -268,8 +254,6 @@ Create a new alert rule | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `title` | string | Yes | The title of the alert rule | | `folderUid` | string | Yes | The UID of the folder to create the alert in \(e.g., folder-abc123\) | @@ -326,8 +310,6 @@ Update an existing alert rule. Fetches the current rule and merges your changes. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `alertRuleUid` | string | Yes | The UID of the alert rule to update | | `title` | string | No | New title for the alert rule | @@ -384,8 +366,6 @@ Delete an alert rule by its UID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `alertRuleUid` | string | Yes | The UID of the alert rule to delete | @@ -407,8 +387,6 @@ List all alert notification contact points | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `name` | string | No | Filter contact points by exact name match | @@ -436,8 +414,6 @@ Create a notification contact point (e.g., Slack, email, PagerDuty) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `name` | string | Yes | Name of the contact point \(groups receivers shown in the UI\) | | `type` | string | Yes | Receiver type \(e.g., slack, email, pagerduty, webhook\) | @@ -468,8 +444,6 @@ Create an annotation on a dashboard or as a global annotation | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `text` | string | Yes | The text content of the annotation | | `tags` | string | No | Comma-separated list of tags | @@ -497,8 +471,6 @@ Query annotations by time range, dashboard, or tags | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `from` | number | No | Start time in epoch milliseconds \(e.g., 1704067200000\) | | `to` | number | No | End time in epoch milliseconds \(e.g., 1704153600000\) | @@ -544,8 +516,6 @@ Update an existing annotation | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `annotationId` | number | Yes | The ID of the annotation to update | | `text` | string | No | New text content for the annotation \(PATCH supports partial updates\) | @@ -572,8 +542,6 @@ Delete an annotation by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `annotationId` | number | Yes | The ID of the annotation to delete | @@ -595,8 +563,6 @@ List all data sources configured in Grafana | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | #### Output @@ -635,8 +601,6 @@ Get a data source by its ID or UID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `dataSourceId` | string | Yes | The UID of the data source to retrieve \(e.g., P1234AB5678\). Numeric ids are not supported — Grafana serves those only behind a disabled-by-default feature toggle | @@ -675,8 +639,6 @@ Test connectivity to a data source by its UID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `dataSourceUid` | string | Yes | The UID of the data source to health-check \(e.g., P1234AB5678\) | @@ -700,8 +662,6 @@ List all folders in Grafana | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `limit` | number | No | Maximum number of folders to return | | `page` | number | No | Page number for pagination | @@ -740,8 +700,6 @@ Create a new folder in Grafana | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `title` | string | Yes | The title of the new folder | | `uid` | string | No | Optional UID for the folder \(auto-generated if not provided\) | @@ -779,8 +737,6 @@ Get a folder by its UID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `folderUid` | string | Yes | The UID of the folder to retrieve \(e.g., folder-abc123\) | @@ -816,8 +772,6 @@ Update (rename) a folder. Fetches the current folder and merges your changes. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `folderUid` | string | Yes | The UID of the folder to update \(e.g., folder-abc123\) | | `title` | string | Yes | New title for the folder | @@ -854,8 +808,6 @@ Delete a folder by its UID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `folderUid` | string | Yes | The UID of the folder to delete \(e.g., folder-abc123\) | | `forceDeleteRules` | boolean | No | Delete any alert rules stored in the folder along with it \(default false\) | @@ -880,8 +832,6 @@ Check the health of the Grafana instance (version, database status) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | #### Output @@ -904,8 +854,6 @@ Replace a contact point by its UID. Grafana has no partial update for contact po | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `contactPointUid` | string | Yes | UID of the contact point to replace | | `name` | string | Yes | Contact point name. Grafana groups receivers that share a name | @@ -933,8 +881,6 @@ Permanently delete a contact point by its UID. Grafana refuses the delete while | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `contactPointUid` | string | Yes | UID of the contact point to delete | @@ -957,8 +903,6 @@ Move a folder under a different parent folder, or to the root by leaving the par | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `folderUid` | string | Yes | UID of the folder to move | | `parentUid` | string | No | UID of the new parent folder. Leave empty to move the folder to the root | @@ -995,8 +939,6 @@ Read an alert rule group: its evaluation interval and every rule in it. The inte | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `folderUid` | string | Yes | UID of the folder holding the rule group | | `ruleGroup` | string | Yes | Name of the rule group | @@ -1042,8 +984,6 @@ Run one or more queries against a Grafana data source that has a backend impleme | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `queries` | string | Yes | JSON array of at least one query. Each needs a datasource.uid and a refId, plus the fields that data source expects — expr for Prometheus, rawSql for SQL. Example: \[\{"refId":"A","datasource":\{"uid":"P123"\},"expr":"up","format":"time_series"\}\] | | `from` | string | No | Start of the time range, either epoch milliseconds or Grafana relative time \(e.g., now-5m\). Defaults to now-1h | diff --git a/apps/docs/content/docs/integrations/grain.mdx b/apps/docs/content/docs/integrations/grain.mdx index 99104201be1..19f1826bb21 100644 --- a/apps/docs/content/docs/integrations/grain.mdx +++ b/apps/docs/content/docs/integrations/grain.mdx @@ -2,7 +2,7 @@ title: Grain description: Access meeting recordings, transcripts, and AI summaries integration: - type: "grain_v2" + type: "grain_v3" color: "#F6FAF9" --- @@ -31,7 +31,6 @@ List recordings from Grain with optional filters and pagination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) | | `cursor` | string | No | Pagination cursor for next page \(returned from previous response\) | | `beforeDatetime` | string | No | Only recordings before this ISO8601 timestamp \(e.g., "2024-01-15T00:00:00Z"\) | | `afterDatetime` | string | No | Only recordings after this ISO8601 timestamp \(e.g., "2024-01-01T00:00:00Z"\) | @@ -75,7 +74,6 @@ Get details of a single recording by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) | | `recordingId` | string | Yes | The recording UUID \(e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"\) | | `includeHighlights` | boolean | No | Include highlights/clips | | `includeParticipants` | boolean | No | Include participant list | @@ -119,7 +117,6 @@ Get the full transcript of a recording | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) | | `recordingId` | string | Yes | The recording UUID \(e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"\) | #### Output @@ -145,7 +142,6 @@ List all teams in the workspace | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) | #### Output @@ -167,7 +163,6 @@ List all meeting types in the workspace | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) | #### Output @@ -190,7 +185,6 @@ Create a webhook for a specific Grain event type (v2 API) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grain API key \(Personal or Workspace Access Token\) | | `hookUrl` | string | Yes | Webhook endpoint URL. Grain performs a reachability test on creation — the endpoint must respond 2xx. | | `hookType` | string | Yes | Event type the hook subscribes to. One of: recording_added, recording_updated, recording_deleted, highlight_added, highlight_updated, highlight_deleted, story_added, story_updated, story_deleted, upload_status | | `include` | json | No | Optional include object controlling payload richness. For recording hooks: \{"participants": true, "highlights": true, "ai_summary": true\}. For highlight hooks: \{"transcript": true, "speakers": true\}. | @@ -218,7 +212,6 @@ List webhooks for the account (v2 API) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grain API key \(Personal or Workspace Access Token\) | | `hookType` | string | No | Only return hooks with this event type. One of: recording_added, recording_updated, recording_deleted, highlight_added, highlight_updated, highlight_deleted, story_added, story_updated, story_deleted, upload_status | | `state` | string | No | Only return hooks that are "enabled" or "disabled" | @@ -246,7 +239,6 @@ Delete a webhook by ID (v2 API) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grain API key \(Personal or Workspace Access Token\) | | `hookId` | string | Yes | The hook UUID to delete \(e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"\) | #### Output diff --git a/apps/docs/content/docs/integrations/granola.mdx b/apps/docs/content/docs/integrations/granola.mdx index 483206f7571..70a629b17ca 100644 --- a/apps/docs/content/docs/integrations/granola.mdx +++ b/apps/docs/content/docs/integrations/granola.mdx @@ -2,7 +2,7 @@ title: Granola description: Access meeting notes, transcripts, and audit events from Granola integration: - type: "granola" + type: "granola_v2" color: "#B2C147" --- @@ -31,7 +31,6 @@ Lists meeting notes from Granola with optional date filters and pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Granola API key | | `createdBefore` | string | No | Return notes created before this date \(ISO 8601\) | | `createdAfter` | string | No | Return notes created after this date \(ISO 8601\) | | `updatedAfter` | string | No | Return notes updated after this date \(ISO 8601\) | @@ -65,7 +64,6 @@ Retrieves a specific meeting note from Granola by ID, including summary, attende | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Granola API key | | `noteId` | string | Yes | The note ID \(e.g., not_1d3tmYTlCICgjy\) | | `includeTranscript` | string | No | Whether to include the meeting transcript | @@ -115,7 +113,6 @@ Retrieves a meeting transcript from Granola one page at a time, including when G | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Granola API key | | `noteId` | string | Yes | The note ID \(e.g., not_1d3tmYTlCICgjy\) | | `cursor` | string | No | Pagination cursor from a previous response | | `pageSize` | number | No | Number of transcript items per page \(1-100, default 50\) | @@ -147,7 +144,6 @@ Lists folders from Granola, sorted alphabetically, with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Granola API key | | `cursor` | string | No | Pagination cursor from a previous response | | `pageSize` | number | No | Number of folders per page \(1-30, default 10\) | @@ -174,7 +170,6 @@ Lists workspace audit events from Granola, with optional action and date filters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Granola API key | | `action` | string | No | Return only events with this exact action, or actions beginning with it followed by a dot \(e.g., "workspace" matches workspace.member_added\). Lowercase. | | `occurredAfter` | string | No | Return events that occurred after this date \(ISO 8601\). Must fall within the one-year retention window. | | `occurredBefore` | string | No | Return events that occurred before this date \(ISO 8601\). Must fall within the one-year retention window. | @@ -212,7 +207,6 @@ Registers an HTTPS URL in Granola to receive note event deliveries. The signing | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Granola API key | | `url` | string | Yes | The publicly reachable HTTPS URL to deliver events to. Private network addresses are rejected. | | `scopes` | string | Yes | Which notes to receive events for, comma-separated: personal, public. With a workspace API key pass exactly "workspace". | | `events` | string | No | Event names to subscribe to, comma-separated: note.generated, note.edited, note.access_granted. Omit to subscribe to all events. | @@ -246,7 +240,6 @@ Lists the Granola webhook endpoints the API key can manage. A personal key sees | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Granola API key | #### Output @@ -276,7 +269,6 @@ Updates a Granola webhook endpoint. Each supplied field replaces its current val | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Granola API key | | `webhookEndpointId` | string | Yes | The webhook endpoint ID \(e.g., whe_2mKr8fQxLp7Ta3\) | | `url` | string | No | New HTTPS URL to deliver events to. Omit to leave unchanged. | | `scopes` | string | No | Replacement scopes, comma-separated: personal, public. Omit to leave unchanged. A workspace-managed endpoint accepts only "workspace". | @@ -311,7 +303,6 @@ Deletes a Granola webhook endpoint by ID, stopping its event deliveries immediat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Granola API key | | `webhookEndpointId` | string | Yes | The webhook endpoint ID \(e.g., whe_2mKr8fQxLp7Ta3\) | #### Output diff --git a/apps/docs/content/docs/integrations/hex.mdx b/apps/docs/content/docs/integrations/hex.mdx index 699e7f231a7..1e8c22216f9 100644 --- a/apps/docs/content/docs/integrations/hex.mdx +++ b/apps/docs/content/docs/integrations/hex.mdx @@ -2,7 +2,7 @@ title: Hex description: Run and manage Hex projects integration: - type: "hex" + type: "hex_v2" color: "#14151A" --- @@ -15,7 +15,7 @@ Use [Hex](https://hex.tech/) to run analytics projects, inspect results and run ## Use the block [#usage-instructions] -Run projects, check run status, manage collections and groups (including membership and deactivating users), list users, and view data connections. Requires a Hex API token. +Run projects, check run status, manage collections and groups (including membership and deactivating users), list users, and view data connections. ## Actions @@ -29,7 +29,6 @@ Cancel an active Hex project run. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `projectId` | string | Yes | The UUID of the Hex project | | `runId` | string | Yes | The UUID of the run to cancel | @@ -53,7 +52,6 @@ Create a new collection in the Hex workspace to organize projects. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `name` | string | Yes | Name for the new collection | | `description` | string | No | Optional description for the collection | @@ -80,7 +78,6 @@ Create a new group in the Hex workspace, optionally with initial members. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `name` | string | Yes | Name for the new group | | `memberUserIds` | json | No | JSON array of user UUIDs to add as initial group members \(e.g., \["uuid1", "uuid2"\]\) | @@ -104,7 +101,6 @@ Deactivate a user in the Hex workspace. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `userId` | string | Yes | The UUID of the user to deactivate | #### Output @@ -126,7 +122,6 @@ Delete a group from the Hex workspace. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `groupId` | string | Yes | The UUID of the group to delete | #### Output @@ -148,7 +143,6 @@ Retrieve details for a specific Hex collection by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `collectionId` | string | Yes | The UUID of the collection | #### Output @@ -174,7 +168,6 @@ Retrieve details for a specific data connection including type, description, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `dataConnectionId` | string | Yes | The UUID of the data connection | #### Output @@ -201,7 +194,6 @@ Retrieve details for a specific Hex group. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `groupId` | string | Yes | The UUID of the group | #### Output @@ -224,7 +216,6 @@ Get metadata and details for a specific Hex project by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `projectId` | string | Yes | The UUID of the Hex project | #### Output @@ -262,7 +253,6 @@ Retrieve API-triggered runs for a Hex project with optional filtering by status | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `projectId` | string | Yes | The UUID of the Hex project | | `limit` | number | No | Maximum number of runs to return \(1-100, default: 25\) | | `offset` | number | No | Offset for paginated results \(default: 0\) | @@ -300,7 +290,6 @@ Return the warehouse tables queried by a Hex project, including data connection | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `projectId` | string | Yes | The UUID of the Hex project | | `limit` | number | No | Maximum number of tables to return \(1-100\) | @@ -326,7 +315,6 @@ Check the status of a Hex project run by its run ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `projectId` | string | Yes | The UUID of the Hex project | | `runId` | string | Yes | The UUID of the run to check | @@ -356,7 +344,6 @@ List all collections in the Hex workspace. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `limit` | number | No | Maximum number of collections to return \(1-500, default: 25\) | | `sortBy` | string | No | Sort by field: NAME | | `after` | string | No | Cursor to fetch the page of results after this value | @@ -389,7 +376,6 @@ List all data connections in the Hex workspace (e.g., Snowflake, PostgreSQL, Big | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `limit` | number | No | Maximum number of connections to return \(1-500, default: 25\) | | `sortBy` | string | No | Sort by field: CREATED_AT or NAME | | `sortDirection` | string | No | Sort direction: ASC or DESC | @@ -424,7 +410,6 @@ List all groups in the Hex workspace with optional sorting. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `limit` | number | No | Maximum number of groups to return \(1-500, default: 25\) | | `sortBy` | string | No | Sort by field: CREATED_AT or NAME | | `sortDirection` | string | No | Sort direction: ASC or DESC | @@ -455,7 +440,6 @@ List all projects in your Hex workspace with optional filtering by status. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `limit` | number | No | Maximum number of projects to return \(1-100\) | | `includeArchived` | boolean | No | Include archived projects in results | | `statusFilter` | string | No | Filter by status: PUBLISHED, DRAFT, or ALL | @@ -505,7 +489,6 @@ List all users in the Hex workspace with optional filtering and sorting. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `limit` | number | No | Maximum number of users to return \(1-100, default: 25\) | | `sortBy` | string | No | Sort by field: NAME or EMAIL | | `sortDirection` | string | No | Sort direction: ASC or DESC | @@ -540,7 +523,6 @@ Execute a published Hex project. Optionally pass input parameters and control ca | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `projectId` | string | Yes | The UUID of the Hex project to run | | `inputParams` | json | No | JSON object of input parameters for the project \(e.g., \{"date": "2024-01-01"\}\) | | `dryRun` | boolean | No | If true, perform a dry run without executing the project | @@ -573,7 +555,6 @@ Update the name or description of an existing Hex collection. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `collectionId` | string | Yes | The UUID of the collection to update | | `name` | string | No | New name for the collection | | `description` | string | No | New description for the collection | @@ -601,7 +582,6 @@ Rename a Hex group or add/remove members from it. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `groupId` | string | Yes | The UUID of the group to update | | `name` | string | No | New name for the group | | `addUserIds` | json | No | JSON array of user UUIDs to add to the group \(e.g., \["uuid1", "uuid2"\]\) | @@ -627,7 +607,6 @@ Update a Hex project status label (e.g., endorsement or custom workspace statuse | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) | | `projectId` | string | Yes | The UUID of the Hex project to update | | `status` | string | Yes | New project status name \(custom workspace status label\) | diff --git a/apps/docs/content/docs/integrations/huggingface.mdx b/apps/docs/content/docs/integrations/huggingface.mdx index 545d678fffa..a09ac12149d 100644 --- a/apps/docs/content/docs/integrations/huggingface.mdx +++ b/apps/docs/content/docs/integrations/huggingface.mdx @@ -2,7 +2,7 @@ title: Hugging Face description: Use Hugging Face Inference API integration: - type: "huggingface" + type: "huggingface_v2" color: "#0B0F19" --- @@ -35,7 +35,6 @@ Generate completions using Hugging Face Inference API | `model` | string | Yes | Model to use for chat completions \(e.g., "deepseek/deepseek-v3-0324", "meta-llama/Llama-3.3-70B-Instruct"\) | | `maxTokens` | number | No | Maximum number of tokens to generate | | `temperature` | number | No | Sampling temperature \(0-2\). Higher values make output more random | -| `apiKey` | string | Yes | Hugging Face API token | #### Output diff --git a/apps/docs/content/docs/integrations/iam.mdx b/apps/docs/content/docs/integrations/iam.mdx index 6679fbac775..68ed7f380d5 100644 --- a/apps/docs/content/docs/integrations/iam.mdx +++ b/apps/docs/content/docs/integrations/iam.mdx @@ -2,7 +2,7 @@ title: AWS IAM description: Manage AWS IAM users, roles, policies, and groups integration: - type: "iam" + type: "iam_v2" color: "linear-gradient(45deg, #BD0816 0%, #FF5252 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -[AWS Identity and Access Management (IAM)](https://aws.amazon.com/iam/) controls access to AWS resources. Use this integration to manage users, roles, policies, groups, and access keys, or simulate a principal’s permissions before changing them. +[AWS Identity and Access Management (IAM)](https://aws.amazon.com/iam/) controls access to AWS resources. Use this integration to manage users, roles, policies, groups, and access keys, or simulate a principal’s permissions before changing them. Connect with a saved [AWS credential](/integrations/aws-credentials). `Simulate Principal Policy` returns one result per action regardless of how many resource ARNs you pass. The top-level decision is the **aggregate** across every resource — most restrictive wins — and the top-level resource name is an ARN *template* for the resource type, not one of your ARNs. Per-resource answers live in `resourceSpecificResults`, and when you supply concrete ARNs, missing context keys are reported there too rather than at the top level. Read `resourceSpecificResults` whenever you simulate against more than one resource: the aggregate alone will tell you a principal is denied when it is in fact allowed on some of them. @@ -34,8 +34,6 @@ List IAM users in your AWS account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `pathPrefix` | string | No | Path prefix to filter users \(e.g., /division_abc/\) | | `maxItems` | number | No | Maximum number of users to return \(1-1000, default 100\) | | `marker` | string | No | Pagination marker from a previous request | @@ -62,8 +60,6 @@ Get detailed information about an IAM user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | No | The name of the IAM user to retrieve \(defaults to the caller if omitted\) | #### Output @@ -92,8 +88,6 @@ Create a new IAM user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | Yes | Name for the new IAM user \(1-64 characters\) | | `path` | string | No | Path for the user \(e.g., /division_abc/\), defaults to / | @@ -121,8 +115,6 @@ Delete an IAM user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | Yes | The name of the IAM user to delete | #### Output @@ -144,8 +136,6 @@ List IAM roles in your AWS account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `pathPrefix` | string | No | Path prefix to filter roles \(e.g., /application/\) | | `maxItems` | number | No | Maximum number of roles to return \(1-1000, default 100\) | | `marker` | string | No | Pagination marker from a previous request | @@ -172,8 +162,6 @@ Get detailed information about an IAM role | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `roleName` | string | Yes | The name of the IAM role to retrieve | #### Output @@ -204,8 +192,6 @@ Create a new IAM role with a trust policy | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `roleName` | string | Yes | Name for the new IAM role \(1-64 characters\) | | `assumeRolePolicyDocument` | string | Yes | Trust policy JSON specifying who can assume this role | | `description` | string | No | Description of the role | @@ -236,8 +222,6 @@ Delete an IAM role | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `roleName` | string | Yes | The name of the IAM role to delete | #### Output @@ -259,8 +243,6 @@ Attach a managed policy to an IAM user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | Yes | The name of the IAM user | | `policyArn` | string | Yes | The ARN of the managed policy to attach | @@ -283,8 +265,6 @@ Remove a managed policy from an IAM user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | Yes | The name of the IAM user | | `policyArn` | string | Yes | The ARN of the managed policy to detach | @@ -307,8 +287,6 @@ Attach a managed policy to an IAM role | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `roleName` | string | Yes | The name of the IAM role | | `policyArn` | string | Yes | The ARN of the managed policy to attach | @@ -331,8 +309,6 @@ Remove a managed policy from an IAM role | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `roleName` | string | Yes | The name of the IAM role | | `policyArn` | string | Yes | The ARN of the managed policy to detach | @@ -355,8 +331,6 @@ List managed IAM policies | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `scope` | string | No | Filter by scope. Must be exactly one of: All, AWS \(AWS-managed\), Local \(customer-managed\) | | `onlyAttached` | boolean | No | If true, only return policies attached to an entity | | `pathPrefix` | string | No | Path prefix to filter policies | @@ -385,8 +359,6 @@ Get details about a managed IAM policy, including its description — the field | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `policyArn` | string | Yes | ARN of the managed policy to retrieve \(e.g., arn:aws:iam::aws:policy/ReadOnlyAccess\) | #### Output @@ -419,8 +391,6 @@ Create a new access key pair for an IAM user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | No | The IAM user to create the key for \(defaults to current user\) | #### Output @@ -447,8 +417,6 @@ Delete an access key pair for an IAM user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `accessKeyIdToDelete` | string | Yes | The access key ID to delete | | `userName` | string | No | The IAM user whose key to delete \(defaults to current user\) | @@ -471,8 +439,6 @@ List an IAM user's access key IDs with their status and age — use to find stal | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | No | The IAM user whose keys to list \(defaults to the calling user if omitted\) | | `maxItems` | number | No | Maximum number of access keys to return \(1-1000\) | | `marker` | string | No | Pagination marker from a previous request | @@ -499,8 +465,6 @@ Activate or deactivate an IAM access key — deactivate an old key and verify no | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `accessKeyIdToUpdate` | string | Yes | The access key ID whose status to change | | `status` | string | Yes | The status to set. Must be exactly one of: Active, Inactive. An Inactive key is rejected by AWS but can be reactivated. | | `userName` | string | No | The IAM user that owns the key \(defaults to the calling user if omitted\) | @@ -524,8 +488,6 @@ List IAM groups in your AWS account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `pathPrefix` | string | No | Path prefix to filter groups | | `maxItems` | number | No | Maximum number of groups to return \(1-1000, default 100\) | | `marker` | string | No | Pagination marker from a previous request | @@ -552,8 +514,6 @@ Add an IAM user to a group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | Yes | The name of the IAM user | | `groupName` | string | Yes | The name of the IAM group | @@ -576,8 +536,6 @@ Remove an IAM user from a group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | Yes | The name of the IAM user | | `groupName` | string | Yes | The name of the IAM group | @@ -600,8 +558,6 @@ List all managed policies attached to an IAM role | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `roleName` | string | Yes | Name of the IAM role | | `pathPrefix` | string | No | Path prefix to filter policies \(e.g., /application/\) | | `maxItems` | number | No | Maximum number of policies to return \(1-1000\) | @@ -629,8 +585,6 @@ List all managed policies attached to an IAM user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `userName` | string | Yes | Name of the IAM user | | `pathPrefix` | string | No | Path prefix to filter policies \(e.g., /application/\) | | `maxItems` | number | No | Maximum number of policies to return \(1-1000\) | @@ -658,8 +612,6 @@ Simulate whether a user, role, or group is allowed to perform specific AWS actio | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `policySourceArn` | string | Yes | ARN of the user, group, or role to simulate \(e.g., arn:aws:iam::123456789012:user/alice\) | | `actionNames` | string | Yes | Comma-separated list of AWS actions to simulate \(e.g., s3:GetObject,ec2:DescribeInstances\) | | `resourceArns` | string | No | Comma-separated list of resource ARNs to simulate against \(defaults to * if not provided\). Read the per-ARN verdict from resourceSpecificResults, not from evalDecision. | diff --git a/apps/docs/content/docs/integrations/identity_center.mdx b/apps/docs/content/docs/integrations/identity_center.mdx index df1d7e7fec3..5655532ee83 100644 --- a/apps/docs/content/docs/integrations/identity_center.mdx +++ b/apps/docs/content/docs/integrations/identity_center.mdx @@ -2,7 +2,7 @@ title: AWS Identity Center description: Manage account access in AWS IAM Identity Center integration: - type: "identity_center" + type: "identity_center_v2" color: "linear-gradient(45deg, #BD0816 0%, #FF5252 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [AWS IAM Identity Center](https://aws.amazon.com/iam/identity-center/) to assign or revoke permission sets for AWS accounts, resolve users and groups, and inspect account access. +Use [AWS IAM Identity Center](https://aws.amazon.com/iam/identity-center/) to assign or revoke permission sets for AWS accounts, resolve users and groups, and inspect account access. Connect with a saved [AWS credential](/integrations/aws-credentials). The same operations support the reverse direction — access review. Starting from an account, an agent can list its assignments, resolve the principals, expand groups into their members, and produce a written report of exactly who can reach that account and through which permission set. @@ -36,8 +36,6 @@ List all AWS IAM Identity Center instances in your account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `maxResults` | number | No | Maximum number of instances to return \(1-100\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -69,8 +67,6 @@ List all AWS accounts in your organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `maxResults` | number | No | Maximum number of accounts to return \(1-20; the AWS Organizations ceiling\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -101,8 +97,6 @@ Retrieve details about a specific AWS account by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `accountId` | string | Yes | AWS account ID to describe | #### Output @@ -129,8 +123,6 @@ List all permission sets defined in an IAM Identity Center instance | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `instanceArn` | string | Yes | ARN of the Identity Center instance | | `maxResults` | number | No | Maximum number of permission sets to return \(1-100\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -161,8 +153,6 @@ Look up a user in the Identity Store by email address | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `identityStoreId` | string | Yes | Identity Store ID \(from the Identity Center instance\) | | `email` | string | Yes | Email address of the user to look up | @@ -188,8 +178,6 @@ Resolve an Identity Store user ID to the user behind it. Use to turn the princip | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `identityStoreId` | string | Yes | Identity Store ID \(e.g., d-1234567890\) | | `userId` | string | Yes | Identity Store user ID, such as the principalId on a USER account assignment | @@ -220,8 +208,6 @@ Look up a group in the Identity Store by display name | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `identityStoreId` | string | Yes | Identity Store ID \(from the Identity Center instance\) | | `displayName` | string | Yes | Display name of the group to look up | @@ -246,8 +232,6 @@ Resolve an Identity Store group ID to the group behind it. Use to turn the princ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `identityStoreId` | string | Yes | Identity Store ID \(e.g., d-1234567890\) | | `groupId` | string | Yes | Identity Store group ID, such as the principalId on a GROUP account assignment | @@ -275,8 +259,6 @@ List all groups in the Identity Store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `identityStoreId` | string | Yes | Identity Store ID \(from the Identity Center instance\) | | `maxResults` | number | No | Maximum number of groups to return \(1-100\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -308,8 +290,6 @@ List the users who belong to an Identity Store group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `identityStoreId` | string | Yes | Identity Store ID \(e.g., d-1234567890\) | | `groupId` | string | Yes | Identity Store group ID whose members to list | | `maxResults` | number | No | Maximum number of memberships to return \(1-100\) | @@ -339,8 +319,6 @@ Grant a user or group access to an AWS account via a permission set (temporary e | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `instanceArn` | string | Yes | ARN of the Identity Center instance | | `accountId` | string | Yes | AWS account ID to grant access to | | `permissionSetArn` | string | Yes | ARN of the permission set to assign | @@ -374,8 +352,6 @@ Revoke a user or group access to an AWS account by removing a permission set ass | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `instanceArn` | string | Yes | ARN of the Identity Center instance | | `accountId` | string | Yes | AWS account ID to revoke access from | | `permissionSetArn` | string | Yes | ARN of the permission set to remove | @@ -409,8 +385,6 @@ Check the provisioning status of an account assignment creation request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `instanceArn` | string | Yes | ARN of the Identity Center instance | | `requestId` | string | Yes | Request ID returned from Create Account Assignment. Deletion request IDs are not accepted — use Check Assignment Deletion Status for those. | @@ -441,8 +415,6 @@ Check the deprovisioning status of an account assignment deletion request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `instanceArn` | string | Yes | ARN of the Identity Center instance | | `requestId` | string | Yes | Request ID returned from Delete Account Assignment | @@ -473,8 +445,6 @@ List every account and permission set a specific user or group is assigned. Use | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `instanceArn` | string | Yes | ARN of the Identity Center instance | | `principalId` | string | Yes | Identity Store ID of the user or group | | `principalType` | string | Yes | Type of principal: USER or GROUP | @@ -506,8 +476,6 @@ List every principal assigned a specific permission set on a specific AWS accoun | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `instanceArn` | string | Yes | ARN of the Identity Center instance | | `accountId` | string | Yes | AWS account ID to list assignments for \(12 digits\) | | `permissionSetArn` | string | Yes | ARN of the permission set to list assignments for | diff --git a/apps/docs/content/docs/integrations/incidentio.mdx b/apps/docs/content/docs/integrations/incidentio.mdx index 5e9f8f96c29..b70b6bd9022 100644 --- a/apps/docs/content/docs/integrations/incidentio.mdx +++ b/apps/docs/content/docs/integrations/incidentio.mdx @@ -2,7 +2,7 @@ title: incident.io description: Manage incidents with incident.io integration: - type: "incidentio" + type: "incidentio_v2" color: "#FFFFFF" --- @@ -29,7 +29,6 @@ List incidents from incident.io. Returns a list of incidents with their details | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `page_size` | number | No | Number of incidents to return per page \(e.g., 10, 25, 50\). Default: 25 | | `after` | string | No | Pagination cursor to fetch the next page of results \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `sort_by` | string | No | Sort order for incidents: created_at_newest_first or created_at_oldest_first | @@ -83,7 +82,6 @@ Create a new incident in incident.io. Requires idempotency_key, severity_id, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `idempotency_key` | string | Yes | Unique identifier to prevent duplicate incident creation. Use a UUID or unique string. | | `name` | string | No | Name of the incident \(e.g., "Database connection issues"\) | | `summary` | string | No | Brief summary of the incident \(e.g., "Intermittent connection failures to primary database"\) | @@ -132,7 +130,6 @@ Retrieve detailed information about a specific incident from incident.io by its | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | ID of the incident to retrieve \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -177,7 +174,6 @@ Update an existing incident in incident.io. Can update name, summary, severity, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | ID of the incident to update \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `name` | string | No | Updated name of the incident \(e.g., "Database connection issues"\) | | `summary` | string | No | Updated summary of the incident \(e.g., "Intermittent connection failures to primary database"\) | @@ -226,7 +222,6 @@ List actions from incident.io. Optionally filter by incident ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `incident_id` | string | No | Filter actions by incident ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `incident_mode` | string | No | Filter actions by incident mode \(standard, retrospective, test, tutorial, or stream\) | @@ -268,7 +263,6 @@ Get detailed information about a specific action from incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | Action ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -309,7 +303,6 @@ List follow-ups from incident.io. Optionally filter by incident ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `incident_id` | string | No | Filter follow-ups by incident ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `incident_mode` | string | No | Filter follow-ups by incident mode \(standard, retrospective, test, tutorial, or stream\) | @@ -357,7 +350,6 @@ Get detailed information about a specific follow-up from incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | Follow-up ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -404,7 +396,6 @@ List all users in your Incident.io workspace. Returns user details including id, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Incident.io API Key | | `page_size` | number | No | Number of results to return per page \(e.g., 10, 25, 50\). Default: 25 | | `after` | string | No | Pagination cursor to fetch the next page of results | | `email` | string | No | Filter users by email address | @@ -436,7 +427,6 @@ Get detailed information about a specific user in your Incident.io workspace by | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Incident.io API Key | | `id` | string | Yes | The unique identifier of the user to retrieve \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -461,7 +451,6 @@ List all workflows in your incident.io workspace. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | #### Output @@ -499,7 +488,6 @@ Create a new workflow in incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `name` | string | Yes | Name of the workflow \(e.g., "Notify on Critical Incidents"\) | | `folder` | string | No | Folder to organize the workflow in | | `state` | string | No | State of the workflow \(active, draft, or disabled\) | @@ -551,7 +539,6 @@ Get details of a specific workflow in incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the workflow to retrieve \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `skip_step_upgrades` | boolean | No | Skip workflow step upgrades when existing workflow step parameters changed | @@ -592,7 +579,6 @@ Update an existing workflow in incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the workflow to update \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `name` | string | Yes | New name for the workflow \(e.g., "Notify on Critical Incidents"\) | | `steps` | string | Yes | Complete array of workflow steps as a JSON string | @@ -644,7 +630,6 @@ Delete a workflow in incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the workflow to delete \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -665,7 +650,6 @@ List all schedules in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `page_size` | number | No | Number of results per page \(e.g., 10, 25, 50\). Default: 25 | | `after` | string | No | Pagination cursor to fetch the next page of results \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | @@ -711,7 +695,6 @@ Create a new schedule in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `name` | string | Yes | Name of the schedule \(e.g., "Primary On-Call"\) | | `timezone` | string | Yes | Timezone for the schedule \(e.g., America/New_York\) | | `config` | string | Yes | Schedule configuration as JSON string with rotations. Example: \{"rotations": \[\{"name": "Primary", "users": \[\{"id": "user_id"\}\], "handover_start_at": "2024-01-01T09:00:00Z", "handovers": \[\{"interval": 1, "interval_type": "weekly"\}\]\}\]\} | @@ -739,7 +722,6 @@ Get details of a specific schedule in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the schedule \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -781,7 +763,6 @@ Update an existing schedule in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the schedule to update \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `name` | string | No | New name for the schedule \(e.g., "Primary On-Call"\) | | `timezone` | string | No | New timezone for the schedule \(e.g., America/New_York\) | @@ -810,7 +791,6 @@ Delete a schedule in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the schedule to delete \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -831,7 +811,6 @@ List all escalation policies in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `page_size` | number | No | Number of escalations to return per page | | `after` | string | No | Pagination cursor to fetch the next page of results | @@ -864,7 +843,6 @@ Create a new escalation policy in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `idempotency_key` | string | Yes | Unique identifier to prevent duplicate escalation creation. Use a UUID or unique string. | | `title` | string | Yes | Title of the escalation \(e.g., "Database Critical Alert"\) | | `escalation_path_id` | string | No | ID of the escalation path to use \(required if user_ids not provided\) | @@ -896,7 +874,6 @@ Get details of a specific escalation policy in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the escalation policy \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -925,7 +902,6 @@ List all custom fields from incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | #### Output @@ -951,7 +927,6 @@ Create a new custom field in incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `name` | string | Yes | Name of the custom field \(e.g., "Affected Service"\) | | `description` | string | Yes | Description of the custom field \(required\) | | `field_type` | string | Yes | Type of the custom field: text, link, numeric, single_select, or multi_select | @@ -980,7 +955,6 @@ Get detailed information about a specific custom field from incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | Custom field ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -1007,7 +981,6 @@ Update an existing custom field in incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | Custom field ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `name` | string | Yes | New name for the custom field \(e.g., "Affected Service"\) | | `description` | string | Yes | New description for the custom field \(required\) | @@ -1036,7 +1009,6 @@ Delete a custom field from incident.io. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | Custom field ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -1057,7 +1029,6 @@ List all severity levels configured in your Incident.io workspace. Returns sever | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Incident.io API Key | #### Output @@ -1081,7 +1052,6 @@ List all incident statuses configured in your Incident.io workspace. Returns sta | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Incident.io API Key | #### Output @@ -1105,7 +1075,6 @@ List all incident types configured in your Incident.io workspace. Returns type d | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Incident.io API Key | #### Output @@ -1129,7 +1098,6 @@ List all incident roles in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | #### Output @@ -1158,7 +1126,6 @@ Create a new incident role in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `name` | string | Yes | Name of the incident role \(e.g., "Incident Commander"\) | | `description` | string | Yes | Description of the incident role | | `instructions` | string | Yes | Instructions for the incident role | @@ -1191,7 +1158,6 @@ Get details of a specific incident role in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the incident role \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -1221,7 +1187,6 @@ Update an existing incident role in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the incident role to update \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `name` | string | Yes | Name of the incident role \(e.g., "Incident Commander"\) | | `description` | string | Yes | Description of the incident role | @@ -1255,7 +1220,6 @@ Delete an incident role in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the incident role to delete \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -1276,7 +1240,6 @@ List all incident timestamp definitions in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | #### Output @@ -1301,7 +1264,6 @@ Get details of a specific incident timestamp definition in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the incident timestamp \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -1327,7 +1289,6 @@ List all updates for a specific incident in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `incident_id` | string | No | The ID of the incident to get updates for \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\). If not provided, returns all updates | | `page_size` | number | No | Number of results to return per page \(e.g., 10, 25, 50\) | | `after` | string | No | Cursor for pagination \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | @@ -1380,7 +1341,6 @@ List all entries for a specific schedule in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `schedule_id` | string | Yes | The ID of the schedule to get entries for \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `entry_window_start` | string | No | Start date/time to filter entries in ISO 8601 format \(e.g., "2024-01-15T09:00:00Z"\) | | `entry_window_end` | string | No | End date/time to filter entries in ISO 8601 format \(e.g., "2024-01-22T09:00:00Z"\) | @@ -1409,7 +1369,6 @@ Create a new schedule override in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `rotation_id` | string | Yes | The ID of the rotation to override \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `layer_id` | string | Yes | The ID of the layer this override applies to | | `schedule_id` | string | Yes | The ID of the schedule \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | @@ -1449,7 +1408,6 @@ List escalation paths in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `page_size` | number | No | Number of escalation paths to return per page | | `after` | string | No | Pagination cursor to fetch the next page of results | @@ -1478,7 +1436,6 @@ Create a new escalation path in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `name` | string | Yes | Name of the escalation path \(e.g., "Critical Incident Path"\) | | `path` | json | Yes | Array of escalation levels with targets and time to acknowledge in seconds. Each level should have: targets \(array of \{id, type, schedule_id?, user_id?, urgency\}\) and time_to_ack_seconds \(number\) | | `working_hours` | json | No | Optional working hours configuration. Array of \{weekday, start_time, end_time\} | @@ -1515,7 +1472,6 @@ Get details of a specific escalation path in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the escalation path \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -1550,7 +1506,6 @@ Update an existing escalation path in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the escalation path to update \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `name` | string | Yes | New name for the escalation path \(e.g., "Critical Incident Path"\) | | `path` | json | Yes | New escalation path configuration. Array of escalation levels with targets and time_to_ack_seconds | @@ -1588,7 +1543,6 @@ Delete an escalation path in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the escalation path to delete \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -1609,7 +1563,6 @@ Get who is currently on call in incident.io, as one row per ongoing shift across | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `schedule_id` | string | No | Limit the result to a single schedule by ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\). Leave empty to return who is on call across every schedule. | | `page_size` | number | No | Number of schedules to scan per page when no schedule ID is given \(e.g., 10, 25\). Defaults to 25; upcoming shifts are only returned at 25 or lower. | | `after` | string | No | Pagination cursor to fetch the next page of schedules \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | @@ -1637,7 +1590,6 @@ List the one-off overrides layered on top of a schedule in incident.io, such as | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `schedule_id` | string | Yes | The ID of the schedule to get overrides for \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `rotation_id` | string | No | Only return overrides on this rotation | | `layer_id` | string | No | Only return overrides on this layer | @@ -1679,7 +1631,6 @@ List alerts in incident.io, optionally filtered by status, source, or created da | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `page_size` | number | No | Number of results per page \(e.g., 10, 25, 50\). Default: 25 | | `after` | string | No | Pagination cursor to fetch the next page of results \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `status` | string | No | Filter by alert status: "firing" or "resolved" | @@ -1725,7 +1676,6 @@ Get a single alert by ID from incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the alert to fetch \(e.g., "01GW2G3V0S59R238FAHPDS1R66"\) | #### Output @@ -1758,7 +1708,6 @@ Resolve a currently firing alert in incident.io. Resolving an already-resolved a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the alert to resolve \(e.g., "01GW2G3V0S59R238FAHPDS1R66"\) | #### Output @@ -1820,7 +1769,6 @@ List the connections between incidents and alerts in incident.io — which alert | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `page_size` | number | No | Number of results per page \(e.g., 10, 25, 50\). Default: 25 | | `after` | string | No | Pagination cursor to fetch the next page of results \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `incident_id` | string | No | Only return alerts attached to this incident \(e.g., "01FDAG4SAP5TYPT98WGR2N7W91"\) | @@ -1868,7 +1816,6 @@ Cancel an escalation in incident.io. Notifications stop, and the escalation will | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the escalation to cancel \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | #### Output @@ -1889,7 +1836,6 @@ List all catalog types in incident.io, including those synced from external reso | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | #### Output @@ -1931,7 +1877,6 @@ List the entries of a catalog type in incident.io — for example every service, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `catalog_type_id` | string | Yes | The ID of the catalog type to list entries for \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `page_size` | number | No | Number of results per page \(e.g., 10, 25, 50\). Default: 25 | | `after` | string | No | Pagination cursor to fetch the next page of results \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | @@ -1992,7 +1937,6 @@ List all teams in the incident.io organisation, along with their members | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `page_size` | number | No | Number of results per page \(e.g., 10, 25, 50\) | | `after` | string | No | Pagination cursor to fetch the next page of results \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | @@ -2028,7 +1972,6 @@ Get a single team by ID from incident.io, along with its members | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the team to fetch \(e.g., "01JPQA75EPNEES4479P16P4XAB"\) | #### Output @@ -2060,7 +2003,6 @@ Create a new follow-up on an incident in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `incident_id` | string | Yes | The ID of the incident the follow-up belongs to \(e.g., "01FDAG4SAP5TYPT98WGR2N7W91"\) | | `title` | string | Yes | Title of the follow-up \(e.g., "Add alerting on connection pool saturation"\) | | `description` | string | No | Description of the follow-up. Supports Markdown. | @@ -2132,7 +2074,6 @@ Update an existing follow-up in incident.io, for example to mark it completed or | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the follow-up to update \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `title` | string | Yes | Title of the follow-up. This endpoint replaces the title, so always send it. | | `status` | string | Yes | Status of the follow-up: "outstanding", "completed", or "not_doing". Deleting is not supported here. | @@ -2204,7 +2145,6 @@ Create a new action on an incident in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `incident_id` | string | Yes | The ID of the incident the action belongs to \(e.g., "01FDAG4SAP5TYPT98WGR2N7W91"\) | | `description` | string | Yes | Description of the action. Supports Markdown. | | `assignee_id` | string | No | ID of the user to assign this action to | @@ -2260,7 +2200,6 @@ Update an existing action in incident.io, for example to mark it completed or re | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `id` | string | Yes | The ID of the action to update \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | | `description` | string | Yes | Description of the action. This endpoint replaces the description, so always send it. | | `status` | string | Yes | Status of the action: "outstanding", "completed", or "not_doing". Deleting is not supported here. | @@ -2317,7 +2256,6 @@ List the participants of an incident in incident.io, split into those actively h | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `incident_id` | string | Yes | The ID of the incident to find participants of \(e.g., "01FDAG4SAP5TYPT98WGR2N7W91"\) | #### Output @@ -2339,7 +2277,6 @@ Make a user a member of a private incident in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `incident_id` | string | Yes | The ID of the private incident \(e.g., "01FCNDV6P870EA6S7TK1DSYD5H"\) | | `user_id` | string | Yes | The ID of the user to grant access to \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | @@ -2371,7 +2308,6 @@ Revoke a user's membership of a private incident in incident.io | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | incident.io API Key | | `incident_id` | string | Yes | The ID of the private incident \(e.g., "01FCNDV6P870EA6S7TK1DSYD5H"\) | | `user_id` | string | Yes | The ID of the user to revoke access from \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) | diff --git a/apps/docs/content/docs/integrations/infisical.mdx b/apps/docs/content/docs/integrations/infisical.mdx index efbcc7ed0ef..92a0593db7d 100644 --- a/apps/docs/content/docs/integrations/infisical.mdx +++ b/apps/docs/content/docs/integrations/infisical.mdx @@ -2,7 +2,7 @@ title: Infisical description: Manage secrets with Infisical integration: - type: "infisical" + type: "infisical_v2" color: "#F7FE62" --- @@ -10,12 +10,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Infisical](https://infisical.com/) to list, read, create, update, or delete secrets in a project environment. Configure the API key, project ID, environment slug, and any secret path required by the selected operation. +Use [Infisical](https://infisical.com/) to list, read, create, update, or delete secrets in a project environment. Connect a saved Infisical credential (a machine identity access token), then set the project ID, environment slug, and any secret path required by the selected operation. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -List, get, create, update, and delete secrets across project environments. +List, get, create, update, and delete secrets across project environments on Infisical Cloud or a self-hosted instance. ## Actions @@ -29,8 +29,6 @@ List all secrets in a project environment. Returns secret keys, values, comments | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Infisical API token | -| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. | | `projectId` | string | Yes | The ID of the project to list secrets from | | `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) | | `secretPath` | string | No | The path of the secrets \(default: "/"\) | @@ -91,8 +89,6 @@ Retrieve a single secret by name from a project environment. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Infisical API token | -| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. | | `projectId` | string | Yes | The ID of the project | | `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) | | `secretName` | string | Yes | The name of the secret to retrieve | @@ -152,8 +148,6 @@ Create a new secret in a project environment. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Infisical API token | -| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. | | `projectId` | string | Yes | The ID of the project | | `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) | | `secretName` | string | Yes | The name of the secret to create | @@ -213,8 +207,6 @@ Update an existing secret in a project environment. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Infisical API token | -| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. | | `projectId` | string | Yes | The ID of the project | | `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) | | `secretName` | string | Yes | The name of the secret to update | @@ -275,8 +267,6 @@ Delete a secret from a project environment. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Infisical API token | -| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. | | `projectId` | string | Yes | The ID of the project | | `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) | | `secretName` | string | Yes | The name of the secret to delete | diff --git a/apps/docs/content/docs/integrations/instantly.mdx b/apps/docs/content/docs/integrations/instantly.mdx index f1791cc0802..f04114e0b84 100644 --- a/apps/docs/content/docs/integrations/instantly.mdx +++ b/apps/docs/content/docs/integrations/instantly.mdx @@ -2,7 +2,7 @@ title: Instantly description: Manage Instantly leads, campaigns, emails, and lead lists integration: - type: "instantly" + type: "instantly_v2" color: "#FFFFFF" --- @@ -31,7 +31,6 @@ Retrieves Instantly V2 leads with search, campaign, list, and pagination filters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `search` | string | No | Search by first name, last name, or email | | `filter` | string | No | Instantly lead filter value, such as FILTER_VAL_CONTACTED or FILTER_VAL_ACTIVE | | `campaign` | string | No | Campaign ID to filter leads | @@ -86,7 +85,6 @@ Retrieves an Instantly V2 lead by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `leadId` | string | Yes | Lead ID | #### Output @@ -125,7 +123,6 @@ Creates an Instantly V2 lead in a campaign or lead list. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `campaign` | string | No | Campaign ID associated with the lead | | `list_id` | string | No | Lead list ID associated with the lead | | `email` | string | No | Lead email address. Required when adding to a campaign. | @@ -183,7 +180,6 @@ Updates fields on an existing Instantly V2 lead. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `leadId` | string | Yes | Lead ID | | `first_name` | string | No | Lead first name | | `last_name` | string | No | Lead last name | @@ -233,7 +229,6 @@ Deletes Instantly V2 leads in bulk from a campaign or lead list. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `campaign_id` | string | No | Campaign ID to delete leads from. Required if list_id is not provided. | | `list_id` | string | No | Lead list ID to delete leads from. Required if campaign_id is not provided. | | `status` | number | No | Optional lead status filter | @@ -258,7 +253,6 @@ Submits an Instantly V2 background job to update a lead interest status. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `lead_email` | string | Yes | Lead email address | | `interest_value` | number | No | Interest status value. Leave empty in the block or pass null to reset to Lead. | | `campaign_id` | string | No | Campaign ID for the lead | @@ -284,7 +278,6 @@ Retrieves Instantly V2 campaigns with search, status, tag, and pagination filter | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `limit` | number | No | Number of campaigns to return, from 1 to 100 | | `starting_after` | string | No | Pagination cursor from next_starting_after | | `search` | string | No | Search by campaign name | @@ -328,7 +321,6 @@ Creates an Instantly V2 campaign using the documented campaign schedule schema. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `name` | string | Yes | Campaign name | | `campaign_schedule` | json | Yes | Campaign schedule object with schedules array | | `sequences` | array | No | Campaign sequence definitions | @@ -378,7 +370,6 @@ Updates documented Instantly V2 campaign fields. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `campaignId` | string | Yes | Campaign ID | | `name` | string | No | Campaign name | | `campaign_schedule` | json | No | Campaign schedule object with schedules array | @@ -429,7 +420,6 @@ Activates, starts, or resumes an Instantly V2 campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `campaignId` | string | Yes | Campaign ID | #### Output @@ -468,7 +458,6 @@ Pauses a running Instantly V2 campaign, stopping further email sends. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `campaignId` | string | Yes | Campaign ID | #### Output @@ -507,7 +496,6 @@ Permanently deletes an Instantly V2 campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `campaignId` | string | Yes | Campaign ID | #### Output @@ -546,7 +534,6 @@ Retrieves Instantly V2 Unibox emails with search and pagination filters. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `limit` | number | No | Number of emails to return, from 1 to 100 | | `starting_after` | string | No | Pagination cursor from next_starting_after | | `search` | string | No | Search query, email address, or thread:<thread-id> | @@ -593,7 +580,6 @@ Sends an Instantly V2 reply to an existing Unibox email. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `eaccount` | string | Yes | Connected email account used to send the reply | | `reply_to_uuid` | string | Yes | Email ID to reply to | | `subject` | string | Yes | Reply subject | @@ -637,7 +623,6 @@ Retrieves Instantly V2 lead lists with search and pagination filters. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `limit` | number | No | Number of lead lists to return, from 1 to 100 | | `starting_after` | string | No | Starting-after timestamp cursor | | `has_enrichment_task` | boolean | No | Filter by enrichment task setting | @@ -679,7 +664,6 @@ Creates an Instantly V2 lead list. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Instantly API key with the required V2 scopes | | `name` | string | Yes | Lead list name | | `has_enrichment_task` | boolean | No | Whether this list runs enrichment for every added lead | | `owned_by` | string | No | User ID of the lead list owner | diff --git a/apps/docs/content/docs/integrations/intercom.mdx b/apps/docs/content/docs/integrations/intercom.mdx index a5aeb158d97..44c2884ee57 100644 --- a/apps/docs/content/docs/integrations/intercom.mdx +++ b/apps/docs/content/docs/integrations/intercom.mdx @@ -2,7 +2,7 @@ title: Intercom description: Manage contacts, companies, conversations, tickets, and messages in Intercom integration: - type: "intercom_v2" + type: "intercom_v3" color: "#FFFFFF" --- diff --git a/apps/docs/content/docs/integrations/jotform.mdx b/apps/docs/content/docs/integrations/jotform.mdx index c8635101e58..def122e3518 100644 --- a/apps/docs/content/docs/integrations/jotform.mdx +++ b/apps/docs/content/docs/integrations/jotform.mdx @@ -2,7 +2,7 @@ title: Jotform description: Read submissions, manage forms, and wire up webhooks in Jotform integration: - type: "jotform" + type: "jotform_v2" color: "#FFFFFF" --- @@ -16,7 +16,7 @@ Use [Jotform](https://www.jotform.com/) to manage forms and submissions, create The integration authenticates with a Jotform API key, created under **Account Settings → API**. Keys are scoped to read-only or full access, so create the key at the level your operations need — creating submissions, editing forms, and managing webhooks all require full access. -An API key is only valid on the host that issued it. Use the **Region** field to match where the account lives: `us` (default), `eu` for EU data residency accounts, or `hipaa` for HIPAA-compliant accounts. A key used against the wrong host returns an authentication error. +An API key is only valid on the host that issued it. Choose the region when you connect the Jotform credential (the legacy block has a **Region** field instead) to match where the account lives: `us` (default), `eu` for EU data residency accounts, or `hipaa` for HIPAA-compliant accounts. A key used against the wrong host returns an authentication error. Jotform enforces a daily API call limit that varies by plan. Schedule high-frequency polling with that budget in mind — or prefer a webhook, to reduce polling calls. Sim still makes API calls to register and manage webhook subscriptions. @@ -45,8 +45,6 @@ List the forms on a Jotform account with their titles, status, and submission co | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `offset` | string | No | Index of the first result to return. Default 0 | | `limit` | string | No | Number of forms to return. Default 20, maximum 1000 | | `orderby` | string | No | Field to order by: id, username, title, status, created_at, updated_at, new, count, or slug | @@ -89,8 +87,6 @@ Get the details of a single Jotform form, including its status, URL, and submiss | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | Form ID, the numeric segment of the form URL | #### Output @@ -125,8 +121,6 @@ Create a new Jotform form from a list of questions, plus optional form propertie | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `questions` | json | Yes | Array of question objects, each with type, text, order, and name, e.g. \[\{"type":"control_email","text":"Email","order":"1","name":"email"\}\] | | `properties` | json | No | Form properties object, e.g. \{"title":"Contact Us","height":"600"\} | | `emails` | json | No | Array of email objects, each with type \(notification or autorespond\), from, to, subject, html, and body | @@ -159,8 +153,6 @@ Clone an existing Jotform form and return the copy. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to clone | #### Output @@ -195,8 +187,6 @@ Delete a Jotform form. The API returns the form with status DELETED. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to delete | #### Output @@ -226,8 +216,6 @@ Get the settings of a form: layout, limits, redirect behavior, notification emai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to read properties from | | `propertyKey` | string | No | Single property to read instead of the whole set, e.g. formWidth, thankurl, or activeRedirect | @@ -249,8 +237,6 @@ Update form settings such as the thank-you redirect, submission limit, width, or | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to update | | `properties` | json | Yes | Properties to set, e.g. \{ "thankurl": "https://example.com/thanks", "activeRedirect": "thankurl", "formWidth": "650" \} | @@ -272,8 +258,6 @@ List every file uploaded through a form, with its download URL, size, type, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form whose uploads to list | #### Output @@ -302,8 +286,6 @@ List every question on a form with its question ID, label, and field type. Quest | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form whose questions to list | #### Output @@ -330,8 +312,6 @@ Get every property of a single form question, including its validation rules and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form the question belongs to | | `questionId` | string | Yes | Question ID, available from the List Questions operation | @@ -359,8 +339,6 @@ Add a question to an existing Jotform form. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to add the question to | | `questionType` | string | Yes | Field type, e.g. control_textbox, control_textarea, control_dropdown, control_radio, control_checkbox, control_fileupload, control_fullname, control_email, or control_datetime | | `text` | string | No | Question label shown on the form | @@ -392,8 +370,6 @@ Edit the properties of a form question, such as its label, order, or validation. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form the question belongs to | | `questionId` | string | Yes | ID of the question to edit | | `text` | string | No | New question label | @@ -419,8 +395,6 @@ Delete a question from a Jotform form. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form the question belongs to | | `questionId` | string | Yes | ID of the question to delete | @@ -443,8 +417,6 @@ List the submissions received by one form, with each answer available both by qu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form whose submissions to list | | `offset` | string | No | Index of the first result to return. Default 0 | | `limit` | string | No | Number of submissions to return. Default 20, maximum 1000 | @@ -484,8 +456,6 @@ List submissions across every form on the account, optionally narrowed to specif | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `offset` | string | No | Index of the first result to return. Default 0 | | `limit` | string | No | Number of submissions to return. Default 20, maximum 1000 | | `orderby` | string | No | Field to order by: id, form_id, IP, created_at, status, new, flag, or updated_at | @@ -524,8 +494,6 @@ Get a single Jotform submission, with its answers available both by question ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `submissionId` | string | Yes | ID of the submission to read | #### Output @@ -556,8 +524,6 @@ Submit an entry to a Jotform form. Answers are keyed by question ID, which the L | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to submit to | | `answers` | json | Yes | Answers keyed by question ID. Multi-field questions take either a nested object or the documented shorthand, e.g. \{"3":\{"first":"Bart","last":"Simpson"\},"4":"Hello"\} or \{"3_first":"Bart","3_last":"Simpson"\} | @@ -580,8 +546,6 @@ Edit an existing Jotform submission. Only the question IDs supplied are changed; | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `submissionId` | string | Yes | ID of the submission to edit | | `answers` | json | Yes | Answers to overwrite, keyed by question ID, e.g. \{"4":"Updated message","3":\{"first":"Lisa"\}\} or the shorthand \{"3_first":"Lisa"\}. The control keys new, flag, and status are passed through unchanged. | @@ -604,8 +568,6 @@ Delete a single Jotform submission. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `submissionId` | string | Yes | ID of the submission to delete | #### Output @@ -627,8 +589,6 @@ List every report on the account, across all forms, with the shareable URL for e | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | #### Output @@ -659,8 +619,6 @@ List the reports built from one form, each with its shareable URL. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form whose reports to list | #### Output @@ -692,8 +650,6 @@ Create a shareable report of a form, choosing the report type and which submissi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to build the report from | | `title` | string | Yes | Title of the report | | `listType` | string | Yes | Report type: excel, csv, grid, table, calendar, rss, or visual | @@ -725,8 +681,6 @@ Get the details and shareable URL of a single Jotform report. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `reportId` | string | Yes | ID of the report to read | #### Output @@ -758,8 +712,6 @@ Delete an existing Jotform report. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `reportId` | string | Yes | ID of the report to delete | #### Output @@ -781,8 +733,6 @@ List the webhooks registered on a form. The returned IDs are what the Delete Web | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form whose webhooks to list | #### Output @@ -805,8 +755,6 @@ Register a webhook on a form so every new submission is posted to the given URL. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to add the webhook to | | `webhookUrl` | string | Yes | URL that Jotform posts submission data to | @@ -830,8 +778,6 @@ Remove a webhook from a form. Returns the webhooks that remain registered on the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form the webhook belongs to | | `webhookId` | string | Yes | Webhook ID, available from the List Webhooks operation | @@ -855,8 +801,6 @@ Get the Jotform account behind the API key, including its plan, status, time zon | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | #### Output @@ -891,8 +835,6 @@ Get this month usage for the account: submissions received, payments, form views | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | #### Output @@ -919,8 +861,6 @@ Read the account activity log: forms created, updated, deleted or purged, and ac | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `action` | string | No | Activity to filter by: all \(default\), userCreation, userLogin, formCreation, formUpdate, formDelete, or formPurge | | `date` | string | No | Named date range to limit results to, e.g. lastWeek. Use startDate and endDate for an explicit range instead | | `sortBy` | string | No | Sort order: ASC or DESC | @@ -952,8 +892,6 @@ Submit several entries to a Jotform form in one call. Each entry is keyed by que | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to submit to | | `submissions` | json | Yes | Array of submissions. Each entry maps a question ID to an object holding its answer, e.g. \[\{"1":\{"text":"Answer 1"\},"2":\{"text":"Answer 2"\}\}\] | @@ -978,8 +916,6 @@ Add several questions to an existing Jotform form in one call. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `formId` | string | Yes | ID of the form to add the questions to | | `questions` | json | Yes | Array of question objects, each with type, text, order, and name, e.g. \[\{"type":"control_head","text":"Text 1","order":"1","name":"Header1"\}\] | @@ -1007,8 +943,6 @@ List the labels on the account as a tree. Labels are how Jotform groups forms, w | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `addResources` | string | No | Set to "true" to include the resources assigned to each label | #### Output @@ -1038,8 +972,6 @@ Get the name, color, and owner of a single Jotform label. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `labelId` | string | Yes | ID of the label to read, available from the List Labels operation | #### Output @@ -1068,8 +1000,6 @@ Create a Jotform label for grouping forms and other assets, optionally nested un | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `labelName` | string | Yes | Name of the label, e.g. "IT Operations" | | `color` | string | No | Label color as a hex code, e.g. "#FFDC7B" | | `parent` | string | No | ID of the parent label to nest this label under | @@ -1098,8 +1028,6 @@ Rename a Jotform label or change its color. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `labelId` | string | Yes | ID of the label to update | | `labelName` | string | No | New name for the label | | `color` | string | No | New label color as a hex code, e.g. "#23FFDD" | @@ -1122,8 +1050,6 @@ Delete a Jotform label along with all of its sublabels. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `labelId` | string | Yes | ID of the label to delete | #### Output @@ -1145,8 +1071,6 @@ List the assets assigned to a label — forms, workflows, sheets, and apps — w | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `labelId` | string | Yes | ID of the label whose assets to list | | `offset` | string | No | Index of the first result to return. Default 0 | | `limit` | string | No | Number of assets to return | @@ -1180,8 +1104,6 @@ Assign forms, workflows, sheets, or apps to a Jotform label. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `labelId` | string | Yes | ID of the label to assign the assets to | | `resources` | json | Yes | Assets to add, each with an id and a type of form, workflow, sheet, or portal, e.g. \[\{"id":"251464995493876","type":"form"\}\] | @@ -1205,8 +1127,6 @@ Unassign forms, workflows, sheets, or apps from a Jotform label. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `labelId` | string | Yes | ID of the label to remove the assets from | | `resources` | json | Yes | Assets to remove, each with an id and a type of form, workflow, sheet, or portal, e.g. \[\{"id":"251464995493876","type":"form"\}\] | @@ -1230,8 +1150,6 @@ List the sub-users on the account with the forms and folders each one can reach, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | #### Output @@ -1257,8 +1175,6 @@ Read the account settings behind the API key, including time zone, language, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `settingsKey` | string | No | Single setting to read instead of the whole set, e.g. time_zone | #### Output @@ -1294,8 +1210,6 @@ Update account settings such as name, email, website, company, industry, or time | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Jotform API key | -| `region` | string | No | Jotform data residency region the API key belongs to: "us" \(default\), "eu", or "hipaa" | | `settingsName` | string | No | New display name on the account | | `email` | string | No | New account email address | | `website` | string | No | New website recorded on the account | diff --git a/apps/docs/content/docs/integrations/jupyter.mdx b/apps/docs/content/docs/integrations/jupyter.mdx index fea8fb677a2..a0e3e48f5e4 100644 --- a/apps/docs/content/docs/integrations/jupyter.mdx +++ b/apps/docs/content/docs/integrations/jupyter.mdx @@ -2,7 +2,7 @@ title: Jupyter description: Manage files, notebooks, kernels, and sessions on a Jupyter server integration: - type: "jupyter_v2" + type: "jupyter_v3" color: "#FFFFFF" --- @@ -35,8 +35,6 @@ List files, notebooks, and subdirectories at a path on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `path` | string | No | Directory path to list, relative to the server root. Leave blank for root. | #### Output @@ -67,8 +65,6 @@ Download a file as a stored file, or read structured notebook and directory cont | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `path` | string | Yes | Path of the file or notebook to read, relative to the server root | #### Output @@ -93,8 +89,6 @@ Create a file, notebook, or directory on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `path` | string | Yes | Path to create, relative to the server root | | `type` | string | Yes | Type of entry to create: file, notebook, or directory | | `content` | string | No | Content to write. For a file, plain text. For a notebook, a JSON-stringified nbformat document \(defaults to an empty notebook\). Ignored for directories. | @@ -121,8 +115,6 @@ Upload a file to a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `directory` | string | No | Destination directory, relative to the server root. Leave blank to upload to the root directory. | | `file` | file | No | The file to upload \(UserFile object\) | | `fileName` | string | No | Optional filename override | @@ -148,8 +140,6 @@ Rename or move a file, notebook, or directory on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `path` | string | Yes | Current path of the entry, relative to the server root | | `newPath` | string | Yes | New path for the entry, relative to the server root | @@ -173,8 +163,6 @@ Delete a file, notebook, or directory on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `path` | string | Yes | Path of the entry to delete, relative to the server root | #### Output @@ -196,8 +184,6 @@ Duplicate a file or notebook into a directory on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `path` | string | Yes | Destination directory path, relative to the server root | | `copyFromPath` | string | Yes | Path of the file or notebook to copy, relative to the server root | @@ -221,8 +207,6 @@ List running kernels on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | #### Output @@ -247,8 +231,6 @@ Start a new kernel on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `kernelName` | string | No | Kernel spec name to start \(e.g. python3\). Defaults to the server default. | #### Output @@ -273,8 +255,6 @@ Shut down a running kernel on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `kernelId` | string | Yes | ID of the kernel to shut down | #### Output @@ -296,8 +276,6 @@ Restart a running kernel on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `kernelId` | string | Yes | ID of the kernel to restart | #### Output @@ -322,8 +300,6 @@ Interrupt a running kernel on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `kernelId` | string | Yes | ID of the kernel to interrupt | #### Output @@ -345,8 +321,6 @@ List available kernel specs (languages/runtimes) on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | #### Output @@ -372,8 +346,6 @@ List active sessions (notebook-to-kernel bindings) on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | #### Output @@ -403,8 +375,6 @@ Create a session that binds a notebook path to a (new or existing) kernel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `path` | string | Yes | Notebook path to bind the session to, relative to the server root | | `kernelName` | string | No | Kernel spec name to start for this session \(e.g. python3\) | | `name` | string | No | Optional session name | @@ -437,8 +407,6 @@ Delete a session and shut down its associated kernel on a Jupyter server | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `serverUrl` | string | Yes | Base URL of the Jupyter server \(e.g. http://localhost:8888) | -| `token` | string | Yes | Jupyter server authentication token | | `sessionId` | string | Yes | ID of the session to delete | #### Output diff --git a/apps/docs/content/docs/integrations/lambda.mdx b/apps/docs/content/docs/integrations/lambda.mdx index d66bbe257ab..bce1356987f 100644 --- a/apps/docs/content/docs/integrations/lambda.mdx +++ b/apps/docs/content/docs/integrations/lambda.mdx @@ -2,7 +2,7 @@ title: Lambda description: Invoke, deploy, and manage AWS Lambda functions integration: - type: "lambda" + type: "lambda_v2" color: "linear-gradient(45deg, #C8511B 0%, #FF9900 100%)" --- @@ -26,7 +26,7 @@ With the Lambda integration, you can: ## Credentials and permissions -The block authenticates with an AWS access key ID and secret access key scoped to a region. Grant the IAM principal only the Lambda actions the operations you use require — for example `lambda:InvokeFunction` for invocation, `lambda:GetFunction` and `lambda:ListFunctions` for read-only inventory, or `lambda:UpdateFunctionCode` and `lambda:PublishVersion` for deployments. +The block signs in with a saved [AWS credential](/integrations/aws-credentials) and runs in the region you set. Grant the IAM principal only the Lambda actions the operations you use require — for example `lambda:InvokeFunction` for invocation, `lambda:GetFunction` and `lambda:ListFunctions` for read-only inventory, or `lambda:UpdateFunctionCode` and `lambda:PublishVersion` for deployments. ## Deployment packages @@ -37,7 +37,7 @@ Use CloudWatch for function logs and metrics, S3 for deployment archives, and SQ ## Use the block [#usage-instructions] -Invoke functions and read their response payload, create and update functions from Amazon S3 packages or container images, publish versions and aliases, wire up event source mappings, manage concurrency, function URLs, layers, permissions, and tags. Requires an AWS access key and secret access key. +Invoke functions and read their response payload, create and update functions from Amazon S3 packages or container images, publish versions and aliases, wire up event source mappings, manage concurrency, function URLs, layers, permissions, and tags. ## Actions @@ -52,8 +52,6 @@ Invoke a Lambda function synchronously for its response payload, asynchronously | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `payload` | json | No | JSON event payload passed to the function handler | | `invocationType` | string | No | RequestResponse waits for the result, Event queues the invocation, DryRun only validates permissions | @@ -84,8 +82,6 @@ List Lambda functions with the version-specific configuration of each | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionVersion` | string | No | Set to ALL to include every published version of each function | | `masterRegion` | string | No | For Lambda@Edge functions, the region of the master function. Requires functionVersion ALL | | `marker` | string | No | Pagination token returned by a previous request | @@ -111,8 +107,6 @@ Get a function's configuration, code location, tags, and reserved concurrency | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -139,8 +133,6 @@ Get a function's version-specific configuration | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -163,8 +155,6 @@ Create a Lambda function from a deployment package in Amazon S3 or a container i | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `role` | string | Yes | ARN of the function's execution role | | `runtime` | string | No | Runtime identifier such as nodejs22.x or python3.13. Required for .zip packages, omit for container images | @@ -212,8 +202,6 @@ Update a function's deployment package from Amazon S3 or a container image | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `s3Bucket` | string | No | Amazon S3 bucket holding the new deployment package, in the same region as the function | | `s3Key` | string | No | Amazon S3 key of the .zip package | @@ -244,8 +232,6 @@ Update a function's settings such as memory, timeout, role, and environment vari | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `role` | string | No | ARN of the function's execution role | | `runtime` | string | No | Runtime identifier such as nodejs22.x or python3.13 | @@ -285,8 +271,6 @@ Delete a Lambda function, or a single published version of it | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number to delete. Omit to delete the whole function including all versions and aliases | @@ -309,8 +293,6 @@ Publish an immutable version from the current code and configuration of a functi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `codeSha256` | string | No | Publish only if the SHA256 hash of the deployment package matches this value | | `description` | string | No | Description of the version | @@ -335,8 +317,6 @@ List the published versions of a Lambda function, plus $LATEST | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `marker` | string | No | Pagination token returned by a previous request | | `maxItems` | number | No | Maximum number of items to return \(1-10000\) | @@ -361,8 +341,6 @@ Create an alias that points to a published function version | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `aliasName` | string | Yes | Name of the alias, such as prod or staging | | `aliasFunctionVersion` | string | Yes | Function version the alias points to | @@ -388,8 +366,6 @@ Get details about a Lambda function alias | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `aliasName` | string | Yes | Name of the alias | @@ -412,8 +388,6 @@ Update the target version, description, or traffic weights of an alias | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `aliasName` | string | Yes | Name of the alias | | `aliasFunctionVersion` | string | No | Function version the alias should point to | @@ -440,8 +414,6 @@ Delete a Lambda function alias | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `aliasName` | string | Yes | Name of the alias | @@ -464,8 +436,6 @@ List the aliases of a Lambda function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `aliasFunctionVersion` | string | No | Return only aliases that point to this function version | | `marker` | string | No | Pagination token returned by a previous request | @@ -491,8 +461,6 @@ Grant an AWS service, account, or organization permission to use a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `statementId` | string | Yes | Unique identifier for the policy statement \(letters, numbers, hyphens, and underscores\) | | `action` | string | Yes | Action the principal is granted, such as lambda:InvokeFunction | @@ -524,8 +492,6 @@ Remove a statement from a function's resource-based policy | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `statementId` | string | Yes | Identifier of the policy statement to remove | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -550,8 +516,6 @@ Get the resource-based IAM policy attached to a function, version, or alias | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -575,8 +539,6 @@ Map an event source such as SQS, Kinesis, DynamoDB Streams, or Kafka to a functi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `eventSourceArn` | string | No | ARN of the event source. Omit only for self-managed Kafka | | `enabled` | boolean | No | Whether the mapping is active | @@ -625,8 +587,6 @@ Get details about an event source mapping | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `uuid` | string | Yes | Identifier of the event source mapping | #### Output @@ -648,8 +608,6 @@ Update the batching, retry, filtering, or enabled state of an event source mappi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `uuid` | string | Yes | Identifier of the event source mapping | | `functionName` | string | No | Function the mapping should invoke | | `enabled` | boolean | No | Whether the mapping is active | @@ -692,8 +650,6 @@ Delete an event source mapping | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `uuid` | string | Yes | Identifier of the event source mapping | #### Output @@ -715,8 +671,6 @@ List event source mappings, optionally filtered by function or event source | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | No | Return only mappings that invoke this function | | `eventSourceArn` | string | No | Return only mappings for this event source ARN | | `marker` | string | No | Pagination token returned by a previous request | @@ -742,8 +696,6 @@ Get the reserved concurrency configured for a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | #### Output @@ -765,8 +717,6 @@ Reserve a share of the account concurrency limit for a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `reservedConcurrentExecutions` | number | Yes | Number of simultaneous executions to reserve for this function | @@ -789,8 +739,6 @@ Remove the reserved concurrency configuration from a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | #### Output @@ -812,8 +760,6 @@ Get the provisioned concurrency configuration of a function version or alias | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | Yes | Version number or alias name the configuration applies to | @@ -836,8 +782,6 @@ Allocate provisioned concurrency to a function version or alias | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | Yes | Version number or alias name the configuration applies to | | `provisionedConcurrentExecutions` | number | Yes | Number of pre-initialized execution environments to allocate | @@ -861,8 +805,6 @@ Remove the provisioned concurrency configuration from a function version or alia | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | Yes | Version number or alias name the configuration applies to | @@ -885,8 +827,6 @@ List the provisioned concurrency configurations of a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `marker` | string | No | Pagination token returned by a previous request | | `maxItems` | number | No | Maximum number of items to return \(1-50\) | @@ -911,8 +851,6 @@ Create a dedicated HTTPS endpoint for a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `authType` | string | Yes | AWS_IAM requires signed requests, NONE allows public unauthenticated access | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -943,8 +881,6 @@ Get details about a function URL | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -967,8 +903,6 @@ Update the auth type, invoke mode, or CORS settings of a function URL | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `authType` | string | No | AWS_IAM requires signed requests, NONE allows public unauthenticated access | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -999,8 +933,6 @@ Delete the URL configuration of a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -1023,8 +955,6 @@ List the URL configurations of a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `marker` | string | No | Pagination token returned by a previous request | | `maxItems` | number | No | Maximum number of items to return \(1-50\) | @@ -1049,8 +979,6 @@ Get the asynchronous invocation retry and destination settings of a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -1073,8 +1001,6 @@ Configure retry limits and destinations for asynchronous invocations of a functi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | | `maximumRetryAttempts` | number | No | Times Lambda retries a failed asynchronous invocation \(0-2\) | @@ -1101,8 +1027,6 @@ Remove the asynchronous invocation configuration of a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -1125,8 +1049,6 @@ List the asynchronous invocation configurations of a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `marker` | string | No | Pagination token returned by a previous request | | `maxItems` | number | No | Maximum number of items to return \(1-50\) | @@ -1151,8 +1073,6 @@ List Lambda layers and the latest version of each | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `compatibleRuntime` | string | No | Return only layers compatible with this runtime, such as python3.13 | | `compatibleArchitecture` | string | No | Return only layers compatible with this instruction set architecture | | `marker` | string | No | Pagination token returned by a previous request | @@ -1178,8 +1098,6 @@ List the versions of a Lambda layer | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `layerName` | string | Yes | The name or ARN of the layer | | `compatibleRuntime` | string | No | Return only versions compatible with this runtime, such as python3.13 | | `compatibleArchitecture` | string | No | Return only versions compatible with this instruction set architecture | @@ -1206,8 +1124,6 @@ Get details and a download link for a specific layer version | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `layerName` | string | Yes | The name or ARN of the layer | | `versionNumber` | number | Yes | Version number of the layer | @@ -1230,8 +1146,6 @@ List the tags applied to a Lambda function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `resourceArn` | string | Yes | The function's Amazon Resource Name \(ARN\) | #### Output @@ -1253,8 +1167,6 @@ Add tags to a Lambda function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `resourceArn` | string | Yes | The function's Amazon Resource Name \(ARN\) | | `tags` | json | Yes | Tags to apply, as a flat key/value JSON object | @@ -1277,8 +1189,6 @@ Remove tags from a Lambda function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `resourceArn` | string | Yes | The function's Amazon Resource Name \(ARN\) | | `tagKeys` | array | Yes | Tag keys to remove | @@ -1301,8 +1211,6 @@ Get the Lambda limits and usage of the current AWS account and region | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | #### Output @@ -1324,8 +1232,6 @@ Get the recursive loop detection setting of a function | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | #### Output @@ -1347,8 +1253,6 @@ Set whether Lambda stops a function that invokes itself recursively | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `recursiveLoop` | string | Yes | Terminate stops the function after 16 recursive invocations, Allow permits recursion | @@ -1371,8 +1275,6 @@ Get the runtime update policy of a function version | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `qualifier` | string | No | Version number or alias name to act on. Omit to target the function itself | @@ -1397,8 +1299,6 @@ Set how and when Lambda applies runtime updates to a function version | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | -| `awsAccessKeyId` | string | Yes | AWS access key ID | -| `awsSecretAccessKey` | string | Yes | AWS secret access key | | `functionName` | string | Yes | Function name, ARN, or partial ARN \(e.g. my-function, or arn:aws:lambda:us-east-1:123456789012:function:my-function\) | | `updateRuntimeOn` | string | Yes | Auto applies updates automatically, FunctionUpdate applies them on the next function update, Manual pins a runtime version | | `runtimeVersionArn` | string | No | ARN of the runtime version to pin to. Required when updateRuntimeOn is Manual | diff --git a/apps/docs/content/docs/integrations/langsmith.mdx b/apps/docs/content/docs/integrations/langsmith.mdx index 9424423025c..70f961cf5fa 100644 --- a/apps/docs/content/docs/integrations/langsmith.mdx +++ b/apps/docs/content/docs/integrations/langsmith.mdx @@ -2,7 +2,7 @@ title: LangSmith description: Forward workflow runs to LangSmith for observability integration: - type: "langsmith" + type: "langsmith_v2" color: "#181C1E" --- @@ -29,7 +29,6 @@ Forward a single run to LangSmith for ingestion. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LangSmith API key | | `id` | string | No | Unique run identifier | | `name` | string | Yes | Run name | | `run_type` | string | Yes | Run type \(tool, chain, llm, retriever, embedding, prompt, parser\) | @@ -68,7 +67,6 @@ Forward multiple runs to LangSmith in a single batch. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LangSmith API key | | `post` | json | No | Array of new runs to ingest | | `patch` | json | No | Array of runs to update/patch | @@ -93,7 +91,6 @@ Patch an existing LangSmith run with outputs, status, or timing once it complete | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LangSmith API key | | `runId` | string | Yes | ID of the run to update | | `name` | string | No | Corrected run name | | `end_time` | string | No | Run end time in ISO-8601 format | @@ -120,7 +117,6 @@ Retrieve a single LangSmith run by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LangSmith API key | | `runId` | string | Yes | ID of the run to retrieve | #### Output @@ -156,7 +152,6 @@ Attach a score, correction, or comment to a LangSmith run. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LangSmith API key | | `runId` | string | Yes | ID of the run to attach feedback to | | `key` | string | Yes | Feedback metric name \(e.g. "correctness", "user_score"\) | | `score` | number | No | Numeric score for the feedback metric | diff --git a/apps/docs/content/docs/integrations/launchdarkly.mdx b/apps/docs/content/docs/integrations/launchdarkly.mdx index d7445234528..8351557e2a4 100644 --- a/apps/docs/content/docs/integrations/launchdarkly.mdx +++ b/apps/docs/content/docs/integrations/launchdarkly.mdx @@ -2,7 +2,7 @@ title: LaunchDarkly description: Manage feature flags with LaunchDarkly integration: - type: "launchdarkly" + type: "launchdarkly_v2" color: "#191919" --- @@ -21,7 +21,7 @@ Create a personal access token or service token under **Organization settings > ## Use the block [#usage-instructions] -List, create, update, toggle, and delete feature flags. Read projects, environments, segments, members, and audit logs. Requires an API key. +List, create, update, toggle, and delete feature flags. Read projects, environments, segments, members, and audit logs. ## Actions @@ -35,7 +35,6 @@ Create a new feature flag in a LaunchDarkly project. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `projectKey` | string | Yes | The project key to create the flag in | | `name` | string | Yes | Human-readable name for the feature flag | | `key` | string | Yes | Unique key for the feature flag \(used in code\) | @@ -75,7 +74,6 @@ Delete a feature flag from a LaunchDarkly project. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `projectKey` | string | Yes | The project key | | `flagKey` | string | Yes | The feature flag key to delete | @@ -97,7 +95,6 @@ List audit log entries from your LaunchDarkly account. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `limit` | number | No | Maximum number of entries to return \(default 10, max 20\) | | `spec` | string | No | Resource specifier filter \(e.g. "proj/*:env/*:flag/*" for all flag changes, or "proj/default:env/production:flag/my-flag" for one flag in one environment\) | @@ -129,7 +126,6 @@ Get a single feature flag by key from a LaunchDarkly project. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `projectKey` | string | Yes | The project key | | `flagKey` | string | Yes | The feature flag key | | `environmentKey` | string | No | Filter flag configuration to a specific environment | @@ -167,7 +163,6 @@ Get the status of a feature flag across environments (active, inactive, launched | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `projectKey` | string | Yes | The project key | | `flagKey` | string | Yes | The feature flag key | | `environmentKey` | string | Yes | The environment key | @@ -192,7 +187,6 @@ List environments in a LaunchDarkly project. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `projectKey` | string | Yes | The project key to list environments for | | `limit` | number | No | Maximum number of environments to return \(default 20\) | @@ -222,7 +216,6 @@ List feature flags in a LaunchDarkly project. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `projectKey` | string | Yes | The project key to list flags for | | `environmentKey` | string | No | Filter flag configurations to a specific environment | | `tag` | string | No | Filter flags by tag name | @@ -262,7 +255,6 @@ List account members in your LaunchDarkly organization. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `limit` | number | No | Maximum number of members to return \(default 20\) | #### Output @@ -292,7 +284,6 @@ List all projects in your LaunchDarkly account. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `limit` | number | No | Maximum number of projects to return \(default 20\) | #### Output @@ -318,7 +309,6 @@ List user segments in a LaunchDarkly project and environment. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `projectKey` | string | Yes | The project key | | `environmentKey` | string | Yes | The environment key | | `limit` | number | No | Maximum number of segments to return \(default 20\) | @@ -350,7 +340,6 @@ Toggle a feature flag on or off in a specific LaunchDarkly environment. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `projectKey` | string | Yes | The project key | | `flagKey` | string | Yes | The feature flag key to toggle | | `environmentKey` | string | Yes | The environment key to toggle the flag in | @@ -389,7 +378,6 @@ Update feature flag metadata (name, description, tags, temporary, archived) usin | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | LaunchDarkly API key | | `projectKey` | string | Yes | The project key | | `flagKey` | string | Yes | The feature flag key to update | | `updateName` | string | No | New name for the flag | diff --git a/apps/docs/content/docs/integrations/lemlist.mdx b/apps/docs/content/docs/integrations/lemlist.mdx index b6477687b85..02cbb9ce0ce 100644 --- a/apps/docs/content/docs/integrations/lemlist.mdx +++ b/apps/docs/content/docs/integrations/lemlist.mdx @@ -2,7 +2,7 @@ title: Lemlist description: Manage outreach activities, leads, and send emails via Lemlist integration: - type: "lemlist" + type: "lemlist_v2" color: "#316BFF" --- @@ -31,7 +31,6 @@ Retrieves campaign activities and steps performed, including email opens, clicks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Lemlist API key | | `type` | string | No | Filter by activity type \(e.g., emailOpened, emailClicked, emailReplied, paused\) | | `campaignId` | string | No | Filter by campaign ID \(e.g., "cam_abc123def456"\) | | `leadId` | string | No | Filter by lead ID \(e.g., "lea_abc123def456"\) | @@ -65,7 +64,6 @@ Retrieves lead information by email address or lead ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Lemlist API key | | `leadIdentifier` | string | Yes | Lead email address \(e.g., "john@example.com"\) or lead ID \(e.g., "lea_abc123def456"\) | #### Output @@ -96,7 +94,6 @@ Sends an email to a contact through the Lemlist inbox. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Lemlist API key | | `sendUserId` | string | Yes | Identifier for the user sending the message \(e.g., "usr_abc123def456"\) | | `sendUserEmail` | string | Yes | Email address of the sender \(e.g., "sales@company.com"\) | | `sendUserMailboxId` | string | Yes | Mailbox identifier for the sender \(e.g., "mbx_abc123def456"\) | diff --git a/apps/docs/content/docs/integrations/linq.mdx b/apps/docs/content/docs/integrations/linq.mdx index e47fbd52339..2290e2ec39f 100644 --- a/apps/docs/content/docs/integrations/linq.mdx +++ b/apps/docs/content/docs/integrations/linq.mdx @@ -2,7 +2,7 @@ title: Linq description: Send iMessage, SMS, and RCS messages and manage conversations with Linq integration: - type: "linq" + type: "linq_v2" color: "#000000" --- @@ -19,7 +19,7 @@ Check iMessage or RCS capability before selecting a preferred service. Upload at ## Use the block [#usage-instructions] -Reach people on iMessage, SMS, and RCS through Linq. Start chats, send messages with media, links, effects, and replies, send voice memos, react with tapbacks, manage group participants, check iMessage/RCS capability, configure contact cards, and subscribe to webhook events — all through a single Linq API key. +Reach people on iMessage, SMS, and RCS through Linq. Start chats, send messages with media, links, effects, and replies, send voice memos, react with tapbacks, manage group participants, check iMessage/RCS capability, configure contact cards, and subscribe to webhook events. ## Actions @@ -33,7 +33,6 @@ Add a participant to a group chat (3+ existing participants) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the group chat | | `handle` | string | Yes | Phone number \(E.164 format\) or email address of the participant to add | @@ -57,7 +56,6 @@ Check whether an address (phone number or email) supports iMessage | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `address` | string | Yes | Phone number \(E.164 format\) or email address to check | | `from` | string | No | Sender phone number to check from \(defaults to an available number\) | @@ -80,7 +78,6 @@ Check whether an address (phone number or email) supports RCS | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `address` | string | Yes | Phone number \(E.164 format\) to check | | `from` | string | No | Sender phone number to check from \(defaults to an available number\) | @@ -103,7 +100,6 @@ Upload a file to Linq as a reusable attachment (max 100MB) and get an attachment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `file` | file | No | File to upload \(a UserFile from a file-upload field or a previous block\) | | `filename` | string | No | Override the file name \(defaults to the uploaded file name\) | | `contentType` | string | No | Override the MIME type \(defaults to the uploaded file type\) | @@ -131,7 +127,6 @@ Start a new iMessage, SMS, or RCS chat and send the first message | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `from` | string | Yes | Sender phone number in E.164 format \(e.g. +14155551234\) | | `to` | array | Yes | Recipient handles \(phone numbers in E.164 format or email addresses\) | | `text` | string | No | Text content of the first message. Optional, but at least one of text, media, attachment, or link is required | @@ -168,7 +163,6 @@ Set up a contact card (Name and Photo Sharing) for a phone number | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `phoneNumber` | string | Yes | Phone number in E.164 format the card applies to | | `firstName` | string | Yes | First name to display | | `lastName` | string | No | Last name to display | @@ -196,7 +190,6 @@ Subscribe an HTTPS endpoint to Linq webhook events | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `targetUrl` | string | Yes | HTTPS endpoint that will receive webhook events | | `subscribedEvents` | array | Yes | Event types to subscribe to \(e.g. message.sent, message.delivered\) | | `phoneNumbers` | array | No | E.164 phone numbers to filter events by \(omit for all numbers\) | @@ -226,7 +219,6 @@ Permanently delete an attachment owned by your account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `attachmentId` | string | Yes | The unique identifier of the attachment to delete | #### Output @@ -247,7 +239,6 @@ Delete a message from the Linq API only (does not unsend it; recipients still se | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `messageId` | string | Yes | The unique identifier of the message to delete | #### Output @@ -268,7 +259,6 @@ Delete a webhook subscription from your account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `subscriptionId` | string | Yes | The unique identifier of the webhook subscription to delete | #### Output @@ -289,7 +279,6 @@ Edit the text of a sent message (up to 5 times, within 15 minutes of sending; iM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `messageId` | string | Yes | The unique identifier of the message to edit | | `text` | string | Yes | New text content for the message part | | `partIndex` | number | No | Index of the message part to edit \(defaults to 0\) | @@ -323,7 +312,6 @@ Retrieve metadata for an attachment, including its download URL and status | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `attachmentId` | string | Yes | The unique identifier of the attachment | #### Output @@ -350,7 +338,6 @@ Retrieve a chat by ID, including participants and line health | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the chat | #### Output @@ -379,7 +366,6 @@ Retrieve contact cards, optionally filtered by phone number | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `phoneNumber` | string | No | E.164 phone number to filter by \(omit to return all cards\) | #### Output @@ -405,7 +391,6 @@ Retrieve a single message by ID, including parts, reactions, and delivery status | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `messageId` | string | Yes | The unique identifier of the message | #### Output @@ -437,7 +422,6 @@ Retrieve a webhook subscription by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `subscriptionId` | string | Yes | The unique identifier of the webhook subscription | #### Output @@ -464,7 +448,6 @@ Leave an iMessage group chat (4+ active participants; not supported for direct c | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the group chat | #### Output @@ -487,7 +470,6 @@ List chats, optionally filtered by sender or participant handle | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `from` | string | No | Filter by sender phone number in E.164 format | | `to` | string | No | Filter by participant handle \(phone number or email\) | | `limit` | number | No | Results per page \(default 20, max 100\) | @@ -512,7 +494,6 @@ List messages in a chat with pagination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the chat | | `limit` | number | No | Maximum number of messages to return | | `cursor` | string | No | Pagination cursor from a previous response | @@ -536,7 +517,6 @@ List all phone numbers assigned to your partner account, with line health | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | #### Output @@ -560,7 +540,6 @@ List all messages in the thread that contains a given message | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `messageId` | string | Yes | The ID of any message in the thread | | `order` | string | No | Sort order: asc \(oldest first\) or desc \(newest first\) | | `limit` | number | No | Maximum number of messages to return | @@ -585,7 +564,6 @@ List all webhook event types available to subscribe to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | #### Output @@ -606,7 +584,6 @@ List all webhook subscriptions on your account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | #### Output @@ -633,7 +610,6 @@ Mark messages in a chat as read (only applies to 1:1 iMessage/RCS; no effect on | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the chat | #### Output @@ -654,7 +630,6 @@ Add or remove a tapback reaction on a message | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `messageId` | string | Yes | The unique identifier of the message to react to | | `operation` | string | Yes | Whether to add or remove the reaction: add or remove | | `type` | string | Yes | Reaction type: love, like, dislike, laugh, emphasize, question, custom, or sticker | @@ -681,7 +656,6 @@ Remove a participant from a group chat (minimum 3 participants must remain) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the group chat | | `handle` | string | Yes | Phone number \(E.164 format\) or email address of the participant to remove | @@ -705,7 +679,6 @@ Send a message to an existing chat, with optional media, link, effect, or reply | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the chat | | `text` | string | No | Text content of the message. Optional, but at least one of text, media, attachment, or link is required | | `mediaUrl` | string | No | Optional publicly accessible HTTPS URL of an image, video, or file to attach | @@ -741,7 +714,6 @@ Send a voice memo to a chat from a URL or a pre-uploaded attachment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the chat | | `voiceMemoUrl` | string | No | Publicly accessible HTTPS URL of the audio file \(MP3, M4A, AAC, CAF, WAV, AIFF, AMR\) | | `attachmentId` | string | No | ID of a pre-uploaded audio attachment \(use instead of voiceMemoUrl\) | @@ -769,7 +741,6 @@ Share your configured contact card (Name and Photo Sharing) with a chat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the chat | #### Output @@ -790,7 +761,6 @@ Show a typing indicator in a one-on-one chat (iMessage only, not group chats) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the chat | #### Output @@ -811,7 +781,6 @@ Stop the typing indicator in a one-on-one chat (iMessage only, not group chats) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the chat | #### Output @@ -832,7 +801,6 @@ Update chat properties such as group display name and icon | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `chatId` | string | Yes | The unique identifier of the chat | | `displayName` | string | No | New display name for the group chat | | `groupChatIcon` | string | No | New group chat icon \(publicly accessible image URL\) | @@ -856,7 +824,6 @@ Partially update an existing active contact card for a phone number | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `phoneNumber` | string | Yes | Phone number in E.164 format identifying the card to update | | `firstName` | string | No | New first name | | `lastName` | string | No | New last name | @@ -884,7 +851,6 @@ Update a webhook subscription (target URL, events, phone filter, or active state | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Linq API key | | `subscriptionId` | string | Yes | The unique identifier of the webhook subscription | | `targetUrl` | string | No | New HTTPS endpoint that will receive events | | `subscribedEvents` | array | No | New set of event types to subscribe to | diff --git a/apps/docs/content/docs/integrations/logfire.mdx b/apps/docs/content/docs/integrations/logfire.mdx index 6c6b3a0b056..78af2860fee 100644 --- a/apps/docs/content/docs/integrations/logfire.mdx +++ b/apps/docs/content/docs/integrations/logfire.mdx @@ -2,7 +2,7 @@ title: Logfire description: Query traces, logs, and metrics in Pydantic Logfire integration: - type: "logfire" + type: "logfire_v2" color: "#000000" --- @@ -14,7 +14,7 @@ Use [Pydantic Logfire](https://pydantic.dev/logfire) to query telemetry collecte Trace lookup returns spans ordered by time with parent-child relationships for request investigation. Credential lookup identifies the organization and project targeted by a read token. -Create a project **read token** under **Settings → Read tokens**. Sim detects the cloud region from its prefix. For a self-hosted instance, set **Host** to a publicly resolvable HTTPS base URL that Sim can reach. +Create a project **read token** under **Settings → Read tokens**. Sim detects the cloud region from its prefix. For a self-hosted instance, enter a publicly resolvable HTTPS base URL that Sim can reach as the credential's region or host (the legacy block's **Host** field). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -33,9 +33,6 @@ Run a read-only SQL query against Logfire traces, logs, and metrics. Reads the r | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Logfire read token | -| `region` | string | No | Logfire data region: auto, us, or eu. Auto reads the region from the token. | -| `host` | string | No | Base URL of a self-hosted Logfire instance, e.g. https://logfire.example.com. Overrides region. Leave blank for Logfire Cloud. | | `sql` | string | Yes | SQL SELECT query to run against the records or metrics table | | `minTimestamp` | string | No | ISO 8601 lower bound on start_timestamp. Defaults to 2020-01-01T00:00:00Z when omitted. | | `maxTimestamp` | string | No | ISO 8601 upper bound on start_timestamp | @@ -66,9 +63,6 @@ Search Logfire spans and logs using structured filters for message text, service | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Logfire read token | -| `region` | string | No | Logfire data region: auto, us, or eu. Auto reads the region from the token. | -| `host` | string | No | Base URL of a self-hosted Logfire instance, e.g. https://logfire.example.com. Overrides region. Leave blank for Logfire Cloud. | | `query` | string | No | Case-insensitive text to match within the record message | | `service` | string | No | Exact service name to filter on | | `spanName` | string | No | Exact span name to filter on | @@ -114,9 +108,6 @@ Fetch every span and log belonging to a Logfire trace, ordered from earliest to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Logfire read token | -| `region` | string | No | Logfire data region: auto, us, or eu. Auto reads the region from the token. | -| `host` | string | No | Base URL of a self-hosted Logfire instance, e.g. https://logfire.example.com. Overrides region. Leave blank for Logfire Cloud. | | `traceId` | string | Yes | 32-character hexadecimal trace identifier | | `minTimestamp` | string | No | ISO 8601 lower bound on start_timestamp. Defaults to 2020-01-01T00:00:00Z when omitted. | | `maxTimestamp` | string | No | ISO 8601 upper bound on start_timestamp | @@ -157,9 +148,6 @@ Resolve which Logfire organization and project a read token belongs to. Useful f | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Logfire read token | -| `region` | string | No | Logfire data region: auto, us, or eu. Auto reads the region from the token. | -| `host` | string | No | Base URL of a self-hosted Logfire instance, e.g. https://logfire.example.com. Overrides region. Leave blank for Logfire Cloud. | #### Output diff --git a/apps/docs/content/docs/integrations/loops.mdx b/apps/docs/content/docs/integrations/loops.mdx index bb6ddc19b55..45c5c9cc551 100644 --- a/apps/docs/content/docs/integrations/loops.mdx +++ b/apps/docs/content/docs/integrations/loops.mdx @@ -2,7 +2,7 @@ title: Loops description: Manage contacts and send emails with Loops integration: - type: "loops" + type: "loops_v2" color: "#FAFAF9" --- @@ -29,7 +29,6 @@ Create a new contact in your Loops audience with an email address and optional p | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `email` | string | Yes | The email address for the new contact | | `firstName` | string | No | The contact first name | | `lastName` | string | No | The contact last name | @@ -59,7 +58,6 @@ Update an existing contact in Loops by email or userId. Creates a new contact if | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `email` | string | No | The contact email address \(at least one of email or userId is required\) | | `userId` | string | No | The contact userId \(at least one of email or userId is required\) | | `firstName` | string | No | The contact first name | @@ -89,7 +87,6 @@ Find a contact in Loops by email address or userId. Returns an array of matching | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `email` | string | No | The contact email address to search for \(at least one of email or userId is required\) | | `userId` | string | No | The contact userId to search for \(at least one of email or userId is required\) | @@ -121,7 +118,6 @@ Delete a contact from Loops by email address or userId. At least one identifier | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `email` | string | No | The email address of the contact to delete \(at least one of email or userId is required\) | | `userId` | string | No | The userId of the contact to delete \(at least one of email or userId is required\) | @@ -144,7 +140,6 @@ Send a transactional email to a recipient using a Loops template. Supports dynam | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `email` | string | Yes | The email address of the recipient | | `transactionalId` | string | Yes | The ID of the transactional email template to send | | `dataVariables` | json | No | Template data variables as key-value pairs \(string or number values\) | @@ -169,7 +164,6 @@ Send an event to Loops to trigger automated email sequences for a contact. Ident | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `email` | string | No | The email address of the contact \(at least one of email or userId is required\) | | `userId` | string | No | The userId of the contact \(at least one of email or userId is required\) | | `eventName` | string | Yes | The name of the event to trigger | @@ -194,7 +188,6 @@ Retrieve all mailing lists from your Loops account. Returns each list with its I | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | #### Output @@ -218,7 +211,6 @@ Retrieve a list of published transactional email templates from your Loops accou | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `perPage` | string | No | Number of results per page \(10-50, default: 20\) | | `cursor` | string | No | Pagination cursor from a previous response to fetch the next page | @@ -253,7 +245,6 @@ Create a new custom contact property in your Loops account. The property name mu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `name` | string | Yes | The property name in camelCase format \(e.g., "favoriteColor"\) | | `type` | string | Yes | The property data type \(e.g., "string", "number", "boolean", "date"\) | @@ -275,7 +266,6 @@ Retrieve a list of contact properties from your Loops account. Returns each prop | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `list` | string | No | Filter type: "all" for all properties \(default\) or "custom" for custom properties only | #### Output @@ -299,7 +289,6 @@ Check whether a Loops contact is on the suppression list (bounced, complained, o | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `email` | string | No | The contact email address to check \(at least one of email or userId is required\) | | `userId` | string | No | The contact userId to check \(at least one of email or userId is required\) | @@ -326,7 +315,6 @@ Remove a Loops contact from the suppression list by email address or userId, all | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `email` | string | No | The contact email address to remove from suppression \(at least one of email or userId is required\) | | `userId` | string | No | The contact userId to remove from suppression \(at least one of email or userId is required\) | @@ -351,7 +339,6 @@ Retrieve a single transactional email template from your Loops account by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Loops API key for authentication | | `transactionalId` | string | Yes | The ID of the transactional email template to retrieve | #### Output diff --git a/apps/docs/content/docs/integrations/luma.mdx b/apps/docs/content/docs/integrations/luma.mdx index 569755306ea..e5c350cbf43 100644 --- a/apps/docs/content/docs/integrations/luma.mdx +++ b/apps/docs/content/docs/integrations/luma.mdx @@ -2,7 +2,7 @@ title: Luma description: Manage events and guests on Luma integration: - type: "luma" + type: "luma_v2" color: "#000000" --- @@ -31,7 +31,6 @@ Retrieve details of a Luma event including name, time, location, hosts, and visi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `eventId` | string | Yes | Event ID \(starts with evt-\) | #### Output @@ -76,7 +75,6 @@ Create a new event on Luma with a name, start time, timezone, and optional detai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `name` | string | Yes | Event name/title | | `startAt` | string | Yes | Event start time in ISO 8601 format \(e.g., 2025-03-15T18:00:00Z\) | | `timezone` | string | Yes | IANA timezone \(e.g., America/New_York, Europe/London\) | @@ -129,7 +127,6 @@ Update an existing Luma event. Only the fields you provide will be changed; all | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `eventId` | string | Yes | Event ID to update \(starts with evt-\) | | `name` | string | No | New event name/title | | `startAt` | string | No | New start time in ISO 8601 format \(e.g., 2025-03-15T18:00:00Z\) | @@ -183,7 +180,6 @@ List events from your Luma calendar with optional date range filtering, sorting, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `after` | string | No | Return events after this ISO 8601 datetime \(e.g., 2025-01-01T00:00:00Z\) | | `before` | string | No | Return events before this ISO 8601 datetime \(e.g., 2025-12-31T23:59:59Z\) | | `paginationLimit` | number | No | Maximum number of events to return per page | @@ -228,7 +224,6 @@ Look up an event by its public URL or event ID to resolve its canonical ID, API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `url` | string | No | Public event URL on lu.ma \(provide this or an event ID\) | | `eventId` | string | No | Event ID to look up \(starts with evt-\). Provide this or a URL. | | `platform` | string | No | Event platform to look up: luma or external \(defaults to luma\) | @@ -254,7 +249,6 @@ Cancel a Luma event. This is irreversible and notifies all registered guests. Re | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `eventId` | string | Yes | Event ID to cancel \(starts with evt-\) | | `cancellationToken` | string | Yes | Cancellation token from the Request Event Cancellation endpoint \(POST /v1/event/cancel/request\) | | `shouldRefund` | boolean | No | Whether to refund paid guests. Required if the event has paid registrations. | @@ -277,7 +271,6 @@ Retrieve the guest list for a Luma event with optional filtering by approval sta | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `eventId` | string | Yes | Event ID \(starts with evt-\) | | `approvalStatus` | string | No | Filter by approval status: approved, session, pending_approval, invited, declined, or waitlist | | `paginationLimit` | number | No | Maximum number of guests to return per page | @@ -316,7 +309,6 @@ Retrieve a single guest's details on a Luma event, including approval status, re | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `eventId` | string | Yes | Event ID the guest belongs to \(starts with evt-\) | | `guestIdentifier` | string | Yes | Guest ID \(gst-...\), guest key \(g-...\), ticket key, or the guest's email address | @@ -349,7 +341,6 @@ Add guests to a Luma event by email. Guests are added with Going (approved) stat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `eventId` | string | Yes | Event ID \(starts with evt-\) | | `guests` | string | Yes | JSON array of guest objects. Each guest requires an "email" field and optionally "name", "first_name", "last_name". Example: \[\{"email": "user@example.com", "name": "John Doe"\}\] | @@ -371,7 +362,6 @@ Send email invitations to guests for a Luma event. Unlike Add Guests (which regi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `eventId` | string | Yes | Event ID to invite guests to \(starts with evt-\) | | `guests` | string | Yes | JSON array of guest objects. Each guest requires an "email" field and optionally "name". Example: \[\{"email": "user@example.com", "name": "John Doe"\}\] | | `message` | string | No | Optional custom message included in the invite email \(max 200 characters\) | @@ -394,7 +384,6 @@ Update a guest's approval status on a Luma event — approve, decline, waitlist, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Luma API key | | `eventId` | string | No | Event ID the guest belongs to \(starts with evt-\) | | `guestIdentifier` | string | Yes | Guest email address or guest ID \(gst-...\). Values containing '@' are treated as emails; otherwise as a guest ID. | | `status` | string | Yes | New approval status: approved, declined, pending_approval, or waitlist | diff --git a/apps/docs/content/docs/integrations/mailchimp.mdx b/apps/docs/content/docs/integrations/mailchimp.mdx index 15e0b60c3a3..f77a710ed18 100644 --- a/apps/docs/content/docs/integrations/mailchimp.mdx +++ b/apps/docs/content/docs/integrations/mailchimp.mdx @@ -2,7 +2,7 @@ title: Mailchimp description: Manage audiences, campaigns, and marketing automation in Mailchimp integration: - type: "mailchimp" + type: "mailchimp_v2" color: "#FFE01B" --- @@ -35,7 +35,6 @@ Retrieve a list of audiences (lists) from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | | `offset` | string | No | Number of results to skip for pagination | @@ -61,7 +60,6 @@ Retrieve details of a specific audience (list) from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | #### Output @@ -85,7 +83,6 @@ Create a new audience (list) in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `audienceName` | string | Yes | The name of the audience/list \(e.g., "Newsletter Subscribers"\) | | `contact` | string | Yes | JSON object of contact information \(e.g., \{"company": "Acme", "address1": "123 Main St", "city": "NYC", "state": "NY", "zip": "10001", "country": "US"\}\) | | `permissionReminder` | string | Yes | Permission reminder text shown to subscribers \(e.g., "You signed up for updates on our website"\) | @@ -114,7 +111,6 @@ Update an existing audience (list) in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `audienceName` | string | No | The name of the audience/list \(e.g., "Newsletter Subscribers"\) | | `permissionReminder` | string | No | Permission reminder text shown to subscribers \(e.g., "You signed up for updates on our website"\) | @@ -143,7 +139,6 @@ Delete an audience (list) from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list to delete \(e.g., "abc123def4"\) | #### Output @@ -164,7 +159,6 @@ Retrieve a list of members from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `status` | string | No | Filter by status: "subscribed", "unsubscribed", "cleaned", or "pending" | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | @@ -192,7 +186,6 @@ Retrieve details of a specific member from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | @@ -217,7 +210,6 @@ Add a new member to a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `emailAddress` | string | Yes | Member email address \(e.g., "user@example.com"\) | | `status` | string | Yes | Subscriber status: "subscribed", "unsubscribed", "cleaned", "pending", or "transactional" | @@ -246,7 +238,6 @@ Add a new member or update an existing member in a Mailchimp audience (upsert) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | | `emailAddress` | string | Yes | Member email address \(e.g., "user@example.com"\) | @@ -276,7 +267,6 @@ Update an existing member in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | | `emailAddress` | string | No | New member email address \(e.g., "user@example.com"\) | @@ -306,7 +296,6 @@ Archive a member from a Mailchimp audience without permanently deleting their da | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | @@ -328,7 +317,6 @@ Request permanent deletion of a member and their identifying data. This targets | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | @@ -352,7 +340,6 @@ Restore an archived member to a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | | `emailAddress` | string | Yes | Member email address \(e.g., "user@example.com"\) | @@ -380,7 +367,6 @@ Retrieve a list of campaigns from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignType` | string | No | Filter by campaign type: "regular", "plaintext", "absplit", "rss", or "variate" | | `status` | string | No | Filter by status: "save", "paused", "schedule", "sending", or "sent" | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | @@ -408,7 +394,6 @@ Retrieve details of a specific campaign from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign \(e.g., "abc123def4"\) | #### Output @@ -432,7 +417,6 @@ Create a new campaign in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignType` | string | Yes | Campaign type: "regular", "plaintext", "absplit", "rss", or "variate" | | `campaignSettings` | string | Yes | JSON object of campaign settings \(e.g., \{"subject_line": "Newsletter", "from_name": "Acme", "reply_to": "news@acme.com"\}\) | | `recipients` | string | No | JSON object of recipients \(e.g., \{"list_id": "abc123"\}\) | @@ -459,7 +443,6 @@ Update an existing campaign in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign \(e.g., "abc123def4"\) | | `campaignSettings` | string | No | JSON object of campaign settings \(e.g., \{"subject_line": "Newsletter", "from_name": "Acme"\}\) | | `recipients` | string | No | JSON object of recipients \(e.g., \{"list_id": "abc123"\}\) | @@ -486,7 +469,6 @@ Delete a campaign from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign to delete \(e.g., "abc123def4"\) | #### Output @@ -507,7 +489,6 @@ Send a Mailchimp campaign | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign to send \(e.g., "abc123def4"\) | #### Output @@ -530,7 +511,6 @@ Schedule a Mailchimp campaign to be sent at a specific time | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign to schedule \(e.g., "abc123def4"\) | | `scheduleTime` | string | Yes | Schedule time in ISO 8601 format \(e.g., "2024-12-25T10:00:00Z"\) | @@ -552,7 +532,6 @@ Unschedule a previously scheduled Mailchimp campaign | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign to unschedule \(e.g., "abc123def4"\) | #### Output @@ -575,7 +554,6 @@ Create a copy of an existing Mailchimp campaign | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign to replicate \(e.g., "abc123def4"\) | #### Output @@ -600,7 +578,6 @@ Retrieve the HTML and plain-text content for a Mailchimp campaign | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign \(e.g., "abc123def4"\) | #### Output @@ -623,7 +600,6 @@ Set the content for a Mailchimp campaign | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign \(e.g., "abc123def4"\) | | `html` | string | No | The HTML content for the campaign | | `plainText` | string | No | The plain-text content for the campaign | @@ -650,7 +626,6 @@ Retrieve a list of automations from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | | `offset` | string | No | Number of results to skip for pagination | @@ -676,7 +651,6 @@ Retrieve details of a specific automation from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `workflowId` | string | Yes | The unique ID for the automation workflow \(e.g., "abc123def4"\) | #### Output @@ -700,7 +674,6 @@ Start all emails in a Mailchimp automation workflow | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `workflowId` | string | Yes | The unique ID for the automation workflow \(e.g., "abc123def4"\) | #### Output @@ -723,7 +696,6 @@ Pause all emails in a Mailchimp automation workflow | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `workflowId` | string | Yes | The unique ID for the automation workflow \(e.g., "abc123def4"\) | #### Output @@ -746,7 +718,6 @@ Manually add a subscriber to a workflow email queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `workflowId` | string | Yes | The unique ID for the automation workflow \(e.g., "abc123def4"\) | | `workflowEmailId` | string | Yes | The unique ID for the workflow email \(e.g., "xyz789"\) | | `emailAddress` | string | Yes | Email address of the subscriber \(e.g., "user@example.com"\) | @@ -772,7 +743,6 @@ Retrieve a list of templates from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | | `offset` | string | No | Number of results to skip for pagination | @@ -798,7 +768,6 @@ Retrieve details of a specific template from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `templateId` | string | Yes | The unique ID for the template \(e.g., "12345"\) | #### Output @@ -822,7 +791,6 @@ Create a new template in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `templateName` | string | Yes | The name of the template \(e.g., "Monthly Newsletter"\) | | `templateHtml` | string | Yes | The HTML content for the template | @@ -848,7 +816,6 @@ Update an existing template in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `templateId` | string | Yes | The unique ID for the template \(e.g., "12345"\) | | `templateName` | string | No | The name of the template \(e.g., "Monthly Newsletter"\) | | `templateHtml` | string | No | The HTML content for the template | @@ -875,7 +842,6 @@ Delete a template from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `templateId` | string | Yes | The unique ID for the template to delete \(e.g., "12345"\) | #### Output @@ -896,7 +862,6 @@ Retrieve a list of campaign reports from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | | `offset` | string | No | Number of results to skip for pagination | @@ -922,7 +887,6 @@ Retrieve the report for a specific campaign from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `campaignId` | string | Yes | The unique ID for the campaign \(e.g., "abc123def4"\) | #### Output @@ -946,7 +910,6 @@ Retrieve a list of segments from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | | `offset` | string | No | Number of results to skip for pagination | @@ -973,7 +936,6 @@ Retrieve details of a specific segment from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `segmentId` | string | Yes | The unique ID for the segment \(e.g., "12345"\) | @@ -998,7 +960,6 @@ Create a new segment in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `segmentName` | string | Yes | The name of the segment \(e.g., "VIP Customers"\) | | `segmentOptions` | string | No | JSON object of segment options for saved segments \(e.g., \{"match": "all", "conditions": \[...\]\}\) | @@ -1025,7 +986,6 @@ Update an existing segment in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `segmentId` | string | Yes | The unique ID for the segment \(e.g., "12345"\) | | `segmentName` | string | No | The name of the segment \(e.g., "VIP Customers"\) | @@ -1053,7 +1013,6 @@ Delete a segment from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `segmentId` | string | Yes | The unique ID for the segment to delete \(e.g., "12345"\) | @@ -1075,7 +1034,6 @@ Retrieve members of a specific segment from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `segmentId` | string | Yes | The unique ID for the segment \(e.g., "12345"\) | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | @@ -1103,7 +1061,6 @@ Add a member to a specific segment in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `segmentId` | string | Yes | The unique ID for the segment \(e.g., "12345"\) | | `emailAddress` | string | Yes | Email address of the member \(e.g., "user@example.com"\) | @@ -1129,7 +1086,6 @@ Remove a member from a specific segment in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `segmentId` | string | Yes | The unique ID for the segment \(e.g., "12345"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | @@ -1154,7 +1110,6 @@ Retrieve tags associated with a member in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | @@ -1180,7 +1135,6 @@ Add tags to a member in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | | `tags` | string | Yes | JSON array of tag objects \(e.g., \[\{"name": "VIP", "status": "active"\}\]\) | @@ -1205,7 +1159,6 @@ Remove tags from a member in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `subscriberEmail` | string | Yes | Member email address or MD5 hash of the lowercase email | | `tags` | string | Yes | JSON array of tag objects with inactive status \(e.g., \[\{"name": "VIP", "status": "inactive"\}\]\) | @@ -1230,7 +1183,6 @@ Retrieve a list of merge fields from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | | `offset` | string | No | Number of results to skip for pagination | @@ -1257,7 +1209,6 @@ Retrieve details of a specific merge field from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `mergeId` | string | Yes | The unique ID for the merge field \(e.g., "1" or "FNAME"\) | @@ -1282,7 +1233,6 @@ Create a new merge field in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `mergeName` | string | Yes | The name of the merge field \(e.g., "First Name"\) | | `mergeType` | string | Yes | The type of the merge field: "text", "number", "address", "phone", "date", "url", "imageurl", "radio", "dropdown", "birthday", or "zip" | @@ -1309,7 +1259,6 @@ Update an existing merge field in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `mergeId` | string | Yes | The unique ID for the merge field \(e.g., "1" or "FNAME"\) | | `mergeName` | string | No | The name of the merge field \(e.g., "First Name"\) | @@ -1336,7 +1285,6 @@ Delete a merge field from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `mergeId` | string | Yes | The unique ID for the merge field to delete \(e.g., "1" or "FNAME"\) | @@ -1358,7 +1306,6 @@ Retrieve a list of interest categories from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | | `offset` | string | No | Number of results to skip for pagination | @@ -1385,7 +1332,6 @@ Retrieve details of a specific interest category from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `interestCategoryId` | string | Yes | The unique ID for the interest category \(e.g., "xyz789"\) | @@ -1410,7 +1356,6 @@ Create a new interest category in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `interestCategoryTitle` | string | Yes | The title of the interest category \(e.g., "Email Preferences"\) | | `interestCategoryType` | string | Yes | The type of interest category: "checkboxes", "dropdown", "radio", or "hidden" | @@ -1437,7 +1382,6 @@ Update an existing interest category in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `interestCategoryId` | string | Yes | The unique ID for the interest category \(e.g., "xyz789"\) | | `interestCategoryTitle` | string | No | The title of the interest category \(e.g., "Email Preferences"\) | @@ -1464,7 +1408,6 @@ Delete an interest category from a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `interestCategoryId` | string | Yes | The unique ID for the interest category to delete \(e.g., "xyz789"\) | @@ -1486,7 +1429,6 @@ Retrieve a list of interests from an interest category in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `interestCategoryId` | string | Yes | The unique ID for the interest category \(e.g., "xyz789"\) | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | @@ -1514,7 +1456,6 @@ Retrieve details of a specific interest from an interest category in a Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `interestCategoryId` | string | Yes | The unique ID for the interest category \(e.g., "xyz789"\) | | `interestId` | string | Yes | The unique ID for the interest \(e.g., "def456"\) | @@ -1540,7 +1481,6 @@ Create a new interest in an interest category in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `interestCategoryId` | string | Yes | The unique ID for the interest category \(e.g., "xyz789"\) | | `interestName` | string | Yes | The name of the interest \(e.g., "Weekly Updates"\) | @@ -1567,7 +1507,6 @@ Update an existing interest in an interest category in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `interestCategoryId` | string | Yes | The unique ID for the interest category \(e.g., "xyz789"\) | | `interestId` | string | Yes | The unique ID for the interest \(e.g., "def456"\) | @@ -1595,7 +1534,6 @@ Delete an interest from an interest category in a Mailchimp audience | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `listId` | string | Yes | The unique ID for the audience/list \(e.g., "abc123def4"\) | | `interestCategoryId` | string | Yes | The unique ID for the interest category \(e.g., "xyz789"\) | | `interestId` | string | Yes | The unique ID for the interest to delete \(e.g., "def456"\) | @@ -1618,7 +1556,6 @@ Retrieve a list of landing pages from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | | `offset` | string | No | Number of results to skip for pagination | @@ -1644,7 +1581,6 @@ Retrieve details of a specific landing page from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `pageId` | string | Yes | The unique ID for the landing page \(e.g., "abc123def4"\) | #### Output @@ -1668,7 +1604,6 @@ Create a new landing page in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `landingPageType` | string | Yes | The type of landing page: "signup" | | `landingPageTitle` | string | No | The title of the landing page \(e.g., "Join Our Newsletter"\) | @@ -1694,7 +1629,6 @@ Update an existing landing page in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `pageId` | string | Yes | The unique ID for the landing page \(e.g., "abc123def4"\) | | `landingPageTitle` | string | No | The title of the landing page \(e.g., "Join Our Newsletter"\) | @@ -1720,7 +1654,6 @@ Delete a landing page from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `pageId` | string | Yes | The unique ID for the landing page to delete \(e.g., "abc123def4"\) | #### Output @@ -1741,7 +1674,6 @@ Publish a landing page in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `pageId` | string | Yes | The unique ID for the landing page \(e.g., "abc123def4"\) | #### Output @@ -1764,7 +1696,6 @@ Unpublish a landing page in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `pageId` | string | Yes | The unique ID for the landing page \(e.g., "abc123def4"\) | #### Output @@ -1787,7 +1718,6 @@ Retrieve a list of batch operations from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `count` | string | No | Number of results to return \(default: 10, max: 1000\) | | `offset` | string | No | Number of results to skip for pagination | @@ -1813,7 +1743,6 @@ Retrieve details of a specific batch operation from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `batchId` | string | Yes | The unique ID for the batch operation \(e.g., "abc123def4"\) | #### Output @@ -1837,7 +1766,6 @@ Create a new batch operation in Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `operations` | string | Yes | JSON array of batch operations \(e.g., \[\{"method": "POST", "path": "/lists/\{list_id\}/members", "body": "..."\}\]\) | #### Output @@ -1862,7 +1790,6 @@ Delete a batch operation from Mailchimp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailchimp API key with server prefix | | `batchId` | string | Yes | The unique ID for the batch operation to delete \(e.g., "abc123def4"\) | #### Output diff --git a/apps/docs/content/docs/integrations/mailgun.mdx b/apps/docs/content/docs/integrations/mailgun.mdx index 85fd0f03684..fb9d98f75e3 100644 --- a/apps/docs/content/docs/integrations/mailgun.mdx +++ b/apps/docs/content/docs/integrations/mailgun.mdx @@ -2,7 +2,7 @@ title: Mailgun description: Send emails and manage mailing lists with Mailgun integration: - type: "mailgun" + type: "mailgun_v2" color: "#C12126" --- @@ -29,8 +29,6 @@ Send an email using Mailgun API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailgun API key | -| `region` | string | No | Mailgun account region: "us" \(default\) or "eu" | | `domain` | string | Yes | Mailgun sending domain \(e.g., mg.example.com\) | | `from` | string | Yes | Sender email address \(e.g., sender@example.com or "Name <sender@example.com>"\) | | `to` | string | Yes | Recipient email address \(e.g., user@example.com\). Use comma-separated values for multiple recipients | @@ -61,8 +59,6 @@ Retrieve a stored message by its key | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailgun API key | -| `region` | string | No | Mailgun account region: "us" \(default\) or "eu" | | `domain` | string | Yes | Mailgun domain for retrieving messages \(e.g., mg.example.com\) | | `messageKey` | string | Yes | Message storage key | @@ -96,8 +92,6 @@ List events (logs) for messages sent through Mailgun | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailgun API key | -| `region` | string | No | Mailgun account region: "us" \(default\) or "eu" | | `domain` | string | Yes | Mailgun domain for listing events \(e.g., mg.example.com\) | | `event` | string | No | Filter by event type \(accepted, delivered, failed, opened, clicked, etc.\) | | `limit` | number | No | Maximum number of events to return \(default: 100\) | @@ -122,8 +116,6 @@ Create a new mailing list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailgun API key | -| `region` | string | No | Mailgun account region: "us" \(default\) or "eu" | | `address` | string | Yes | Mailing list address \(e.g., newsletter@mg.example.com\) | | `name` | string | No | Mailing list name | | `description` | string | No | Mailing list description | @@ -149,8 +141,6 @@ Get details of a mailing list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailgun API key | -| `region` | string | No | Mailgun account region: "us" \(default\) or "eu" | | `address` | string | Yes | Mailing list address to retrieve \(e.g., newsletter@mg.example.com\) | #### Output @@ -172,8 +162,6 @@ Add a member to a mailing list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailgun API key | -| `region` | string | No | Mailgun account region: "us" \(default\) or "eu" | | `listAddress` | string | Yes | Mailing list address \(e.g., list@mg.example.com\) | | `address` | string | Yes | Member email address to add \(e.g., user@example.com\) | | `name` | string | No | Member name | @@ -200,8 +188,6 @@ List all domains for your Mailgun account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailgun API key | -| `region` | string | No | Mailgun account region: "us" \(default\) or "eu" | #### Output @@ -223,8 +209,6 @@ Get details of a specific domain | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Mailgun API key | -| `region` | string | No | Mailgun account region: "us" \(default\) or "eu" | | `domain` | string | Yes | Domain name to retrieve details for \(e.g., mg.example.com\) | #### Output diff --git a/apps/docs/content/docs/integrations/mem0.mdx b/apps/docs/content/docs/integrations/mem0.mdx index 74e3aebe6da..33fa41bc6fa 100644 --- a/apps/docs/content/docs/integrations/mem0.mdx +++ b/apps/docs/content/docs/integrations/mem0.mdx @@ -2,7 +2,7 @@ title: Mem0 description: Agent memory management integration: - type: "mem0" + type: "mem0_v2" color: "#181C1E" --- @@ -31,7 +31,6 @@ Add memories to Mem0 for persistent storage and retrieval | --------- | ---- | -------- | ----------- | | `userId` | string | Yes | User ID associated with the memory \(e.g., "user_123", "alice@example.com"\) | | `messages` | json | Yes | Array of message objects with role and content \(e.g., \[\{"role": "user", "content": "Hello"\}\]\) | -| `apiKey` | string | Yes | Your Mem0 API key | #### Output @@ -56,7 +55,6 @@ Search for memories in Mem0 using semantic search | `userId` | string | Yes | User ID to search memories for \(e.g., "user_123", "alice@example.com"\) | | `query` | string | Yes | Search query to find relevant memories \(e.g., "What are my favorite foods?"\) | | `limit` | number | No | Maximum number of results to return \(e.g., 10, 50, 100\) | -| `apiKey` | string | Yes | Your Mem0 API key | #### Output @@ -95,7 +93,6 @@ Retrieve memories from Mem0 by ID or filter criteria | `endDate` | string | No | End date for filtering by created_at \(e.g., "2024-12-31"\) | | `limit` | number | No | Maximum number of results to return \(e.g., 10, 50, 100\) | | `page` | number | No | Page number to retrieve for paginated list results | -| `apiKey` | string | Yes | Your Mem0 API key | #### Output diff --git a/apps/docs/content/docs/integrations/meta.json b/apps/docs/content/docs/integrations/meta.json index 15dd2516e0a..1193d5113c4 100644 --- a/apps/docs/content/docs/integrations/meta.json +++ b/apps/docs/content/docs/integrations/meta.json @@ -21,9 +21,11 @@ "atlassian-service-account", "attio", "attio-service-account", + "aws-credentials", "azure_data_explorer", "azure_devops", "bitbucket", + "bitbucket-service-account", "box", "box-service-account", "brandfetch", @@ -56,6 +58,7 @@ "cursor", "dagster", "databricks", + "databricks-credentials", "datadog", "datagma", "daytona", @@ -63,6 +66,7 @@ "devin", "discord", "docusign", + "docusign-service-account", "downdetector", "dropbox", "dropcontact", @@ -169,6 +173,7 @@ "manageengine_sdp", "mem0", "memory", + "microsoft-app-credentials", "microsoft_ad", "microsoft_dataverse", "microsoft_dynamics_365", diff --git a/apps/docs/content/docs/integrations/microsoft-app-credentials.mdx b/apps/docs/content/docs/integrations/microsoft-app-credentials.mdx new file mode 100644 index 00000000000..4e91652bc1d --- /dev/null +++ b/apps/docs/content/docs/integrations/microsoft-app-credentials.mdx @@ -0,0 +1,76 @@ +--- +title: Microsoft app credentials +description: Run Microsoft Entra ID, SharePoint, Dataverse, and Dynamics 365 blocks as an app registration with a client secret, a certificate, or a keyless federated credential that trusts Sim +--- + +import { Callout } from '@/components/ui/callout' +import { Step, Steps } from '@/components/ui/steps' + +A Microsoft app credential signs in as a Microsoft Entra app registration instead of a person (the OAuth 2.0 client credentials flow). One credential works for the **Azure AD** (Microsoft Entra ID), **SharePoint**, **Microsoft Dataverse**, and **Dynamics 365** blocks. Connecting a Microsoft account through OAuth keeps working exactly as before; the app credential is an additional option in the same account picker. + + +Outlook, OneDrive, Excel, Word, Planner, and Teams act on behalf of a signed-in person and don't accept an app credential. The Azure AD **Reset password** operation also needs a signed-in admin, because Microsoft Graph doesn't support application permissions for it. + + +## Choose how the app signs in + +| Option | What Sim stores | When to use it | +| --- | --- | --- | +| **Federated credential (keyless, via Sim OIDC)** | Nothing secret: the tenant ID, client ID, and this credential's subject | Recommended. No secret to rotate, expire, or leak. | +| **Client secret** | The tenant ID, client ID, and client secret | Quick setup. Secrets expire, so plan to rotate them. | +| **Certificate** | The tenant ID, client ID, the certificate, and its private key | When your organization requires certificate credentials. Sim signs each token request with the key. | + +Every option resolves on Sim's servers at run time, and tokens never appear in block outputs, logs, or anything a model sees. Azure AD and SharePoint blocks get a Microsoft Graph token; Dataverse and Dynamics 365 blocks get a token for the credential's own environment. + +## Register the app and grant permissions + + + + In the [Microsoft Entra admin center](https://entra.microsoft.com), go to **App registrations** → **New registration**. A single-tenant app is fine. Copy the **Application (client) ID** and **Directory (tenant) ID** from its Overview page. + + + Under **API permissions** → **Add a permission** → **Microsoft Graph** → **Application permissions**, add what your workflows need, then select **Grant admin consent**. Application permissions always need admin consent. For example: + - Azure AD: `User.Read.All` or `User.ReadWrite.All`, `Group.ReadWrite.All`, `GroupMember.ReadWrite.All`, `AuditLog.Read.All` for sign-in and audit logs, `Policy.Read.All` for conditional access, `UserAuthenticationMethod.Read.All` for authentication methods, `User.RevokeSessions.All` to revoke sessions. Setting a password needs `User-PasswordProfile.ReadWrite.All` **and** the app assigned at least the User Administrator role. + - SharePoint: `Sites.Read.All` or `Sites.ReadWrite.All`. Creating and editing pages needs `Sites.ReadWrite.All`. `Sites.Selected` limits the app to sites you grant it, but searching for sites doesn't work with it, so enter site IDs directly. + + + Dataverse and Dynamics 365 don't use API permissions. Instead, in the [Power Platform admin center](https://admin.powerplatform.microsoft.com), open the environment → **Settings** → **Users + permissions** → **Application users** → **New app user**, add this app, pick a business unit, and assign a security role. Then enter the environment URL (like `https://contoso.crm.dynamics.com`) on the credential. + + + +## Set up a keyless federated credential [#keyless-federated-credential] + + + + In Sim, add a **Microsoft app credential**, choose **Federated credential (keyless, via Sim OIDC)**, and enter the tenant ID and client ID. The dialog shows an **Issuer**, **Audience**, and **Subject**. + + + In the app registration, go to **Certificates & secrets** → **Federated credentials** → **Add credential** → **Other issuer**. Paste the **Issuer** and the **Subject identifier** exactly as shown, set the audience to `api://AzureADTokenExchange` (the dialog's value), and give it a name. Matching is case-sensitive. + + + Save in Sim. Sim requests a token as the app and reads the app's own service principal from Microsoft Graph, which needs no extra permission. It also checks that the app refuses a subject no credential holds, so only this credential can use the app. A new federated credential can take a few minutes to take effect; if saving fails with a matching error right after you add it, wait and try again. + + + +An app can have up to 20 federated credentials. Each Sim credential has its own subject, and reconnecting keeps the subject by default so the federated credential keeps working. Keyless credentials are available for workspace credentials, not organization credentials. + +### From the API or CLI [#keyless-api-cli] + +Run `sim credentials keyless-subject microsoft-app-service-account`, trust the printed subject as above, then add `tenantId` and `clientId` (optionally `environmentUrl`) to its `keylessConfig` and pass it to `sim credentials create microsoft-app-service-account`. See [the CLI credentials reference](/cli/credentials) for the full two-step example; the proof expires after 24 hours and works only for the user who requested it. + +## Use a client secret or certificate + +- **Client secret:** under **Certificates & secrets** → **Client secrets** → **New client secret**, copy the secret's **Value** (not the Secret ID). +- **Certificate:** upload the certificate's public part (`.cer`, `.pem`, or `.crt`) under **Certificates & secrets** → **Certificates**, then paste the certificate and its unencrypted PEM RSA private key (2048 to 4096 bits) into Sim. Sim checks the key belongs to the certificate before saving, and signs a PS256 assertion with the certificate's SHA-256 thumbprint. + +## Self-hosted deployments + +Keyless credentials need `WORKLOAD_IDENTITY_SIGNING_KEY` set and an `https` issuer Microsoft Entra can reach over the public internet. See [AWS credentials](/integrations/aws-credentials#self-hosted-deployments) for the same settings and for rotating the signing key. App credentials use the global Microsoft Entra endpoint (`login.microsoftonline.com`) and public-cloud Dataverse environments. + +## Troubleshooting + +- **"No federated credential on this app matches…":** the federated credential's issuer, subject, or audience differs from the dialog (check case and trailing slashes), it's on a different app than the client ID, or it was added moments ago. +- **"Microsoft Entra rejected the client secret":** you pasted the Secret ID, or the secret was deleted. **"This client secret has expired":** create a new one and reconnect. +- **"…could not verify the signed assertion":** the certificate in Sim isn't the one uploaded to the app registration. +- **"…the Dataverse environment did not accept it":** add the app as an application user of the environment with a security role. +- **A block fails with 403 "Insufficient privileges":** the app is missing an application permission for that operation, or admin consent wasn't granted after adding it. diff --git a/apps/docs/content/docs/integrations/microsoft_ad.mdx b/apps/docs/content/docs/integrations/microsoft_ad.mdx index 1ab1662a7b1..b3ba8497c9c 100644 --- a/apps/docs/content/docs/integrations/microsoft_ad.mdx +++ b/apps/docs/content/docs/integrations/microsoft_ad.mdx @@ -13,6 +13,8 @@ import { ReferenceDetails } from "@/components/ui/reference-details" Use [Microsoft Entra ID](https://entra.microsoft.com), formerly Azure Active Directory, to manage directory users, groups, memberships, licenses, passwords, sign-in sessions, app roles, and directory roles. Read sign-in and audit logs, registered devices, and conditional access policies; device writes are excluded. Use OData filters to query directory records. Grant the connection the permissions and administrative roles required by each operation before using it for onboarding, offboarding, or access review. Microsoft 365 groups have a 30-day recovery window. Cloud security-group recovery is a [public-cloud preview with tenant restrictions](https://learn.microsoft.com/en-us/entra/identity/users/groups-restore-deleted); confirm eligibility before deleting a group. + +To run as an app registration instead of a signed-in admin, use a [Microsoft app credential](/integrations/microsoft-app-credentials). {/* MANUAL-CONTENT-END */} ## OAuth scopes [#oauth-scopes] diff --git a/apps/docs/content/docs/integrations/microsoft_dataverse.mdx b/apps/docs/content/docs/integrations/microsoft_dataverse.mdx index e846aa23bc7..dc1510f15eb 100644 --- a/apps/docs/content/docs/integrations/microsoft_dataverse.mdx +++ b/apps/docs/content/docs/integrations/microsoft_dataverse.mdx @@ -11,6 +11,8 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} Use [Microsoft Dataverse](https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-platform-intro) to manage records and relationships in Dynamics 365, Power Platform, or custom environments. The block also supports queries, bulk operations, file transfer, metadata lookup, and Dataverse actions and functions. + +To run as an application user instead of a signed-in person, use a [Microsoft app credential](/integrations/microsoft-app-credentials). {/* MANUAL-CONTENT-END */} ## OAuth scopes [#oauth-scopes] diff --git a/apps/docs/content/docs/integrations/microsoft_dynamics_365.mdx b/apps/docs/content/docs/integrations/microsoft_dynamics_365.mdx index ad857b39fc2..a2e540d4a6a 100644 --- a/apps/docs/content/docs/integrations/microsoft_dynamics_365.mdx +++ b/apps/docs/content/docs/integrations/microsoft_dynamics_365.mdx @@ -18,6 +18,8 @@ Two block operations intentionally reuse the new CRM tools under a simpler CRM l - **Assign Record** updates the selected record's `ownerid@odata.bind` to an explicitly chosen user or team. Connect one credential per Dynamics environment. The credential is bound to that environment during Microsoft OAuth, and CRM requests refuse a different environment. This release supports public-cloud Dataverse hosts only. + +To run as an application user instead of a signed-in person, use a [Microsoft app credential](/integrations/microsoft-app-credentials). {/* MANUAL-CONTENT-END */} ## OAuth scopes [#oauth-scopes] diff --git a/apps/docs/content/docs/integrations/mistral_parse.mdx b/apps/docs/content/docs/integrations/mistral_parse.mdx index b98396758f6..b87a88d1007 100644 --- a/apps/docs/content/docs/integrations/mistral_parse.mdx +++ b/apps/docs/content/docs/integrations/mistral_parse.mdx @@ -1,6 +1,6 @@ --- title: Mistral Parser -description: Extract text from PDF documents +description: Extract text, tables, and images from PDF documents integration: type: "mistral_parse_v3" color: "#000000" @@ -10,12 +10,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Mistral's OCR API](https://mistral.ai/) to extract text and page structure from a PDF uploaded to Sim or supplied by URL. The current parser returns page Markdown, image metadata, the model, and usage information. **Specific Pages** accepts zero-based indices such as `0,1,2`; leave it empty to process all pages. The block offers Markdown, plain-text, and JSON format values, but its current result uses the structured page output documented below. Base64 image data is a hidden tool input and has no block control. +Use [Mistral's OCR API](https://mistral.ai/) to extract text and page structure from a PDF uploaded to Sim or passed in as a file from a previous block. The current parser returns page Markdown, image metadata, the model, and usage information. **Specific Pages** accepts zero-based indices such as `0,1,2`; leave it empty to process all pages. The block offers Markdown, plain-text, and JSON format values, but its current result uses the structured page output documented below. Turn on **Extract Images** to get the images Mistral finds in the document (figures, logos, photos) as files on the `images` output, up to 100 per parse; each page's image entry carries a `file_name` matching its file. **Max Images** and **Min Image Size** (advanced) narrow what is extracted. **Tables** keeps tables inline in the page Markdown by default, or returns them separately as Markdown or HTML in each page's `tables` list. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Extract text from uploaded PDF documents, or from a URL. +Extract text, tables, and images from an uploaded PDF or a file from a previous block. Images come back as files an agent can pass on. ## Actions @@ -31,7 +31,11 @@ Parse PDF documents using Mistral OCR API | --------- | ---- | -------- | ----------- | | `file` | file | Yes | Normalized UserFile from file upload or file reference | | `resultType` | string | No | Legacy parser output format \(markdown, text, or json; default markdown\). Versions 2 and 3 return structured OCR pages regardless of this value. | +| `includeImageBase64` | boolean | No | Extract the images in the document \(figures, logos, photos\) as files on the images output | | `pages` | array | No | Specific pages to process \(array of page numbers, starting from 0\) | +| `imageLimit` | number | No | Maximum number of images to extract \(at most 100\) | +| `imageMinSize` | number | No | Minimum height and width of images to extract from the PDF | +| `tableFormat` | string | No | Return tables separately on each page as markdown or html instead of inline in the page markdown | | `apiKey` | string | Yes | Mistral API key \(MISTRAL_API_KEY\) | #### Output @@ -47,13 +51,13 @@ Parse PDF documents using Mistral OCR API | ↳ `top_left_y` | number | Top-left Y coordinate in pixels | | ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels | | ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels | -| ↳ `image_base64` | string | Base64-encoded image data; returned only when the hidden includeImageBase64 input is enabled | +| ↳ `file_name` | string | Name of this image in the images output; present only when image extraction is on | | ↳ `dimensions` | object | Page dimensions | | ↳ `dpi` | number | Dots per inch | | ↳ `height` | number | Page height in pixels | | ↳ `width` | number | Page width in pixels | -| ↳ `tables` | array | Separate table objects, referenced from the markdown via placeholders like \[tbl-0.html\]. Mistral populates these only when table_format is "markdown" or "html"; Sim never sets it, so tables stay inline in the markdown and this list is empty | -| ↳ `hyperlinks` | array | Array of URL strings detected in the page \(e.g., \["https://...", "mailto:..."\]\) | +| ↳ `tables` | array | Separate table objects \(\{ id, content, format \}\), referenced from the markdown via placeholders like \[tbl-0.html\]. Populated only when tableFormat is markdown or html; otherwise tables stay inline in the markdown and this list is empty | +| ↳ `hyperlinks` | array | Array of URL strings detected in the page | | ↳ `header` | string | Page header content. Mistral returns it only when extract_header is true \(it defaults to false\); Sim never sets it, so this is not returned | | ↳ `footer` | string | Page footer content. Mistral returns it only when extract_footer is true \(it defaults to false\); Sim never sets it, so this is not returned | | `model` | string | Mistral OCR model identifier \(e.g., mistral-ocr-latest\) | @@ -61,6 +65,7 @@ Parse PDF documents using Mistral OCR API | ↳ `pages_processed` | number | Total number of pages processed | | ↳ `doc_size_bytes` | number | Document file size in bytes | | `document_annotation` | string | Structured annotation data as JSON string \(when applicable\) | +| `images` | file[] | Images extracted from the document, named page-<index>-<image id> with the extension of the detected image type; empty unless image extraction is on | diff --git a/apps/docs/content/docs/integrations/new_relic.mdx b/apps/docs/content/docs/integrations/new_relic.mdx index 1f137bd3f51..eba47ccd62c 100644 --- a/apps/docs/content/docs/integrations/new_relic.mdx +++ b/apps/docs/content/docs/integrations/new_relic.mdx @@ -2,7 +2,7 @@ title: New Relic description: Query observability data and record deployments in New Relic integration: - type: "new_relic" + type: "new_relic_v2" color: "#000000" --- @@ -31,8 +31,6 @@ Run a NRQL query against a New Relic account using NerdGraph. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | New Relic user API key for NerdGraph | -| `region` | string | No | New Relic data center region: us or eu | | `accountId` | number | Yes | New Relic account ID to query | | `nrql` | string | Yes | NRQL query to execute | | `timeout` | number | No | Optional query timeout in seconds | @@ -56,8 +54,6 @@ Search New Relic entities by name, GUID, domain type, tags, or reporting state. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | New Relic user API key for NerdGraph | -| `region` | string | No | New Relic data center region: us or eu | | `query` | string | Yes | Entity search query, for example: name like "api" or domainType = "APM-APPLICATION" | | `cursor` | string | No | Pagination cursor from a previous entity search | @@ -91,8 +87,6 @@ Fetch a New Relic entity by GUID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | New Relic user API key for NerdGraph | -| `region` | string | No | New Relic data center region: us or eu | | `guid` | string | Yes | Entity GUID | #### Output @@ -122,8 +116,6 @@ Record a deployment change event in New Relic change tracking. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | New Relic user API key for NerdGraph | -| `region` | string | No | New Relic data center region: us or eu | | `entityGuid` | string | Yes | GUID of the entity associated with the deployment | | `version` | string | Yes | Deployment version, release name, or commit SHA | | `shortDescription` | string | No | Short description of the deployment | diff --git a/apps/docs/content/docs/integrations/okta.mdx b/apps/docs/content/docs/integrations/okta.mdx index e844de7c47c..d5d2eeec9be 100644 --- a/apps/docs/content/docs/integrations/okta.mdx +++ b/apps/docs/content/docs/integrations/okta.mdx @@ -2,7 +2,7 @@ title: Okta description: Manage users, groups, apps, and MFA in Okta integration: - type: "okta" + type: "okta_v2" color: "#191919" --- @@ -31,8 +31,6 @@ List users in your Okta organization with optional search and filtering. Users w | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `search` | string | No | Okta search expression \(e.g., profile.firstName eq "John" or profile.email co "example.com"\) | | `filter` | string | No | Okta filter expression \(e.g., status eq "ACTIVE"\) | | `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call | @@ -74,8 +72,6 @@ Get a specific user by ID or login from your Okta organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | User ID or login \(email\) to look up | #### Output @@ -119,8 +115,6 @@ Create a new user in your Okta organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `firstName` | string | Yes | First name of the user | | `lastName` | string | Yes | Last name of the user | | `email` | string | Yes | Email address of the user | @@ -157,8 +151,6 @@ Update a user profile in your Okta organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | User ID or login to update | | `firstName` | string | No | Updated first name | | `lastName` | string | No | Updated last name | @@ -194,8 +186,6 @@ Activate a user in your Okta organization. Can only be performed on users with S | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | User ID or login to activate | | `sendEmail` | boolean | No | Send activation email to the user \(default: true\) | @@ -221,8 +211,6 @@ Deactivate a user in your Okta organization. This transitions the user to DEPROV | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | User ID or login to deactivate | | `sendEmail` | boolean | No | Send deactivation email to admin \(default: false\) | @@ -246,8 +234,6 @@ Suspend a user in your Okta organization. Only users with ACTIVE status can be s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | User ID or login to suspend | #### Output @@ -270,8 +256,6 @@ Unsuspend a previously suspended user in your Okta organization. Returns the use | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | User ID or login to unsuspend | #### Output @@ -294,8 +278,6 @@ Generate a one-time token to reset a user password. Can email the reset link to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | User ID or login to reset password for | | `sendEmail` | boolean | No | Send password reset email to the user. Okta requires this parameter and declares no default of its own; leaving it blank sends the email | @@ -319,8 +301,6 @@ Permanently delete a user from your Okta organization. Can only be performed on | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | User ID or login \(email\) to delete | | `sendEmail` | boolean | No | Send deactivation email to admin \(default: false\) | @@ -344,8 +324,6 @@ List all groups in your Okta organization with optional search and filtering | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `search` | string | No | Okta search expression for groups \(e.g., profile.name sw "Engineering" or type eq "OKTA_GROUP"\) | | `filter` | string | No | Okta filter expression \(e.g., type eq "OKTA_GROUP"\) | | `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call | @@ -380,8 +358,6 @@ Get a specific group by ID from your Okta organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupId` | string | Yes | Group ID to look up | #### Output @@ -409,8 +385,6 @@ Create a new group in your Okta organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `name` | string | Yes | Name of the group | | `description` | string | No | Description of the group | @@ -439,8 +413,6 @@ Update a group profile in your Okta organization. Only groups of OKTA_GROUP type | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupId` | string | Yes | Group ID to update | | `name` | string | No | Updated group name. Leave blank to keep the stored name | | `description` | string | No | Updated group description | @@ -470,8 +442,6 @@ Delete a group from your Okta organization. Groups of OKTA_GROUP or APP_GROUP ty | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupId` | string | Yes | Group ID to delete | #### Output @@ -494,8 +464,6 @@ Add a user to a group in your Okta organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupId` | string | Yes | Group ID to add the user to | | `userId` | string | Yes | User ID to add to the group | @@ -520,8 +488,6 @@ Remove a user from a group in your Okta organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupId` | string | Yes | Group ID to remove the user from | | `userId` | string | Yes | User ID to remove from the group | @@ -546,8 +512,6 @@ List all members of a specific group in your Okta organization | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupId` | string | Yes | Group ID to list members for | | `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call | | `limit` | number | No | Maximum number of members to return per page \(default: 1000, but Okta recommends 200\) | @@ -588,8 +552,6 @@ List the group rules in your Okta organization. Each rule assigns users to group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `search` | string | No | Keyword to search group rules for | | `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call | | `limit` | number | No | Maximum number of rules to return \(default: 50, max: 200\) | @@ -627,8 +589,6 @@ Retrieve a single Okta group rule by ID, including the expression that decides w | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupRuleId` | string | Yes | Group rule ID to look up | #### Output @@ -660,8 +620,6 @@ Create a group rule that automatically assigns users matching an Okta expression | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `ruleName` | string | Yes | Name for the group rule \(maximum 50 characters\) | | `expression` | string | Yes | Okta expression that must evaluate to a boolean \(e.g., user.department=="Engineering"\) | | `assignUserToGroupIds` | string | Yes | Comma-separated group IDs that matching users are assigned to | @@ -696,8 +654,6 @@ Activate a group rule so Okta starts applying it, assigning every matching user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupRuleId` | string | Yes | Group rule ID to activate | #### Output @@ -720,8 +676,6 @@ Deactivate a group rule so Okta stops applying it. Existing memberships the rule | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupRuleId` | string | Yes | Group rule ID to deactivate | #### Output @@ -744,8 +698,6 @@ Permanently delete a group rule. Destructive and irreversible. Optionally also r | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `groupRuleId` | string | Yes | Group rule ID to delete | | `removeUsers` | boolean | No | Also remove the users this rule assigned from the groups it targeted \(default: false\) | @@ -769,8 +721,6 @@ List the MFA factors a user has enrolled, with each factor type, provider, and e | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | Okta user ID \(not a login or email\) to list enrolled factors for | #### Output @@ -801,8 +751,6 @@ Retrieve a single enrolled MFA factor for a user, including its type, provider, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | Okta user ID \(not a login or email\) the factor belongs to | | `factorId` | string | Yes | Factor ID to look up | @@ -832,8 +780,6 @@ Enroll an MFA factor for a user. The profile fields required depend on the facto | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | Okta user ID \(not a login or email\) to enroll the factor for | | `factorType` | string | Yes | Factor type to enroll \(sms, call, email, question, push, token:software:totp, u2f, webauthn\) | | `provider` | string | Yes | Factor provider \(OKTA, GOOGLE, FIDO, DUO, RSA, SYMANTEC, YUBICO, CUSTOM\). Each provider supports a subset of factor types | @@ -870,8 +816,6 @@ Unenroll one specific MFA factor for a user so they can re-enroll it. Destructiv | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | Okta user ID \(not a login or email\) the factor belongs to | | `factorId` | string | Yes | Factor ID to unenroll | | `removeRecoveryEnrollment` | boolean | No | Also remove the phone number as a recovery method, not only as a factor. Applies to sms and call factors only \(default: false\) | @@ -897,8 +841,6 @@ Reset every MFA factor for a user, returning all enrollments to the unenrolled s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | User ID or login whose MFA factors will all be reset | #### Output @@ -921,8 +863,6 @@ Revoke every active Okta session for a user, signing them out of all devices imm | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | Okta user ID \(not a login or email\) whose sessions will be revoked | | `oauthTokens` | boolean | No | Also revoke the user OpenID Connect and OAuth refresh and access tokens \(default: false\) | | `forgetDevices` | boolean | No | Clear the user remembered factors for all devices \(default: true\) | @@ -947,8 +887,6 @@ Retrieve an Okta session by ID, including who it belongs to, when it expires, an | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `sessionId` | string | Yes | Session ID to look up | #### Output @@ -980,8 +918,6 @@ Revoke a single Okta session by ID, ending that sign-in immediately. Destructive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `sessionId` | string | Yes | Session ID to revoke | #### Output @@ -1004,8 +940,6 @@ List the applications configured in your Okta organization, with optional name s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `q` | string | No | Search for applications whose name or label starts with this value | | `filter` | string | No | Okta filter expression \(e.g., status eq "ACTIVE"\) | | `includeNonDeleted` | boolean | No | Also return inactive applications. Deleted applications stay excluded either way \(default: false\) | @@ -1042,8 +976,6 @@ Retrieve a single Okta application by ID, including its sign-on mode, status, en | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `appId` | string | Yes | Application ID to look up | #### Output @@ -1085,8 +1017,6 @@ List the users assigned to an Okta application, including how each assignment wa | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `appId` | string | Yes | Application ID to list assigned users for | | `q` | string | No | Search assigned users whose userName, firstName, lastName, or email starts with this value | | `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call | @@ -1126,8 +1056,6 @@ Assign a user to an Okta application, granting them access to it. Applications t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `appId` | string | Yes | Application ID to assign the user to | | `userId` | string | Yes | Okta user ID to assign | | `scope` | string | No | Assignment scope: USER for a direct assignment, or GROUP | @@ -1164,8 +1092,6 @@ Unassign a user from an Okta application, revoking their access. Destructive and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `appId` | string | Yes | Application ID to remove the user from | | `userId` | string | Yes | Okta user ID to unassign | | `sendEmail` | boolean | No | Send a deactivation email to the administrator \(default: false\) | @@ -1191,8 +1117,6 @@ List the groups assigned to an Okta application. Every member of an assigned gro | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `appId` | string | Yes | Application ID to list assigned groups for | | `q` | string | No | Search assigned groups whose name starts with this value | | `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call | @@ -1224,8 +1148,6 @@ Assign a group to an Okta application so every member of the group inherits acce | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `appId` | string | Yes | Application ID to assign the group to | | `groupId` | string | Yes | Group ID to assign | | `priority` | number | No | Assignment priority, which resolves conflicting profile mappings when a user belongs to several assigned groups | @@ -1253,8 +1175,6 @@ Unassign a group from an Okta application. Destructive: every member who had acc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `appId` | string | Yes | Application ID to remove the group from | | `groupId` | string | Yes | Group ID to unassign | @@ -1279,8 +1199,6 @@ List the administrator roles assigned to a user. Returns both standard roles and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | Okta user ID \(not a login or email\) to list admin roles for | #### Output @@ -1312,8 +1230,6 @@ Grant a user an administrator role. Use a standard role type such as USER_ADMIN | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | Okta user ID \(not a login or email\) to assign the admin role to | | `roleType` | string | Yes | Role type to assign: SUPER_ADMIN, ORG_ADMIN, APP_ADMIN, USER_ADMIN, HELP_DESK_ADMIN, READ_ONLY_ADMIN, API_ACCESS_MANAGEMENT_ADMIN, GROUP_MEMBERSHIP_ADMIN, REPORT_ADMIN, WORKFLOWS_ADMIN, ACCESS_CERTIFICATIONS_ADMIN, ACCESS_REQUESTS_ADMIN, or CUSTOM | | `customRoleId` | string | No | Custom role ID. Required when the role type is CUSTOM | @@ -1348,8 +1264,6 @@ Revoke an administrator role from a user. Destructive: the user immediately lose | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `userId` | string | Yes | Okta user ID \(not a login or email\) to revoke the admin role from | | `roleAssignmentId` | string | Yes | Role assignment ID to revoke, as returned by List User Roles. For a custom role this is the resource set binding ID | @@ -1374,8 +1288,6 @@ Query the Okta System Log for sign-ins, admin changes, and security events. Supp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Okta API token for authentication | -| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | | `since` | string | No | Start of the query time window as an ISO 8601 timestamp \(default: 7 days before "until"\). Ignored when a cursor is supplied in "after", which already encodes the resume position | | `until` | string | No | End of the query time window as an ISO 8601 timestamp \(default: now\) | | `filter` | string | No | SCIM filter expression \(e.g., eventType eq "user.session.start" or outcome.result eq "FAILURE"\) | diff --git a/apps/docs/content/docs/integrations/otter.mdx b/apps/docs/content/docs/integrations/otter.mdx index 62db5f7e8f0..33641015c63 100644 --- a/apps/docs/content/docs/integrations/otter.mdx +++ b/apps/docs/content/docs/integrations/otter.mdx @@ -2,7 +2,7 @@ title: Otter.ai description: Read Otter.ai meeting conversations, transcripts, and summaries integration: - type: "otter" + type: "otter_v2" color: "#FFFFFF" --- @@ -31,7 +31,6 @@ List Otter conversations for the authenticated user, most recent first, optional | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Otter API key | | `includeShared` | boolean | No | Include conversations shared with the user \(default false\). Automatically true when a channel ID is provided. | | `channelId` | string | No | Only return conversations in this channel. Overrides includeShared. | | `limit` | number | No | Number of conversations per page \(1-100\) | @@ -100,7 +99,6 @@ Get an Otter conversation with its summary and details, plus any requested actio | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Otter API key | | `conversationId` | string | Yes | The Otter conversation ID | | `include` | string | Yes | Comma-separated related data to include: action_items, insights, outline, transcript, or all \(e.g., "insights,transcript"\) | @@ -190,7 +188,6 @@ Get the MP3 audio download link for an Otter conversation. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Otter API key | | `conversationId` | string | Yes | The Otter conversation ID | #### Output @@ -212,7 +209,6 @@ Import an audio or video recording into Otter as a new conversation from a publi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Otter API key | | `fileUrl` | string | Yes | URL of the recording to import \(e.g., https://example.com/call.mp4) | | `name` | string | No | Name for the new conversation | @@ -236,7 +232,6 @@ List the Otter channels available to the authenticated user, in alphabetical ord | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Otter API key | #### Output @@ -267,7 +262,6 @@ List the members of an Otter channel, in alphabetical order. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Otter API key | | `channelId` | string | Yes | The Otter channel ID | #### Output @@ -294,7 +288,6 @@ Get the Otter workspace of the authenticated user, including its ID, name, owner | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Otter API key | #### Output @@ -325,7 +318,6 @@ List every conversation in an Otter workspace, most recent first. Requires an Ot | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Otter API key of a Super Admin | | `workspaceId` | string | Yes | The Otter workspace ID \(the id returned by Get Workspace\) | | `limit` | number | No | Number of conversations per page \(1-100\) | | `cursor` | string | No | Pagination cursor \(nextCursor from a previous response\) | diff --git a/apps/docs/content/docs/integrations/pagerduty.mdx b/apps/docs/content/docs/integrations/pagerduty.mdx index bf354e22aba..a2182e65860 100644 --- a/apps/docs/content/docs/integrations/pagerduty.mdx +++ b/apps/docs/content/docs/integrations/pagerduty.mdx @@ -2,7 +2,7 @@ title: PagerDuty description: Manage incidents and on-call schedules with PagerDuty integration: - type: "pagerduty" + type: "pagerduty_v2" color: "#06AC38" --- @@ -29,7 +29,6 @@ List incidents from PagerDuty with optional filters. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `statuses` | string | No | Comma-separated statuses to filter \(triggered, acknowledged, resolved\) | | `urgencies` | string | No | Comma-separated urgencies to filter \(high, low\) | | `serviceIds` | string | No | Comma-separated service IDs to filter | @@ -73,7 +72,6 @@ Get a single incident from PagerDuty by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `incidentId` | string | Yes | ID of the incident to fetch | #### Output @@ -109,7 +107,6 @@ Create a new incident in PagerDuty. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `fromEmail` | string | Yes | Email address of a valid PagerDuty user | | `title` | string | Yes | Incident title/summary | | `serviceId` | string | Yes | ID of the PagerDuty service | @@ -145,7 +142,6 @@ Update an incident in PagerDuty (acknowledge, resolve, change urgency, etc.). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `fromEmail` | string | Yes | Email address of a valid PagerDuty user | | `incidentId` | string | Yes | ID of the incident to update | | `status` | string | No | New status \(triggered, acknowledged, or resolved\) | @@ -178,7 +174,6 @@ Snooze a triggered PagerDuty incident for a number of seconds, after which it re | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `fromEmail` | string | Yes | Email address of a valid PagerDuty user | | `incidentId` | string | Yes | ID of the incident to snooze | | `duration` | string | Yes | Number of seconds to snooze the incident for \(1 to 604800\) | @@ -204,7 +199,6 @@ Merge one or more source incidents into a target incident. Source incidents are | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `fromEmail` | string | Yes | Email address of a valid PagerDuty user | | `targetIncidentId` | string | Yes | ID of the incident that will absorb the source incidents | | `sourceIncidentIds` | string | Yes | Comma-separated IDs of the incidents to merge into the target incident | @@ -231,7 +225,6 @@ Add a note to an existing PagerDuty incident. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `fromEmail` | string | Yes | Email address of a valid PagerDuty user | | `incidentId` | string | Yes | ID of the incident to add the note to | | `content` | string | Yes | Note content text | @@ -257,7 +250,6 @@ List the individual alerts attached to a PagerDuty incident. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `incidentId` | string | Yes | ID of the incident whose alerts to list | | `statuses` | string | No | Comma-separated statuses to filter \(triggered, resolved\) | | `limit` | string | No | Maximum number of results \(max 100\) | @@ -293,7 +285,6 @@ List services from PagerDuty with optional name filter. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `query` | string | No | Filter services by name | | `limit` | string | No | Maximum number of results \(max 100\) | | `offset` | string | No | Offset to start pagination search results | @@ -327,7 +318,6 @@ Get a single service from PagerDuty by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `serviceId` | string | Yes | ID of the service to fetch | #### Output @@ -358,7 +348,6 @@ List current on-call entries from PagerDuty. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `escalationPolicyIds` | string | No | Comma-separated escalation policy IDs to filter | | `scheduleIds` | string | No | Comma-separated schedule IDs to filter | | `since` | string | No | Start time filter \(ISO 8601 format\) | @@ -396,7 +385,6 @@ List escalation policies from PagerDuty with an optional name filter. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `query` | string | No | Filter escalation policies by name | | `limit` | string | No | Maximum number of results \(max 100\) | | `offset` | string | No | Offset to start pagination search results | @@ -428,7 +416,6 @@ List on-call schedules from PagerDuty with an optional name filter. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `query` | string | No | Filter schedules by name | | `limit` | string | No | Maximum number of results \(max 100\) | | `offset` | string | No | Offset to start pagination search results | @@ -459,7 +446,6 @@ List users from PagerDuty with an optional name/email filter. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | PagerDuty REST API Key | | `query` | string | No | Filter users by name or email | | `limit` | string | No | Maximum number of results \(max 100\) | | `offset` | string | No | Offset to start pagination search results | diff --git a/apps/docs/content/docs/integrations/persona.mdx b/apps/docs/content/docs/integrations/persona.mdx index 1bfb0477ae5..4df5f84bc49 100644 --- a/apps/docs/content/docs/integrations/persona.mdx +++ b/apps/docs/content/docs/integrations/persona.mdx @@ -2,7 +2,7 @@ title: Persona description: Verify identities with Persona integration: - type: "persona" + type: "persona_v2" color: "#FFFFFF" --- @@ -12,7 +12,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} Use [Persona](https://withpersona.com/) to create inquiries from verification templates, generate one-time links, inspect verification results, and update inquiry decisions or lifecycle state. The actions also retrieve summary PDFs and documents, manage accounts and CSV imports, run screening reports, and inspect cases. -Provide your Persona API key and the template or resource identifiers required by the action. Keep a human review step where your process requires one before approving or declining an inquiry. +Save your Persona API key as a Persona credential, select it in the block, and provide the template or resource identifiers the action requires. Keep a human review step where your process requires one before approving or declining an inquiry. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -31,7 +31,6 @@ Create a new identity verification inquiry from an inquiry template. Returns the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryTemplateId` | string | Yes | Inquiry template ID \(starts with itmpl_\), inquiry template version ID \(starts with itmplv_\), or legacy template ID \(starts with tmpl_\) | | `accountId` | string | No | Account ID \(starts with act_\) to associate with this inquiry | | `referenceId` | string | No | Reference ID that refers to an entity in your user model. An account is auto-created for it if one does not exist. | @@ -57,7 +56,6 @@ Retrieve a single identity verification inquiry by ID, including its status, col | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to retrieve \(starts with inq_\) | #### Output @@ -78,7 +76,6 @@ List identity verification inquiries, optionally filtered by status, account ID, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `status` | string | No | Filter by inquiry status \(created, pending, completed, failed, expired, needs_review, approved, declined\) | | `accountId` | string | No | Filter by account ID \(starts with act_\); comma-separate multiple IDs | | `referenceId` | string | No | Filter by reference ID | @@ -106,7 +103,6 @@ Update an inquiry’s note, fields, tags, or redirect URI. Only the provided val | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to update \(starts with inq_\) | | `note` | string | No | Free-form note to set on the inquiry | | `fields` | json | No | JSON object of field name to field value pairs to set, as defined by the inquiry template \(e.g. \{"name-first": "Jane"\}\) | @@ -131,7 +127,6 @@ Approve an identity verification inquiry. Approving prevents further progress on | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to approve \(starts with inq_\) | #### Output @@ -152,7 +147,6 @@ Decline an identity verification inquiry. Declining prevents further progress on | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to decline \(starts with inq_\) | #### Output @@ -173,7 +167,6 @@ Mark an identity verification inquiry for manual review, moving it to the needs_ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to mark for review \(starts with inq_\) | #### Output @@ -194,7 +187,6 @@ Resume a pending or expired inquiry, creating a new session so the individual ca | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to resume \(starts with inq_\) | #### Output @@ -216,7 +208,6 @@ Expire an in-progress inquiry, invalidating its sessions and one-time links so t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to expire \(starts with inq_\) | #### Output @@ -237,7 +228,6 @@ Generate a one-time link for an inquiry that the individual can open to complete | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to generate a one-time link for \(starts with inq_\) | | `expiresInSeconds` | number | No | Number of seconds from now until the link expires \(must be greater than 0; defaults to the inquiry template setting, typically 24 hours\) | @@ -261,7 +251,6 @@ Download a PDF summary of an inquiry, including its collected information and ve | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to print \(starts with inq_\) | #### Output @@ -282,7 +271,6 @@ Permanently delete all personally identifiable information collected by an inqui | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `inquiryId` | string | Yes | Inquiry ID to redact \(starts with inq_\) | #### Output @@ -303,7 +291,6 @@ Create an account that represents an individual in Persona. Accounts consolidate | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `accountTypeId` | string | No | Account type ID to create the account for \(starts with acttp_\); defaults to your organization default | | `referenceId` | string | No | Reference ID that refers to an entity in your user model | | `countryCode` | string | No | ISO 3166-1 alpha-2 country code \(e.g. US\) | @@ -328,7 +315,6 @@ Retrieve a single account by ID, including its reference ID, fields, tags, and s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `accountId` | string | Yes | Account ID to retrieve \(starts with act_\) | #### Output @@ -349,7 +335,6 @@ List accounts in your Persona organization, optionally filtered by reference ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `referenceId` | string | No | Filter by reference ID | | `pageSize` | number | No | Number of accounts to return per page \(1-100, default 10\) | | `pageAfter` | string | No | Pagination cursor: return accounts after this account ID | @@ -373,7 +358,6 @@ Update an account’s reference ID, country code, fields, or tags. Only the prov | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `accountId` | string | Yes | Account ID to update \(starts with act_\) | | `referenceId` | string | No | Reference ID that refers to an entity in your user model | | `countryCode` | string | No | ISO 3166-1 alpha-2 country code \(e.g. US\) | @@ -398,7 +382,6 @@ Bulk-import accounts into Persona from a CSV file. Returns an importer whose sta | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `file` | file | Yes | CSV file of accounts to import | #### Output @@ -419,7 +402,6 @@ Permanently delete all personally identifiable information stored on an account, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `accountId` | string | Yes | Account ID to redact \(starts with act_\) | #### Output @@ -440,7 +422,6 @@ List manual review cases, optionally filtered by status, account ID, or referenc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `status` | string | No | Filter by case status \(e.g. Open, Resolved\) | | `accountId` | string | No | Filter by account ID \(starts with act_\) | | `referenceId` | string | No | Filter by reference ID | @@ -466,7 +447,6 @@ Retrieve a single manual review case by ID, including its status, resolution, an | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `caseId` | string | Yes | Case ID to retrieve \(starts with case_\) | #### Output @@ -487,7 +467,6 @@ Run a screening report (watchlist, adverse media, or politically exposed person) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `reportType` | string | Yes | Type of report to run: watchlist, adverse-media, or politically-exposed-person | | `reportTemplateId` | string | Yes | Report template ID to run \(starts with rptp_\) | | `term` | string | No | Full-name search term \(e.g. "Jane Q Doe"\). Provide this or the separate name parts. | @@ -516,7 +495,6 @@ Retrieve a single screening report by ID, including its status, match results, a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `reportId` | string | Yes | Report ID to retrieve \(starts with rep_\) | #### Output @@ -537,7 +515,6 @@ List screening reports, optionally filtered by account ID or reference ID. Resul | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `accountId` | string | No | Filter by account ID \(starts with act_\) | | `referenceId` | string | No | Filter by reference ID | | `pageSize` | number | No | Number of reports to return per page \(1-100, default 10\) | @@ -562,7 +539,6 @@ Retrieve a single verification by ID (government ID, selfie, document, database, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `verificationId` | string | Yes | Verification ID to retrieve \(starts with ver_\) | #### Output @@ -583,7 +559,6 @@ Retrieve a single document by ID (government ID, generic document, and more), in | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `documentId` | string | Yes | Document ID to retrieve \(starts with doc_\) | #### Output @@ -604,7 +579,6 @@ List the inquiry templates in your Persona organization, to discover template ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Persona API key | | `pageSize` | number | No | Number of templates to return per page \(1-100, default 10\) | | `pageAfter` | string | No | Pagination cursor: return templates after this template ID | diff --git a/apps/docs/content/docs/integrations/plane.mdx b/apps/docs/content/docs/integrations/plane.mdx index 1a5fe006d8c..c88799d7d96 100644 --- a/apps/docs/content/docs/integrations/plane.mdx +++ b/apps/docs/content/docs/integrations/plane.mdx @@ -2,7 +2,7 @@ title: Plane description: Manage projects and work items in Plane integration: - type: "plane" + type: "plane_v2" color: "#FFFFFF" --- @@ -19,7 +19,7 @@ With the Plane block, you can: - **Keep knowledge current**: Manage pages and sync project work items or pages into a Sim knowledge base. - **React to changes**: Start agents from authenticated Plane webhook events. -Create a personal access token in Plane's profile settings and enter it as the API token. API v2 is the default. For Community Edition or an older self-hosted instance, select API v1 in the advanced settings and set the instance origin, optionally including its reverse-proxy path prefix. Omit `/api/v1` and `/api/v2` from the instance URL. The block exposes core workflow actions. Supported self-hosted actions use the same operation list. Resource availability depends on the instance's edition and version. +Create a personal access token in Plane's profile settings and enter it as the API token. API v2 is the default. For Community Edition or an older self-hosted instance, select API v1 in the advanced settings and set the instance origin on the Plane credential (the legacy block's advanced settings), optionally including its reverse-proxy path prefix. Omit `/api/v1` and `/api/v2` from the instance URL. The block exposes core workflow actions. Supported self-hosted actions use the same operation list. Resource availability depends on the instance's edition and version. Explicit empty strings, nulls, and empty arrays can be supplied through **Body Overrides** using the action's canonical input names. Clearing work item labels and assignees is verified through API v1 because Plane v2 can continue returning deleted relationship IDs. The update action returns the verified cleared state; subsequent v2 reads may still include those IDs. Use API v1 when reading active relationships after a clear. @@ -44,8 +44,6 @@ Archive a project in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `pk` | string | Yes | The project id. | @@ -68,8 +66,6 @@ Archive a work item in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the work item belongs to. | @@ -140,8 +136,6 @@ Confirm a work item attachment upload in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -182,8 +176,6 @@ Create a work item attachment upload in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -262,8 +254,6 @@ Create a comment in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -335,8 +325,6 @@ Create a cycle in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -407,8 +395,6 @@ Create a label in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -458,8 +444,6 @@ Create a link in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -505,8 +489,6 @@ Create a module in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -576,8 +558,6 @@ Create a project in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -689,8 +669,6 @@ Create a project page in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -773,8 +751,6 @@ Create a state in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -830,8 +806,6 @@ Create a webhook in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -871,8 +845,6 @@ Create a work item in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -1014,8 +986,6 @@ Delete an attachment in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the resource belongs to. Accepts the project UUID or its bare identifier, for example `ENG`. | @@ -1040,8 +1010,6 @@ Delete a comment in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the work item belongs to. | @@ -1066,8 +1034,6 @@ Delete a cycle in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the cycle belongs to. | @@ -1091,8 +1057,6 @@ Delete a label in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the label belongs to. | @@ -1116,8 +1080,6 @@ Delete a link in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the resource belongs to. Accepts the project UUID or its bare identifier, for example `ENG`. | @@ -1142,8 +1104,6 @@ Delete a module in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the module belongs to. | @@ -1167,8 +1127,6 @@ Delete a project in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `pk` | string | Yes | The project id. | @@ -1191,8 +1149,6 @@ Delete a project page in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the resource belongs to. Accepts the project UUID or its bare identifier, for example `ENG`. | @@ -1216,8 +1172,6 @@ Delete a state in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the state belongs to. | @@ -1241,8 +1195,6 @@ Delete a webhook in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `pk` | string | Yes | The webhook id. | @@ -1265,8 +1217,6 @@ Delete a work item in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the work item belongs to. | @@ -1290,8 +1240,6 @@ Get an attachment in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the resource belongs to. Accepts the project UUID or its bare identifier, for example `ENG`. | @@ -1329,8 +1277,6 @@ Get a comment in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the work item belongs to. | @@ -1395,8 +1341,6 @@ Get current user in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | #### Output @@ -1426,8 +1370,6 @@ Get a cycle in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the cycle belongs to. A cycle id from a different project returns `404`, not the record. | @@ -1488,8 +1430,6 @@ Get a label in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the label belongs to. | @@ -1532,8 +1472,6 @@ Get a link in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the resource belongs to. Accepts the project UUID or its bare identifier, for example `ENG`. | @@ -1597,8 +1535,6 @@ Get a module in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the module belongs to. | @@ -1657,8 +1593,6 @@ Get a project in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `pk` | string | Yes | The project id. | @@ -1740,8 +1674,6 @@ Get a project page in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the resource belongs to. Accepts the project UUID or its bare identifier, for example `ENG`. | @@ -1811,8 +1743,6 @@ Get a project summary in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `pk` | string | Yes | Project ID or bare project identifier whose summary to retrieve. | @@ -1851,8 +1781,6 @@ Get a state in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the state belongs to. A state id from a different project returns `404`, even inside the same workspace. | @@ -1898,8 +1826,6 @@ Get a webhook in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `pk` | string | Yes | The webhook id. | @@ -1932,8 +1858,6 @@ Get a work item in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the work item belongs to. | @@ -2048,8 +1972,6 @@ Get a work item by identifier in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `identifier` | string | Yes | The work item's human key: the project identifier, a hyphen, and the work item's number — `PROJ-142`. The project part must start with a letter, and it is upper-cased before lookup, so `proj-142` resolves the same work item as `PROJ-142`. A project identifier is unique within a workspace, which is what makes this key unambiguous. | @@ -2119,8 +2041,6 @@ List attachments in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the resource belongs to. Accepts the project UUID or its bare identifier, for example `ENG`. | @@ -2208,8 +2128,6 @@ List comments in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the work item belongs to. | @@ -2312,8 +2230,6 @@ List cycles in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project whose cycles you want. | @@ -2429,8 +2345,6 @@ List labels in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project whose labels you want. | @@ -2547,8 +2461,6 @@ List links in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the resource belongs to. Accepts the project UUID or its bare identifier, for example `ENG`. | @@ -2634,8 +2546,6 @@ List modules in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project whose modules you want. | @@ -2745,8 +2655,6 @@ List project pages in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the resource belongs to. Accepts the project UUID or its bare identifier, for example `ENG`. | @@ -2878,8 +2786,6 @@ List projects in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `count` | boolean | No | Set to false to skip the total_count COUNT\(\*\) \(omits total_count\). | @@ -3033,8 +2939,6 @@ List states in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project whose states you want to list. | @@ -3128,8 +3032,6 @@ List webhooks in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `count` | boolean | No | Set to false to skip the total_count COUNT\(\*\) \(omits total_count\). | @@ -3179,8 +3081,6 @@ List work items in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project to list work items from. | @@ -3388,8 +3288,6 @@ List work items across a workspace in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `assignee_id` | string | No | Filter by `assignee_id`. | @@ -3510,8 +3408,6 @@ Add or remove cycle work items in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -3555,8 +3451,6 @@ Add or remove module work items in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -3659,8 +3553,6 @@ Regenerate a webhook secret in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `pk` | string | Yes | The webhook id. | @@ -3694,8 +3586,6 @@ Transfer work items between cycles in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -3723,8 +3613,6 @@ Unarchive a project in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `pk` | string | Yes | The project id. | @@ -3747,8 +3635,6 @@ Unarchive a work item in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | | `project_id` | string | Yes | The project the work item belongs to. | @@ -3819,8 +3705,6 @@ Update a comment in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -3891,8 +3775,6 @@ Update a cycle in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -3964,8 +3846,6 @@ Update a label in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -4016,8 +3896,6 @@ Update a link in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -4067,8 +3945,6 @@ Update a module in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -4139,8 +4015,6 @@ Update a project in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -4253,8 +4127,6 @@ Update a project page in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -4338,8 +4210,6 @@ Update a state in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -4396,8 +4266,6 @@ Update a webhook in Plane. Requires API v2. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | @@ -4437,8 +4305,6 @@ Update a work item in Plane. Supports API v1 compatibility. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Plane personal access token from Profile → API tokens. | -| `baseUrl` | string | No | Plane instance origin, optionally with a reverse-proxy path prefix. Defaults to https://api.plane.so. Do not include /api/v1 or /api/v2. | | `apiVersion` | string | No | API version: v2 \(default\) for Plane Cloud and current commercial instances; v1 for Community Edition or older self-hosted instances. Unsupported operations or fields fail explicitly. | | `bodyOverrides` | json | No | JSON overrides using the canonical input names for the selected API version. Preserves empty strings, null, and empty arrays; unsupported fields are rejected. | | `workspace_slug` | string | Yes | Workspace slug from the Plane URL \(for example, my-team\). | diff --git a/apps/docs/content/docs/integrations/profound.mdx b/apps/docs/content/docs/integrations/profound.mdx index 41d6157701b..cede1cc099c 100644 --- a/apps/docs/content/docs/integrations/profound.mdx +++ b/apps/docs/content/docs/integrations/profound.mdx @@ -2,7 +2,7 @@ title: Profound description: AI visibility and analytics with Profound integration: - type: "profound" + type: "profound_v2" color: "#000000" --- @@ -31,7 +31,6 @@ List all organization categories in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | #### Output @@ -53,7 +52,6 @@ List all organization regions in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | #### Output @@ -75,7 +73,6 @@ List all AI models/platforms tracked in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | #### Output @@ -97,7 +94,6 @@ List all organization domains in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | #### Output @@ -120,7 +116,6 @@ List all organization assets (companies/brands) across all categories in Profoun | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | #### Output @@ -149,7 +144,6 @@ List all organization personas across all categories in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | #### Output @@ -174,7 +168,6 @@ List topics for a specific category in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | #### Output @@ -197,7 +190,6 @@ List tags for a specific category in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | #### Output @@ -220,7 +212,6 @@ List prompts for a specific category in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | | `limit` | number | No | Maximum number of results \(default 10000, max 10000\) | | `cursor` | string | No | Pagination cursor from previous response | @@ -260,7 +251,6 @@ List assets (companies/brands) for a specific category in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | #### Output @@ -288,7 +278,6 @@ List personas for a specific category in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | #### Output @@ -312,7 +301,6 @@ Query AI visibility report for a category in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) | @@ -343,7 +331,6 @@ Query sentiment report for a category in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) | @@ -374,7 +361,6 @@ Query citations report for a category in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) | @@ -405,7 +391,6 @@ Query fanout report showing how AI models expand prompts into sub-queries in Pro | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) | @@ -436,7 +421,6 @@ Get raw prompt answers data for a category in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `categoryId` | string | Yes | Category ID \(UUID\) | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) | @@ -472,7 +456,6 @@ Query bot traffic report with hourly granularity for a domain in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `domain` | string | Yes | Domain to query bot traffic for \(e.g. example.com\) | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now | @@ -503,7 +486,6 @@ Query human referral traffic report with hourly granularity for a domain in Prof | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `domain` | string | Yes | Domain to query referral traffic for \(e.g. example.com\) | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now | @@ -534,7 +516,6 @@ Get raw traffic logs with filters for a domain in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `domain` | string | Yes | Domain to query logs for \(e.g. example.com\) | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now | @@ -563,7 +544,6 @@ Get identified bot visit logs with filters for a domain in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `domain` | string | Yes | Domain to query bot logs for \(e.g. example.com\) | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now | @@ -592,7 +572,6 @@ List content optimization entries for an asset in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `assetId` | string | Yes | Asset ID \(UUID\) | | `limit` | number | No | Maximum number of results \(default 10000, max 50000\) | | `offset` | number | No | Offset for pagination \(default 0\) | @@ -622,7 +601,6 @@ Get detailed content optimization analysis for a specific content item in Profou | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `assetId` | string | Yes | Asset ID \(UUID\) | | `contentId` | string | Yes | Content/optimization ID \(UUID\) | @@ -663,7 +641,6 @@ Query prompt volume data to understand search demand across AI platforms in Prof | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) | | `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) | | `metrics` | string | Yes | Comma-separated metrics: volume, change | @@ -693,7 +670,6 @@ Get prompts that cite a specific domain across AI platforms in Profound | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Profound API Key | | `inputDomain` | string | Yes | Domain to look up citations for \(e.g. ramp.com\) | #### Output diff --git a/apps/docs/content/docs/integrations/pulse.mdx b/apps/docs/content/docs/integrations/pulse.mdx index b91ebfc1e12..4d271b4d764 100644 --- a/apps/docs/content/docs/integrations/pulse.mdx +++ b/apps/docs/content/docs/integrations/pulse.mdx @@ -2,7 +2,7 @@ title: Pulse description: Extract text from documents using Pulse OCR integration: - type: "pulse_v2" + type: "pulse_v3" color: "#FFFFFF" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Pulse](https://www.runpulse.com) to extract text and structured content from PDFs, images, and Office files with optical character recognition (OCR). Supply a file URL or upload, select pages when needed, and choose the supported Markdown, text, or JSON output options. +Use [Pulse](https://www.runpulse.com) to extract text and structured content from PDFs, images, and Office files with optical character recognition (OCR). Supply an uploaded file or a file reference, select pages when needed, and choose the supported Markdown, text, or JSON output options. Optional figure and table extraction returns captions and descriptions. The result also includes processing metadata such as file type, page count, and processing time. Pass the extracted content to later steps for review or summarization. {/* MANUAL-CONTENT-END */} @@ -35,7 +35,6 @@ Parse documents (PDF, images, Office docs) using Pulse OCR API | `pages` | string | No | Page range to process \(1-indexed, e.g., "1-2,5"\) | | `chunking` | string | No | Chunking strategies \(comma-separated: semantic, header, page, recursive\) | | `chunkSize` | number | No | Maximum characters per chunk when chunking is enabled | -| `apiKey` | string | Yes | Pulse API key | #### Output diff --git a/apps/docs/content/docs/integrations/quartr.mdx b/apps/docs/content/docs/integrations/quartr.mdx index cb8dea0bd46..c554df9d8b3 100644 --- a/apps/docs/content/docs/integrations/quartr.mdx +++ b/apps/docs/content/docs/integrations/quartr.mdx @@ -2,7 +2,7 @@ title: Quartr description: Access earnings calls, transcripts, filings, and slides integration: - type: "quartr" + type: "quartr_v2" color: "#000000" --- @@ -14,12 +14,12 @@ Use [Quartr](https://quartr.com/) to look up companies, corporate events, and ev Downloaded reports, slide decks, and transcripts are stored as execution files, so they can be passed straight into agents, knowledge bases, or other blocks in your workflow. -To use the integration, generate an API key from the Quartr API portal and paste it into the block. Your key inherits the datasets enabled on your Quartr subscription. +To use the integration, generate an API key from the Quartr API portal and add it as a Quartr credential. Your key inherits the datasets enabled on your Quartr subscription. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Look up companies, corporate events, and event types, retrieve AI-generated event summaries, list and download reports, slide decks, and transcripts, and access archived audio or live event streams. Supply a Quartr API key. +Look up companies, corporate events, and event types, retrieve AI-generated event summaries, list and download reports, slide decks, and transcripts, and access archived audio or live event streams. ## Actions @@ -33,7 +33,6 @@ List companies covered by Quartr, filterable by ticker, ISIN, CIK, OpenFIGI, cou | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `tickers` | string | No | Comma-separated list of company tickers \(e.g., "AAPL,MSFT"\) | | `isins` | string | No | Comma-separated list of ISINs \(e.g., "US0378331005"\) | | `ciks` | string | No | Comma-separated list of SEC CIKs \(e.g., "0000320193"\) | @@ -79,7 +78,6 @@ Retrieve a single company from Quartr by its company ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `companyId` | number | Yes | Quartr company ID \(e.g., 4742\) | #### Output @@ -113,7 +111,6 @@ List corporate events (earnings calls, capital markets days, etc.) from Quartr, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `companyIds` | string | No | Comma-separated list of Quartr company IDs \(e.g., "4742,128"\) | | `tickers` | string | No | Comma-separated list of company tickers \(e.g., "AAPL,MSFT"\) | | `isins` | string | No | Comma-separated list of ISINs \(e.g., "US0378331005"\) | @@ -160,7 +157,6 @@ Retrieve a single corporate event from Quartr by its event ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `eventId` | number | Yes | Quartr event ID \(e.g., 128301\) | #### Output @@ -192,7 +188,6 @@ Retrieve the AI-generated summary of a corporate event from Quartr, with selecta | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `eventId` | number | Yes | Quartr event ID \(e.g., 128301\) | | `summaryLength` | string | No | Length preset for the summary: "line", "short", or "long" \(default: short\) | | `plainSummary` | boolean | No | Return a plain-text summary without embedded document source tags | @@ -224,7 +219,6 @@ List the event types available in Quartr (e.g., earnings calls), useful for filt | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `limit` | number | No | Maximum number of items to return in a single request \(default: 10, max: 500\) | | `cursor` | number | No | Pagination cursor from the previous response \(nextCursor\) for the next page | | `direction` | string | No | Sort direction by id: "asc" or "desc" \(default: asc\) | @@ -253,7 +247,6 @@ List documents of all kinds (reports, slide decks, and transcripts) from Quartr, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `companyIds` | string | No | Comma-separated list of Quartr company IDs \(e.g., "4742,128"\) | | `eventIds` | string | No | Comma-separated list of Quartr event IDs \(e.g., "128301"\) | | `tickers` | string | No | Comma-separated list of company tickers \(e.g., "AAPL,MSFT"\) | @@ -305,7 +298,6 @@ List the document types available in Quartr (e.g., 10-Q quarterly reports), usef | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `limit` | number | No | Maximum number of items to return in a single request \(default: 10, max: 500\) | | `cursor` | number | No | Pagination cursor from the previous response \(nextCursor\) for the next page | | `direction` | string | No | Sort direction by id: "asc" or "desc" \(default: asc\) | @@ -337,7 +329,6 @@ List filings and reports (10-K, 10-Q, earnings releases, etc.) from Quartr, filt | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `companyIds` | string | No | Comma-separated list of Quartr company IDs \(e.g., "4742,128"\) | | `eventIds` | string | No | Comma-separated list of Quartr event IDs \(e.g., "128301"\) | | `tickers` | string | No | Comma-separated list of company tickers \(e.g., "AAPL,MSFT"\) | @@ -389,7 +380,6 @@ Retrieve a filing or report (10-K, 10-Q, earnings release, etc.) from Quartr by | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `reportId` | number | Yes | Quartr document ID of the report \(e.g., 432907\) | #### Output @@ -426,7 +416,6 @@ List slide presentations from Quartr, filterable by company, event, document typ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `companyIds` | string | No | Comma-separated list of Quartr company IDs \(e.g., "4742,128"\) | | `eventIds` | string | No | Comma-separated list of Quartr event IDs \(e.g., "128301"\) | | `tickers` | string | No | Comma-separated list of company tickers \(e.g., "AAPL,MSFT"\) | @@ -478,7 +467,6 @@ Retrieve a slide presentation from Quartr by its document ID and download the PD | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `slideDeckId` | number | Yes | Quartr document ID of the slide deck \(e.g., 432907\) | #### Output @@ -515,7 +503,6 @@ List event transcripts from Quartr, filterable by company, event, document type, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `companyIds` | string | No | Comma-separated list of Quartr company IDs \(e.g., "4742,128"\) | | `eventIds` | string | No | Comma-separated list of Quartr event IDs \(e.g., "128301"\) | | `tickers` | string | No | Comma-separated list of company tickers \(e.g., "AAPL,MSFT"\) | @@ -567,7 +554,6 @@ Retrieve an event transcript from Quartr by its document ID and download the tra | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `transcriptId` | number | Yes | Quartr document ID of the transcript \(e.g., 432907\) | #### Output @@ -604,7 +590,6 @@ List archived event audio recordings from Quartr, filterable by company, event, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `companyIds` | string | No | Comma-separated list of Quartr company IDs \(e.g., "4742,128"\) | | `eventIds` | string | No | Comma-separated list of Quartr event IDs \(e.g., "128301"\) | | `tickers` | string | No | Comma-separated list of company tickers \(e.g., "AAPL,MSFT"\) | @@ -660,7 +645,6 @@ Retrieve an archived event audio recording from Quartr by its audio ID. Returns | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `audioId` | number | Yes | Quartr audio ID \(e.g., 123964\) | #### Output @@ -701,7 +685,6 @@ List live and upcoming events from Quartr with live audio and transcript stream | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Quartr API key | | `companyIds` | string | No | Comma-separated list of Quartr company IDs \(e.g., "4742,128"\) | | `eventIds` | string | No | Comma-separated list of Quartr event IDs \(e.g., "128301"\) | | `tickers` | string | No | Comma-separated list of company tickers \(e.g., "AAPL,MSFT"\) | diff --git a/apps/docs/content/docs/integrations/quiver.mdx b/apps/docs/content/docs/integrations/quiver.mdx index 337da479f53..4146c050ee3 100644 --- a/apps/docs/content/docs/integrations/quiver.mdx +++ b/apps/docs/content/docs/integrations/quiver.mdx @@ -2,7 +2,7 @@ title: Quiver description: Generate and vectorize SVGs integration: - type: "quiver_v2" + type: "quiver_v3" color: "#FFFFFF" --- @@ -31,7 +31,6 @@ Generate SVG images from text prompts using QuiverAI | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | QuiverAI API key | | `prompt` | string | Yes | A text description of the desired SVG | | `model` | string | Yes | The model to use for SVG generation \(e.g., "arrow-preview"\) | | `instructions` | string | No | Style or formatting guidance for the SVG output | @@ -65,7 +64,6 @@ Convert raster images into vector SVG format using QuiverAI | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | QuiverAI API key | | `model` | string | Yes | The model to use for vectorization \(e.g., "arrow-preview"\) | | `image` | file | Yes | The raster image to vectorize into SVG | | `temperature` | number | No | Sampling temperature \(0-2, default 1\) | @@ -98,7 +96,6 @@ List all available QuiverAI models | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | QuiverAI API key | #### Output diff --git a/apps/docs/content/docs/integrations/rb2b.mdx b/apps/docs/content/docs/integrations/rb2b.mdx index 6f7a67efb22..409a68a4f54 100644 --- a/apps/docs/content/docs/integrations/rb2b.mdx +++ b/apps/docs/content/docs/integrations/rb2b.mdx @@ -2,7 +2,7 @@ title: RB2B description: Identify and enrich website visitors integration: - type: "rb2b" + type: "rb2b_v2" color: "#51FF00" --- @@ -17,7 +17,7 @@ Connect an RB2B API key and inspect the returned match before adding a contact t ## Use the block [#usage-instructions] -Resolve IP addresses, hashed emails, and LinkedIn profiles into person-level identity and B2B enrichment data using the RB2B API. Convert IPs to hashed emails, MAIDs, and company domains; enrich emails into LinkedIn profiles, business profiles, and mobile IDs; and look up emails or phone numbers from LinkedIn. Requires an RB2B API key. +Resolve IP addresses, hashed emails, and LinkedIn profiles into person-level identity and B2B enrichment data using the RB2B API. Convert IPs to hashed emails, MAIDs, and company domains; enrich emails into LinkedIn profiles, business profiles, and mobile IDs; and look up emails or phone numbers from LinkedIn. ## Actions @@ -31,7 +31,6 @@ Check the number of API credits remaining on your RB2B account. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | #### Output @@ -51,7 +50,6 @@ Convert an IP address (and optional user agent) into hashed email addresses (HEM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `ip_address` | string | Yes | The IP address to resolve \(IPv4 or IPv6\) | | `user_agent` | string | No | Optional user agent string to improve match accuracy | | `include_sha256` | boolean | No | Whether to include SHA-256 hashes in addition to MD5 hashes | @@ -77,7 +75,6 @@ Resolve an IP address (and optional user agent) into mobile advertising identifi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `ip_address` | string | Yes | The IP address to resolve \(IPv4 or IPv6\) | | `user_agent` | string | No | Optional user agent string to improve match accuracy | @@ -101,7 +98,6 @@ Identify the company domains associated with an IP address, ranked by confidence | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `ip_address` | string | Yes | The IP address to resolve \(IPv4 or IPv6\) | #### Output @@ -124,7 +120,6 @@ Return a full business profile (name, title, company, industry, seniority and mo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `email` | string | Yes | A plaintext email address or an MD5 hash of the email | #### Output @@ -162,7 +157,6 @@ Return the most recently active LinkedIn profile URL for an email address or MD5 | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `email` | string | Yes | A plaintext email address or an MD5 hash of the email | #### Output @@ -183,7 +177,6 @@ Return the LinkedIn slug (the profile identifier portion of the URL) for an emai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `email` | string | Yes | A plaintext email address or an MD5 hash of the email | #### Output @@ -204,7 +197,6 @@ Return up to five mobile advertising identifiers (MAIDs) associated with an emai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `email` | string | Yes | A plaintext email address or an MD5 hash of the email | #### Output @@ -227,7 +219,6 @@ Return the last known active date for an email address. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `email` | string | Yes | The email address to look up | #### Output @@ -253,7 +244,6 @@ Return a full business profile (name, title, company, emails and more) for a Lin | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output @@ -290,7 +280,6 @@ Return the personal email with the most recent known network activity for a Link | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output @@ -311,7 +300,6 @@ Return the personal email addresses associated with a LinkedIn profile. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output @@ -332,7 +320,6 @@ Return the business and personal hashed emails (MD5 and SHA-256) associated with | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output @@ -357,7 +344,6 @@ Return the mobile phone number with the most recent known network activity for a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output @@ -378,7 +364,6 @@ Find a LinkedIn profile URL from a first name, last name, and company domain. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | RB2B API key | | `first_name` | string | Yes | The person’s first name | | `last_name` | string | Yes | The person’s last name | | `company_domain` | string | Yes | The company domain \(e.g. example.com\) | diff --git a/apps/docs/content/docs/integrations/rds.mdx b/apps/docs/content/docs/integrations/rds.mdx index e1c7aa78bcd..493311bd8ad 100644 --- a/apps/docs/content/docs/integrations/rds.mdx +++ b/apps/docs/content/docs/integrations/rds.mdx @@ -2,7 +2,7 @@ title: Amazon RDS description: Connect to Amazon RDS via Data API integration: - type: "rds" + type: "rds_v2" color: "linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Amazon RDS Aurora Serverless](https://aws.amazon.com/rds/aurora/serverless/) through the Data API to query or modify rows, execute SQL, and inspect the database schema from a workflow. +Use [Amazon RDS Aurora Serverless](https://aws.amazon.com/rds/aurora/serverless/) through the Data API to query or modify rows, execute SQL, and inspect the database schema from a workflow. Connect with a saved [AWS credential](/integrations/aws-credentials). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -30,8 +30,6 @@ Execute a SELECT query on Amazon RDS using the Data API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `resourceArn` | string | Yes | ARN of the Aurora DB cluster \(e.g., arn:aws:rds:us-east-1:123456789012:cluster:my-cluster\) | | `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials | | `database` | string | No | Database name to connect to \(e.g., mydb, production_db\) | @@ -58,8 +56,6 @@ Insert data into an Amazon RDS table using the Data API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `resourceArn` | string | Yes | ARN of the Aurora DB cluster \(e.g., arn:aws:rds:us-east-1:123456789012:cluster:my-cluster\) | | `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials | | `database` | string | No | Database name to connect to \(e.g., mydb, production_db\) | @@ -87,8 +83,6 @@ Update data in an Amazon RDS table using the Data API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `resourceArn` | string | Yes | ARN of the Aurora DB cluster \(e.g., arn:aws:rds:us-east-1:123456789012:cluster:my-cluster\) | | `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials | | `database` | string | No | Database name to connect to \(e.g., mydb, production_db\) | @@ -117,8 +111,6 @@ Delete data from an Amazon RDS table using the Data API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `resourceArn` | string | Yes | ARN of the Aurora DB cluster \(e.g., arn:aws:rds:us-east-1:123456789012:cluster:my-cluster\) | | `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials | | `database` | string | No | Database name to connect to \(e.g., mydb, production_db\) | @@ -146,8 +138,6 @@ Execute raw SQL on Amazon RDS using the Data API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `resourceArn` | string | Yes | ARN of the Aurora DB cluster \(e.g., arn:aws:rds:us-east-1:123456789012:cluster:my-cluster\) | | `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials | | `database` | string | No | Database name to connect to \(e.g., mydb, production_db\) | @@ -174,8 +164,6 @@ Introspect Amazon RDS Aurora database schema to retrieve table structures, colum | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `resourceArn` | string | Yes | ARN of the Aurora DB cluster \(e.g., arn:aws:rds:us-east-1:123456789012:cluster:my-cluster\) | | `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials | | `database` | string | No | Database name to connect to \(e.g., mydb, production_db\) | diff --git a/apps/docs/content/docs/integrations/reducto.mdx b/apps/docs/content/docs/integrations/reducto.mdx index f22dbf6bc94..042674fabf7 100644 --- a/apps/docs/content/docs/integrations/reducto.mdx +++ b/apps/docs/content/docs/integrations/reducto.mdx @@ -2,7 +2,7 @@ title: Reducto description: Extract text from PDF documents integration: - type: "reducto_v2" + type: "reducto_v3" color: "#5c0c5c" --- @@ -32,7 +32,6 @@ Parse PDF documents using Reducto OCR API | `file` | file | Yes | PDF document to be processed | | `pages` | array | No | Specific pages to process \(1-indexed page numbers\) | | `tableOutputFormat` | string | No | Table output format \(`md` for Markdown or `html` for HTML\). Defaults to `md`. | -| `apiKey` | string | Yes | Reducto API key \(REDUCTO_API_KEY\) | #### Output diff --git a/apps/docs/content/docs/integrations/resend.mdx b/apps/docs/content/docs/integrations/resend.mdx index b3c1d2aad9d..cefddb4d4b3 100644 --- a/apps/docs/content/docs/integrations/resend.mdx +++ b/apps/docs/content/docs/integrations/resend.mdx @@ -2,7 +2,7 @@ title: Resend description: Send emails and manage contacts with Resend. integration: - type: "resend" + type: "resend_v2" color: "#181C1E" --- @@ -17,7 +17,7 @@ Use returned email IDs to check delivery status. The block's actions define the ## Use the block [#usage-instructions] -Send emails, retrieve email status, manage contacts, and view domains. Requires API Key. +Send emails, retrieve email status, manage contacts, and view domains. ## Actions @@ -41,7 +41,6 @@ Send an email using your own Resend API key and from address | `replyTo` | string | No | Reply-to email address | | `scheduledAt` | string | No | Schedule email to be sent later in ISO 8601 format | | `tags` | string | No | Comma-separated key:value pairs for email tags \(e.g., "category:welcome,type:onboarding"\) | -| `resendApiKey` | string | Yes | Resend API key for sending emails | #### Output @@ -66,7 +65,6 @@ Retrieve details of a previously sent email by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `emailId` | string | Yes | The ID of the email to retrieve | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -101,7 +99,6 @@ Cancel a scheduled email before it is sent | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `cancelEmailId` | string | Yes | The ID of the scheduled email to cancel | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -125,7 +122,6 @@ Create a new contact in Resend | `firstName` | string | No | First name of the contact | | `lastName` | string | No | Last name of the contact | | `unsubscribed` | boolean | No | Whether the contact is unsubscribed from all broadcasts | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -145,7 +141,6 @@ List all contacts in Resend | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -173,7 +168,6 @@ Retrieve details of a contact by ID or email | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `contactId` | string | Yes | The contact ID or email address to retrieve | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -202,7 +196,6 @@ Update an existing contact in Resend | `firstName` | string | No | Updated first name | | `lastName` | string | No | Updated last name | | `unsubscribed` | boolean | No | Whether the contact should be unsubscribed from all broadcasts | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -223,7 +216,6 @@ Delete a contact from Resend by ID or email | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `contactId` | string | Yes | The contact ID or email address to delete | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -245,7 +237,6 @@ Create a new audience in Resend | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `audienceName` | string | Yes | The name of the audience to create | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -267,7 +258,6 @@ Retrieve details of an audience by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `audienceId` | string | Yes | The ID of the audience to retrieve | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -289,7 +279,6 @@ List all audiences in Resend | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -314,7 +303,6 @@ Delete an audience from Resend by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `audienceId` | string | Yes | The ID of the audience to delete | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -343,7 +331,6 @@ Create a broadcast email for an audience in Resend | `broadcastReplyTo` | string | No | Reply-to email address | | `broadcastName` | string | No | Friendly internal name for the broadcast | | `broadcastPreviewText` | string | No | Preview text shown in the inbox before the email is opened | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -365,7 +352,6 @@ Send a broadcast immediately or schedule it for later | --------- | ---- | -------- | ----------- | | `broadcastId` | string | Yes | The ID of the broadcast to send | | `broadcastScheduledAt` | string | No | Schedule delivery in natural language \(e.g., "in 1 min"\) or ISO 8601 format. Sends immediately if omitted | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -386,7 +372,6 @@ Retrieve details of a broadcast by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `broadcastId` | string | Yes | The ID of the broadcast to retrieve | -| `resendApiKey` | string | Yes | Resend API key | #### Output @@ -417,7 +402,6 @@ List all verified domains in your Resend account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `resendApiKey` | string | Yes | Resend API key | #### Output diff --git a/apps/docs/content/docs/integrations/rippling.mdx b/apps/docs/content/docs/integrations/rippling.mdx index 7e3fcd8c845..39e4bc686b4 100644 --- a/apps/docs/content/docs/integrations/rippling.mdx +++ b/apps/docs/content/docs/integrations/rippling.mdx @@ -2,7 +2,7 @@ title: Rippling description: Manage workers, departments, custom objects, and company data in Rippling integration: - type: "rippling" + type: "rippling_v2" color: "#502D3C" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Rippling](https://www.rippling.com/) to read workforce data, manage organizational resources and custom objects, and configure custom apps and pages. Connect with an API key. Reporting actions start a report run and retrieve its result; Create Draft Hires submits employees to the onboarding flow. +Use [Rippling](https://www.rippling.com/) to read workforce data, manage organizational resources and custom objects, and configure custom apps and pages. Connect with a saved Rippling API token. Reporting actions start a report run and retrieve its result; Create Draft Hires submits employees to the onboarding flow. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -29,7 +29,6 @@ List all workers with optional filtering and pagination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `filter` | string | No | Filter expression | | `expand` | string | No | Comma-separated fields to expand | | `orderBy` | string | No | Sort field. Prefix with - for descending | @@ -88,7 +87,6 @@ Get a specific worker by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | | `expand` | string | No | Comma-separated fields to expand | @@ -142,7 +140,6 @@ List all users with optional pagination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -182,7 +179,6 @@ Get a specific user by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -218,7 +214,6 @@ List all companies | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `expand` | string | No | Comma-separated fields to expand | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -255,7 +250,6 @@ Get SSO information for the current user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `expand` | string | No | Comma-separated fields to expand | #### Output @@ -281,7 +275,6 @@ List all entitlements | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | #### Output @@ -307,7 +300,6 @@ List all departments | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `expand` | string | No | Comma-separated fields to expand | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -342,7 +334,6 @@ Get a specific department by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | | `expand` | string | No | Comma-separated fields to expand | @@ -373,7 +364,6 @@ Create a new department | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `name` | string | Yes | Department name | | `parentId` | string | No | Parent department ID | | `referenceCode` | string | No | Reference code | @@ -404,7 +394,6 @@ Update an existing department | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Department ID | | `name` | string | No | Department name | | `parentId` | string | No | Parent department ID | @@ -436,7 +425,6 @@ List all teams | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `expand` | string | No | Comma-separated fields to expand | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -468,7 +456,6 @@ Get a specific team by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | | `expand` | string | No | Comma-separated fields to expand | @@ -496,7 +483,6 @@ List all employment types | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -529,7 +515,6 @@ Get a specific employment type by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -558,7 +543,6 @@ List all titles | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -587,7 +571,6 @@ Get a specific title by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -612,7 +595,6 @@ Create a new title | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `name` | string | Yes | Title name | #### Output @@ -636,7 +618,6 @@ Update an existing title | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Title ID | | `name` | string | No | Title name | @@ -661,7 +642,6 @@ Delete a title | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | ID of the resource to delete | #### Output @@ -682,7 +662,6 @@ List all custom fields | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -714,7 +693,6 @@ List all job functions | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -743,7 +721,6 @@ Get a specific job function by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -768,7 +745,6 @@ List all work locations | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -798,7 +774,6 @@ Get a specific work location by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -824,7 +799,6 @@ Create a new work location | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `name` | string | Yes | Location name | | `streetAddress` | string | Yes | Street address | | `locality` | string | No | City | @@ -855,7 +829,6 @@ Update a work location | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Location ID | | `name` | string | No | Location name | | `streetAddress` | string | No | Street address | @@ -887,7 +860,6 @@ Delete a work location | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | ID of the resource to delete | #### Output @@ -908,7 +880,6 @@ List all business partners | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `filter` | string | No | Filter expression | | `expand` | string | No | Comma-separated fields to expand | | `orderBy` | string | No | Sort field. Prefix with - for descending | @@ -942,7 +913,6 @@ Get a specific business partner by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | | `expand` | string | No | Comma-separated fields to expand | @@ -971,7 +941,6 @@ Create a new business partner | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `businessPartnerGroupId` | string | Yes | Business partner group ID | | `workerId` | string | Yes | Worker ID | @@ -999,7 +968,6 @@ Delete a business partner | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | ID of the resource to delete | #### Output @@ -1020,7 +988,6 @@ List all business partner groups | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `expand` | string | No | Comma-separated fields to expand | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -1052,7 +1019,6 @@ Get a specific business partner group by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | | `expand` | string | No | Comma-separated fields to expand | @@ -1080,7 +1046,6 @@ Create a new business partner group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `name` | string | Yes | Group name | | `domain` | string | No | Domain \(HR, IT, FINANCE, RECRUITING, OTHER\) | | `defaultBusinessPartnerId` | string | No | Default business partner ID | @@ -1108,7 +1073,6 @@ Delete a business partner group | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | ID of the resource to delete | #### Output @@ -1129,7 +1093,6 @@ List all supergroups | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `filter` | string | No | Filter expression \(filterable fields: app_owner_id, group_type\) | | `orderBy` | string | No | Sort field. Prefix with - for descending | @@ -1173,7 +1136,6 @@ Get a specific supergroup by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -1213,7 +1175,6 @@ List members of a supergroup | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `groupId` | string | Yes | Supergroup ID | | `expand` | string | No | Fields to expand \(e.g., worker\) | | `orderBy` | string | No | Sort field | @@ -1246,7 +1207,6 @@ List inclusion members of a supergroup | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `groupId` | string | Yes | Supergroup ID | | `expand` | string | No | Fields to expand \(e.g., worker\) | | `orderBy` | string | No | Sort field | @@ -1279,7 +1239,6 @@ List exclusion members of a supergroup | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `groupId` | string | Yes | Supergroup ID | | `expand` | string | No | Fields to expand \(e.g., worker\) | | `orderBy` | string | No | Sort field | @@ -1312,7 +1271,6 @@ Update inclusion members of a supergroup | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `groupId` | string | Yes | Supergroup ID | | `operations` | json | Yes | Operations array \[\{op: "add"\|"remove", value: \[\{id: "member_id"\}\]\}\] | @@ -1334,7 +1292,6 @@ Update exclusion members of a supergroup | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `groupId` | string | Yes | Supergroup ID | | `operations` | json | Yes | Operations array \[\{op: "add"\|"remove", value: \[\{id: "member_id"\}\]\}\] | @@ -1356,7 +1313,6 @@ List all custom objects | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | #### Output @@ -1390,7 +1346,6 @@ Get a custom object by API name | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | custom object api name | #### Output @@ -1422,7 +1377,6 @@ Create a new custom object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `name` | string | Yes | Object name | | `description` | string | No | Description | | `category` | string | No | Category | @@ -1456,7 +1410,6 @@ Update a custom object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `name` | string | No | Name | | `description` | string | No | Description | @@ -1493,7 +1446,6 @@ Delete a custom object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | ID of the resource to delete | #### Output @@ -1514,7 +1466,6 @@ List all fields for a custom object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | #### Output @@ -1550,7 +1501,6 @@ Get a specific field of a custom object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `fieldApiName` | string | Yes | Field API name | @@ -1584,7 +1534,6 @@ Create a field on a custom object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `name` | string | Yes | Field name | | `description` | string | No | Description | @@ -1628,7 +1577,6 @@ Update a field on a custom object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `fieldApiName` | string | Yes | Field API name | | `name` | string | No | Field name | @@ -1673,7 +1621,6 @@ Delete a field from a custom object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `fieldApiName` | string | Yes | Field API name | @@ -1695,7 +1642,6 @@ List all records for a custom object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | #### Output @@ -1728,7 +1674,6 @@ Get a specific custom object record | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `codrId` | string | Yes | Record ID | @@ -1759,7 +1704,6 @@ Get a custom object record by external ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `externalId` | string | Yes | External ID | @@ -1790,7 +1734,6 @@ Query custom object records with filters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `query` | string | No | Query expression | | `limit` | number | No | Maximum number of records to return | @@ -1826,7 +1769,6 @@ Create a custom object record | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `externalId` | string | No | External ID for the record | | `data` | json | Yes | Record data | @@ -1858,7 +1800,6 @@ Update a custom object record | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `codrId` | string | Yes | Record ID | | `externalId` | string | No | External ID for the record | @@ -1891,7 +1832,6 @@ Delete a custom object record | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `codrId` | string | Yes | Record ID | @@ -1913,7 +1853,6 @@ Bulk create custom object records | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `rowsToWrite` | json | Yes | Array of records to create \[\{external_id?, data\}\] | | `allOrNothing` | boolean | No | If true, fail entire batch on any error | @@ -1937,7 +1876,6 @@ Bulk update custom object records | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `rowsToUpdate` | json | Yes | Array of records to update | | `allOrNothing` | boolean | No | If true, fail entire batch on any error | @@ -1961,7 +1899,6 @@ Bulk delete custom object records | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `customObjectApiName` | string | Yes | Custom object API name | | `rowsToDelete` | json | Yes | Array of records to delete | | `allOrNothing` | boolean | No | If true, fail entire batch on any error | @@ -1984,7 +1921,6 @@ List all custom apps | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | #### Output @@ -2015,7 +1951,6 @@ Get a specific custom app | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -2044,7 +1979,6 @@ Create a new custom app | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `name` | string | Yes | App name | | `apiName` | string | Yes | API name | | `description` | string | No | Description | @@ -2074,7 +2008,6 @@ Update a custom app | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | App ID | | `name` | string | No | App name | | `apiName` | string | No | API name | @@ -2105,7 +2038,6 @@ Delete a custom app | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | ID of the resource to delete | #### Output @@ -2126,7 +2058,6 @@ List all custom pages | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | #### Output @@ -2158,7 +2089,6 @@ Get a specific custom page | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -2188,7 +2118,6 @@ Create a new custom page | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `name` | string | Yes | Page name | #### Output @@ -2217,7 +2146,6 @@ Update a custom page | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Page ID | | `name` | string | No | Page name | @@ -2247,7 +2175,6 @@ Delete a custom page | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | ID of the resource to delete | #### Output @@ -2268,7 +2195,6 @@ List all custom settings | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `orderBy` | string | No | Sort field. Prefix with - for descending | | `cursor` | string | No | Pagination cursor from previous response | @@ -2303,7 +2229,6 @@ Get a specific custom setting | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -2334,7 +2259,6 @@ Create a new custom setting | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `displayName` | string | No | Display name | | `apiName` | string | No | Unique API name | | `dataType` | string | No | Data type of the setting | @@ -2370,7 +2294,6 @@ Update a custom setting | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Setting ID | | `displayName` | string | No | Display name | | `apiName` | string | No | Unique API name | @@ -2407,7 +2330,6 @@ Delete a custom setting | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | ID of the resource to delete | #### Output @@ -2428,7 +2350,6 @@ List all object categories | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | #### Output @@ -2455,7 +2376,6 @@ Get a specific object category | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Resource ID | #### Output @@ -2480,7 +2400,6 @@ Create a new object category | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `name` | string | Yes | Category name | | `description` | string | No | Description | @@ -2506,7 +2425,6 @@ Update an object category | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | Category ID | | `name` | string | No | Category name | | `description` | string | No | Description | @@ -2533,7 +2451,6 @@ Delete an object category | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `id` | string | Yes | ID of the resource to delete | #### Output @@ -2554,7 +2471,6 @@ Get a report run by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `runId` | string | Yes | run id | #### Output @@ -2581,7 +2497,6 @@ Trigger a new report run | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `reportId` | string | Yes | Report ID to run | | `includeObjectIds` | boolean | No | Include object IDs in the report | | `includeTotalRows` | boolean | No | Include total row count | @@ -2612,7 +2527,6 @@ Create bulk draft hires | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rippling API key | | `draftHires` | json | Yes | Array of draft hire objects | #### Output diff --git a/apps/docs/content/docs/integrations/rocketlane.mdx b/apps/docs/content/docs/integrations/rocketlane.mdx index 45ae4072bbd..2c65ece8311 100644 --- a/apps/docs/content/docs/integrations/rocketlane.mdx +++ b/apps/docs/content/docs/integrations/rocketlane.mdx @@ -2,7 +2,7 @@ title: Rocketlane description: Manage client onboarding projects, tasks, time tracking, and invoices integration: - type: "rocketlane" + type: "rocketlane_v2" color: "#000000" --- @@ -31,7 +31,6 @@ Create a new Rocketlane project with a customer, owner, dates, team members, tem | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectName` | string | Yes | Name of the project | | `customerCompanyName` | string | Yes | Name of the customer company \(case-sensitive exact match; cannot be changed after creation\) | | `ownerUserId` | number | No | User ID of the project owner \(either ownerUserId or ownerEmailId must be provided\) | @@ -185,7 +184,6 @@ Retrieve a Rocketlane project by its unique identifier | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project | | `includeFields` | string | No | Comma-separated extra fields to return in the response \(e.g. budgetedHours,progressPercentage\) | | `includeAllFields` | boolean | No | Return all fields in the response body | @@ -309,7 +307,6 @@ List Rocketlane projects with optional filters, sorting, and token-based paginat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `pageSize` | number | No | Maximum number of projects per page \(defaults to 100\) | | `pageToken` | string | No | Page token from a previous response \(valid for 15 minutes\) | | `includeFields` | string | No | Comma-separated extra fields to return in the response \(e.g. budgetedHours,progressPercentage\) | @@ -456,7 +453,6 @@ Update a Rocketlane project by ID, including name, dates, visibility, owner, sta | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project to update | | `projectName` | string | No | New name of the project | | `startDate` | string | No | Date on which the project begins \(YYYY-MM-DD\) | @@ -593,7 +589,6 @@ Archive a Rocketlane project by ID, making it dormant while preserving its detai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project to archive | #### Output @@ -615,7 +610,6 @@ Permanently delete a Rocketlane project by ID (irreversible; only Admins, Super | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project to delete | #### Output @@ -637,7 +631,6 @@ Add team members and customer stakeholders to a Rocketlane project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project | | `memberUserIds` | array | No | User IDs of team members from your organization to add | | `memberEmailIds` | array | No | Emails of team members from your organization to add | @@ -763,7 +756,6 @@ Remove team members from a Rocketlane project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project | | `memberUserIds` | array | No | User IDs of team members to remove \(at least one of memberUserIds or memberEmailIds is required\) | | `memberEmailIds` | array | No | Emails of team members to remove \(at least one of memberUserIds or memberEmailIds is required\) | @@ -887,7 +879,6 @@ Import a project template into an existing Rocketlane project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project to import the template into | | `templateId` | number | Yes | Unique identifier of the template to import | | `startDate` | string | Yes | Date on which the template goes into effect for the project \(YYYY-MM-DD\) | @@ -1012,7 +1003,6 @@ List the placeholders of a Rocketlane project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project | #### Output @@ -1049,7 +1039,6 @@ Assign a placeholder in a Rocketlane project to a user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project | | `placeholderId` | number | Yes | Unique identifier of the placeholder to assign | | `userId` | number | No | User ID of the project member to assign \(either userId or userEmailId must be provided; must be a customer user for CUSTOMER placeholders\) | @@ -1189,7 +1178,6 @@ Unassign a placeholder from its user in a Rocketlane project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | Unique identifier of the project | | `placeholderId` | number | Yes | Unique identifier of the placeholder to unassign | @@ -1327,7 +1315,6 @@ Create a new task in a Rocketlane project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskName` | string | Yes | Name of the task | | `projectId` | number | Yes | ID of the project the task belongs to | | `taskDescription` | string | No | Description of the task in HTML format | @@ -1444,7 +1431,6 @@ Retrieve detailed information about a Rocketlane task by its unique identifier | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to retrieve | | `includeFields` | array | No | Extra fields to include in the response \(startDateActual, dueDateActual, type, phase, assignees, followers, dependencies, billable, csatEnabled, priority, timeEntryCategory, financialsBudget, taskPrivateNote, parent, externalReferenceId\) | | `includeAllFields` | boolean | No | Whether all fields should be returned in the response | @@ -1543,7 +1529,6 @@ Retrieve all Rocketlane tasks with optional filters, sorting, and pagination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `pageSize` | number | No | Number of tasks per page \(defaults to 100\) | | `pageToken` | string | No | Token pointing to the next page of results, from a previous response | | `includeFields` | array | No | Extra fields to include in the response \(startDateActual, dueDateActual, type, phase, assignees, followers, dependencies, billable, csatEnabled, priority, timeEntryCategory, financialsBudget, taskPrivateNote, parent, externalReferenceId\) | @@ -1662,7 +1647,6 @@ Update the properties of an existing Rocketlane task by its unique identifier | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to update | | `taskName` | string | No | New name of the task | | `taskDescription` | string | No | Description of the task in HTML format | @@ -1773,7 +1757,6 @@ Permanently delete a Rocketlane task by its unique identifier | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to delete | #### Output @@ -1795,7 +1778,6 @@ Move a Rocketlane task to a given phase, associating the task with that phase | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to move | | `phaseId` | number | Yes | Unique identifier of the phase to move the task to | @@ -1893,7 +1875,6 @@ Add members as assignees to a Rocketlane task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to add assignees to | | `memberUserIds` | array | No | User IDs of members to add as assignees | | `memberEmailIds` | array | No | Email addresses of members to add as assignees | @@ -1992,7 +1973,6 @@ Remove assignees from a Rocketlane task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to remove assignees from | | `memberUserIds` | array | No | User IDs of members to remove from the assignees | | `memberEmailIds` | array | No | Email addresses of members to remove from the assignees | @@ -2091,7 +2071,6 @@ Add members as followers to a Rocketlane task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to add followers to | | `memberUserIds` | array | No | User IDs of members to add as followers | | `memberEmailIds` | array | No | Email addresses of members to add as followers | @@ -2190,7 +2169,6 @@ Remove followers from a Rocketlane task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to remove followers from | | `memberUserIds` | array | No | User IDs of members to remove from the followers | | `memberEmailIds` | array | No | Email addresses of members to remove from the followers | @@ -2289,7 +2267,6 @@ Add finish-to-start dependencies between a Rocketlane task and other tasks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to add dependencies to | | `dependencyTaskIds` | array | Yes | Task IDs the task should depend on | @@ -2387,7 +2364,6 @@ Remove dependencies from a Rocketlane task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `taskId` | number | Yes | Unique identifier of the task to remove dependencies from | | `dependencyTaskIds` | array | Yes | Task IDs to remove from the task dependencies | @@ -2485,7 +2461,6 @@ Create a new phase in a Rocketlane project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `phaseName` | string | Yes | Name of the phase | | `projectId` | number | Yes | ID of the project to create the phase in | | `startDate` | string | Yes | Planned start date of the phase \(YYYY-MM-DD\) | @@ -2538,7 +2513,6 @@ Retrieve a single Rocketlane phase by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `phaseId` | number | Yes | ID of the phase to retrieve | | `includeFields` | string | No | Comma-separated extra phase properties to include in the response \(supported: startDateActual, dueDateActual\) | | `includeAllFields` | boolean | No | Whether to return all phase properties in the response | @@ -2586,7 +2560,6 @@ List phases of a Rocketlane project, with optional filters, sorting, and paginat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | ID of the project to list phases for | | `pageSize` | number | No | Number of phases per page | | `pageToken` | string | No | Page token returned by a previous request \(valid for 15 minutes\) | @@ -2645,7 +2618,6 @@ Update the name, dates, status, or privacy of an existing Rocketlane phase | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `phaseId` | number | Yes | ID of the phase to update | | `phaseName` | string | No | New name of the phase | | `startDate` | string | No | New planned start date of the phase \(YYYY-MM-DD\) | @@ -2698,7 +2670,6 @@ Permanently delete a Rocketlane phase by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `phaseId` | number | Yes | ID of the phase to delete | #### Output @@ -2720,7 +2691,6 @@ Create a custom field in your Rocketlane account, with options for SINGLE_CHOICE | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `fieldLabel` | string | Yes | Name of the field | | `fieldType` | string | Yes | Type of the field \(TEXT, MULTI_LINE_TEXT, YES_OR_NO, DATE, SINGLE_CHOICE, MULTIPLE_CHOICE, SINGLE_USER, MULTIPLE_USER, NUMBER, NOTE, RATING\) | | `objectType` | string | Yes | Object the field is associated with \(PROJECT, TASK, or USER\) | @@ -2774,7 +2744,6 @@ Retrieve a single Rocketlane field by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `fieldId` | number | Yes | ID of the field to retrieve | | `includeFields` | string | No | Comma-separated extra field properties to include in the response \(supported: options\) | | `includeAllFields` | boolean | No | Whether to return all field properties in the response | @@ -2821,7 +2790,6 @@ List fields in your Rocketlane account, with optional filters, sorting, and pagi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `pageSize` | number | No | Number of fields per page | | `pageToken` | string | No | Page token returned by a previous request \(valid for 15 minutes\) | | `includeFields` | string | No | Comma-separated extra field properties to include in the response \(supported: options\) | @@ -2881,7 +2849,6 @@ Update the label, description, enabled state, or privacy of an existing Rocketla | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `fieldId` | number | Yes | ID of the field to update | | `fieldLabel` | string | No | New name of the field | | `fieldDescription` | string | No | New description of the field | @@ -2932,7 +2899,6 @@ Permanently delete a Rocketlane field by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `fieldId` | number | Yes | ID of the field to delete | #### Output @@ -2954,7 +2920,6 @@ Add a new option to an existing SINGLE_CHOICE or MULTIPLE_CHOICE Rocketlane fiel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `fieldId` | number | Yes | ID of the field to add the option to | | `optionLabel` | string | Yes | Display label of the new option | | `optionColor` | string | Yes | Color of the new option \(RED, YELLOW, GREEN, TEAL, CYAN, BLUE, PURPLE, MAGENTA, GRAY, COOL_GRAY\) | @@ -2980,7 +2945,6 @@ Update the label or color of an existing option on a SINGLE_CHOICE or MULTIPLE_C | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `fieldId` | number | Yes | ID of the field the option belongs to | | `optionValue` | number | Yes | Unique identifier of the option to update | | `optionLabel` | string | No | New display label of the option | @@ -3007,7 +2971,6 @@ Create a time entry in Rocketlane against an adhoc activity, task, project phase | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `date` | string | Yes | Date of the time entry in YYYY-MM-DD format | | `minutes` | number | Yes | Duration of the time entry in minutes \(1-1440\) | | `activityName` | string | No | Name of the adhoc activity to track time against. Exactly one source is required among activityName, taskId, projectPhaseId, and projectId — providing more than one results in an error | @@ -3107,7 +3070,6 @@ Get a single Rocketlane time entry by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `timeEntryId` | number | Yes | ID of the time entry to retrieve | | `includeFields` | string | No | Comma-separated extra fields to include in the response \(notes, sourceType, deleted, status, submittedBy, submittedAt, approvedBy, approvedAt, rejectedBy, rejectedAt, costRate, billRate\) | | `includeAllFields` | boolean | No | Whether all fields should be returned in the response | @@ -3197,7 +3159,6 @@ List Rocketlane time entries with optional filters, sorting, and cursor-based pa | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `dateEq` | string | No | Only entries on this exact date \(YYYY-MM-DD\) | | `dateGt` | string | No | Only entries after this date \(YYYY-MM-DD\) | | `dateGe` | string | No | Only entries on or after this date \(YYYY-MM-DD\) | @@ -3320,7 +3281,6 @@ Search Rocketlane time entries with filters, sorting, and pagination (deprecated | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `dateEq` | string | No | Only entries on this exact date \(YYYY-MM-DD\) | | `dateGt` | string | No | Only entries after this date \(YYYY-MM-DD\) | | `dateGe` | string | No | Only entries on or after this date \(YYYY-MM-DD\) | @@ -3433,7 +3393,6 @@ Update a Rocketlane time entry by ID. The activityName, notes, billable, and min | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `timeEntryId` | number | Yes | ID of the time entry to update | | `date` | string | Yes | Date of the time entry in YYYY-MM-DD format \(mandatory so the total time for the date does not exceed 24 hours\) | | `minutes` | number | Yes | Duration of the time entry in minutes \(1-1440\) | @@ -3529,7 +3488,6 @@ Permanently delete a Rocketlane time entry by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `timeEntryId` | number | Yes | ID of the time entry to delete | #### Output @@ -3551,7 +3509,6 @@ List the time entry categories configured in Rocketlane | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `pageSize` | number | No | Maximum number of categories per page \(defaults to 100\) | | `pageToken` | string | No | Page token from a previous response for fetching the next page | @@ -3580,7 +3537,6 @@ Create a time-off for a team member in Rocketlane. Holidays and weekends within | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `userId` | number | No | User ID of the team member taking the time-off \(provide this or the user email\) | | `userEmail` | string | No | Email of the team member taking the time-off \(provide this or the user ID\) | | `startDate` | string | Yes | Time-off start date in YYYY-MM-DD format | @@ -3636,7 +3592,6 @@ Retrieve a Rocketlane time-off by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `timeOffId` | number | Yes | Unique identifier of the time-off | | `includeFields` | array | No | Optional fields to include in the response: note, notifyUsers | | `includeAllFields` | boolean | No | Return all fields in the response | @@ -3683,7 +3638,6 @@ List time-offs in Rocketlane with optional date, type, and user filters, sorting | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `pageSize` | number | No | Maximum number of time-offs per page \(defaults to 100\) | | `pageToken` | string | No | Page token from a previous response \(valid for 15 minutes\) | | `includeFields` | array | No | Optional fields to include in the response: note, notifyUsers | @@ -3758,7 +3712,6 @@ Permanently delete a Rocketlane time-off by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `timeOffId` | number | Yes | Unique identifier of the time-off to delete | #### Output @@ -3780,7 +3733,6 @@ Retrieve a Rocketlane user by their ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `userId` | number | Yes | ID of the user to retrieve | | `includeFields` | string | No | Comma-separated extra fields to include: role, company, permission, holidayCalendar, capacityInMinutes, profilePictureUrl | | `includeAllFields` | boolean | No | Whether to include all fields in the response | @@ -3840,7 +3792,6 @@ List users in your Rocketlane account, with optional filters, sorting, and pagin | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `pageSize` | number | No | Maximum number of users per page \(defaults to 100\) | | `pageToken` | string | No | Page token from a previous request \(valid for 15 minutes\) | | `includeFields` | string | No | Comma-separated extra fields to include: role, company, permission, holidayCalendar, capacityInMinutes, profilePictureUrl | @@ -3944,7 +3895,6 @@ Create a new space in a Rocketlane project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | ID of the project the space belongs to | | `spaceName` | string | Yes | Name of the space | | `private` | boolean | No | Whether the space is private or shared \(defaults to false\) | @@ -3985,7 +3935,6 @@ Retrieve a Rocketlane space by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `spaceId` | number | Yes | ID of the space to retrieve | #### Output @@ -4024,7 +3973,6 @@ List spaces in a Rocketlane project, with optional filters, sorting, and paginat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | ID of the project whose spaces to list | | `pageSize` | number | No | Maximum number of spaces per page \(defaults to 100\) | | `pageToken` | string | No | Page token from a previous request \(valid for 15 minutes\) | @@ -4086,7 +4034,6 @@ Update a Rocketlane space by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `spaceId` | number | Yes | ID of the space to update | | `spaceName` | string | No | New name of the space | @@ -4126,7 +4073,6 @@ Permanently delete a Rocketlane space by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `spaceId` | number | Yes | ID of the space to delete | #### Output @@ -4148,7 +4094,6 @@ Create a new space document in a Rocketlane space | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `spaceId` | number | Yes | ID of the space the document belongs to | | `spaceDocumentType` | string | Yes | Type of the space document: ROCKETLANE_DOCUMENT or EMBEDDED_DOCUMENT | | `spaceDocumentName` | string | No | Name of the space document \(defaults to Untitled\) | @@ -4196,7 +4141,6 @@ Retrieve a Rocketlane space document by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `spaceDocumentId` | number | Yes | ID of the space document to retrieve | #### Output @@ -4240,7 +4184,6 @@ List space documents in a Rocketlane project, with optional filters, sorting, an | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `projectId` | number | Yes | ID of the project whose space documents to list | | `pageSize` | number | No | Maximum number of space documents per page \(defaults to 100\) | | `pageToken` | string | No | Page token from a previous request \(valid for 15 minutes\) | @@ -4308,7 +4251,6 @@ Update a Rocketlane space document by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `spaceDocumentId` | number | Yes | ID of the space document to update | | `spaceDocumentName` | string | No | New name of the space document | | `url` | string | No | New URL to embed in the space document \(for embedded documents\) | @@ -4354,7 +4296,6 @@ Permanently delete a Rocketlane space document by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `spaceDocumentId` | number | Yes | ID of the space document to delete | #### Output @@ -4376,7 +4317,6 @@ List resource allocations in Rocketlane within a date range, with optional membe | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `startDate` | string | Yes | Return allocations that start on or after this date \(YYYY-MM-DD\) | | `endDate` | string | Yes | Return allocations that end on or before this date \(YYYY-MM-DD\) | | `pageSize` | number | No | Maximum number of allocations per page \(defaults to 100\) | @@ -4463,7 +4403,6 @@ Retrieve a Rocketlane invoice by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `invoiceId` | number | Yes | Unique identifier of the invoice | | `includeFields` | array | No | Optional fields to include in the response: notes, attachments | | `includeAllFields` | boolean | No | Return all fields in the response | @@ -4530,7 +4469,6 @@ Search invoices in Rocketlane with date, amount, company, invoice-number, and st | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `pageSize` | number | No | Maximum number of invoices per page \(defaults to 100\) | | `pageToken` | string | No | Page token from a previous response \(valid for 15 minutes\) | | `includeFields` | array | No | Optional fields to include in the response: notes, attachments | @@ -4650,7 +4588,6 @@ List line items of a Rocketlane invoice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `invoiceId` | number | Yes | Unique identifier of the invoice | | `pageSize` | number | No | Maximum number of line items per page \(defaults to 100\) | | `pageToken` | string | No | Page token from a previous response \(valid for 15 minutes\) | @@ -4696,7 +4633,6 @@ List payments recorded against a Rocketlane invoice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rocketlane API key | | `invoiceId` | number | Yes | Unique identifier of the invoice | | `pageSize` | number | No | Maximum number of payments per page \(defaults to 100\) | | `pageToken` | string | No | Page token from a previous response \(valid for 15 minutes\) | diff --git a/apps/docs/content/docs/integrations/rootly.mdx b/apps/docs/content/docs/integrations/rootly.mdx index 1771c2d2c90..90796f70d1f 100644 --- a/apps/docs/content/docs/integrations/rootly.mdx +++ b/apps/docs/content/docs/integrations/rootly.mdx @@ -2,7 +2,7 @@ title: Rootly description: Manage incidents, alerts, and on-call with Rootly integration: - type: "rootly" + type: "rootly_v2" color: "#6C72C8" --- @@ -29,7 +29,6 @@ Create a new incident in Rootly with optional severity, services, and teams. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `title` | string | No | The title of the incident \(auto-generated if not provided\) | | `summary` | string | No | A summary of the incident | | `severityId` | string | No | Severity ID to attach to the incident | @@ -79,7 +78,6 @@ Retrieve a single incident by ID from Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident to retrieve | #### Output @@ -118,7 +116,6 @@ Update an existing incident in Rootly (status, severity, summary, etc.). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident to update | | `title` | string | No | Updated incident title | | `summary` | string | No | Updated incident summary | @@ -172,7 +169,6 @@ List incidents from Rootly with optional filtering by status, severity, and more | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `status` | string | No | Filter by status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) | | `severity` | string | No | Filter by severity slug | | `search` | string | No | Search term to filter incidents | @@ -220,7 +216,6 @@ Create a new alert in Rootly for on-call notification and routing. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `summary` | string | Yes | The summary of the alert | | `description` | string | No | A detailed description of the alert | | `source` | string | No | The source of the alert \(e.g., api, manual, datadog, pagerduty\) | @@ -263,7 +258,6 @@ List alerts from Rootly with optional filtering by status, source, and services. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `status` | string | No | Filter by status \(open, triggered, acknowledged, resolved\) | | `source` | string | No | Filter by source \(e.g., api, datadog, pagerduty\) | | `services` | string | No | Filter by service slugs \(comma-separated\) | @@ -304,7 +298,6 @@ Add a timeline event to an existing incident in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident to add the event to | | `event` | string | Yes | The summary/description of the event | | `visibility` | string | No | Event visibility \(internal or external\) | @@ -332,7 +325,6 @@ List services from Rootly with optional search filtering. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter services | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -363,7 +355,6 @@ List severity levels configured in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter severities | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -396,7 +387,6 @@ List teams (groups) configured in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter teams | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -427,7 +417,6 @@ List environments configured in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter environments | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -458,7 +447,6 @@ List incident types configured in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Filter incident types by name | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -489,7 +477,6 @@ List functionalities configured in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter functionalities | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -520,7 +507,6 @@ List incident retrospectives (post-mortems) from Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `status` | string | No | Filter by status \(draft, published\) | | `search` | string | No | Search term to filter retrospectives | | `pageSize` | number | No | Number of items per page \(default: 20\) | @@ -554,7 +540,6 @@ Delete an incident by ID from Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident to delete | #### Output @@ -576,7 +561,6 @@ Retrieve a single alert by ID from Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `alertId` | string | Yes | The ID of the alert to retrieve | #### Output @@ -610,7 +594,6 @@ Update an existing alert in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `alertId` | string | Yes | The ID of the alert to update | | `summary` | string | No | Updated alert summary | | `description` | string | No | Updated alert description | @@ -653,7 +636,6 @@ Acknowledge an alert in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `alertId` | string | Yes | The ID of the alert to acknowledge | #### Output @@ -687,7 +669,6 @@ Resolve an alert in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `alertId` | string | Yes | The ID of the alert to resolve | | `resolutionMessage` | string | No | Message describing how the alert was resolved | | `resolveRelatedIncidents` | boolean | No | Whether to also resolve related incidents | @@ -723,7 +704,6 @@ Create a new action item for an incident in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident to add the action item to | | `summary` | string | Yes | The title of the action item | | `description` | string | No | A detailed description of the action item | @@ -760,7 +740,6 @@ List action items for an incident in Rootly. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident to list action items for | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -793,7 +772,6 @@ List users from Rootly with optional search and email filtering. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter users | | `email` | string | No | Filter users by email address | | `pageSize` | number | No | Number of items per page \(default: 20\) | @@ -826,7 +804,6 @@ List current on-call entries from Rootly with optional filtering. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `scheduleIds` | string | No | Comma-separated schedule IDs to filter by | | `escalationPolicyIds` | string | No | Comma-separated escalation policy IDs to filter by | | `userIds` | string | No | Comma-separated user IDs to filter by | @@ -859,7 +836,6 @@ List on-call schedules from Rootly with optional search filtering. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter schedules | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -889,7 +865,6 @@ List escalation policies from Rootly with optional search filtering. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter escalation policies | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -921,7 +896,6 @@ List causes from Rootly with optional search filtering. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter causes | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -952,7 +926,6 @@ List playbooks from Rootly with pagination support. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -981,7 +954,6 @@ Transition a Rootly incident to the mitigated state. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident to mitigate | | `mitigationMessage` | string | No | How was the incident mitigated? | @@ -1021,7 +993,6 @@ Transition a Rootly incident to the resolved state. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident to resolve | | `resolutionMessage` | string | No | How was the incident resolved? | @@ -1061,7 +1032,6 @@ Assign an incident role (e.g. commander) to a user on a Rootly incident. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident | | `userId` | string | Yes | The ID of the user to assign \(use List Users to find IDs\) | | `incidentRoleId` | string | Yes | The ID of the incident role \(use List Incident Roles to find IDs\) | @@ -1102,7 +1072,6 @@ Remove an incident role assignment from a user on a Rootly incident. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident | | `userId` | string | Yes | The ID of the user to unassign \(use List Users to find IDs\) | | `incidentRoleId` | string | Yes | The ID of the incident role to remove \(use List Incident Roles to find IDs\) | @@ -1143,7 +1112,6 @@ Subscribe users to a Rootly incident so they receive updates. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident | | `userIds` | string | Yes | Comma-separated user IDs to subscribe \(use List Users to find IDs\) | @@ -1183,7 +1151,6 @@ Unsubscribe users from a Rootly incident. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident | | `userIds` | string | Yes | Comma-separated user IDs to unsubscribe \(use List Users to find IDs\) | @@ -1223,7 +1190,6 @@ Post a public status page update for a Rootly incident. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident | | `event` | string | Yes | The status page update message to publish | | `statusPageId` | string | No | The ID of the status page to post to | @@ -1258,7 +1224,6 @@ Update a Rootly incident action item (status, priority, assignee, etc.). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `actionItemId` | string | Yes | The ID of the action item to update | | `summary` | string | No | Updated action item title | | `description` | string | No | Updated description | @@ -1295,7 +1260,6 @@ Delete a Rootly incident action item. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `actionItemId` | string | Yes | The ID of the action item to delete | #### Output @@ -1317,7 +1281,6 @@ Snooze a Rootly alert for a set number of minutes. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `alertId` | string | Yes | The ID of the alert to snooze | | `delayMinutes` | number | Yes | Number of minutes to snooze the alert | @@ -1352,7 +1315,6 @@ Escalate a Rootly alert, optionally to a specific escalation policy or level. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `alertId` | string | Yes | The ID of the alert to escalate | | `escalationPolicyId` | string | No | Escalation policy ID to escalate to \(use List Escalation Policies to find IDs\) | | `escalationPolicyLevel` | number | No | Escalation policy level to escalate to | @@ -1388,7 +1350,6 @@ List the timeline events for a Rootly incident. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `incidentId` | string | Yes | The ID of the incident | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | @@ -1418,7 +1379,6 @@ Trigger a Rootly automation workflow, optionally scoped to an incident or alert. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `workflowId` | string | Yes | The ID of the workflow to run | | `incidentId` | string | No | Incident ID to run the workflow against | | `alertId` | string | No | Alert ID to run the workflow against | @@ -1454,7 +1414,6 @@ List incident roles configured in Rootly (e.g. commander, scribe). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Rootly API key | | `search` | string | No | Search term to filter incident roles | | `pageSize` | number | No | Number of items per page \(default: 20\) | | `pageNumber` | number | No | Page number for pagination | diff --git a/apps/docs/content/docs/integrations/s3.mdx b/apps/docs/content/docs/integrations/s3.mdx index ba5e8c119f1..232b598899d 100644 --- a/apps/docs/content/docs/integrations/s3.mdx +++ b/apps/docs/content/docs/integrations/s3.mdx @@ -2,7 +2,7 @@ title: S3 description: Upload, download, list, and manage S3 files and buckets integration: - type: "s3" + type: "s3_v2" color: "linear-gradient(45deg, #1B660F 0%, #6CAE3E 100%)" --- @@ -10,12 +10,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Amazon S3](https://aws.amazon.com/s3/) to manage objects and buckets from a workflow. Upload, download, copy, or delete objects, inspect metadata, and create time-limited presigned URLs. Configure an AWS access key and secret access key. +Use [Amazon S3](https://aws.amazon.com/s3/) to manage objects and buckets from a workflow. Upload, download, copy, or delete objects, inspect metadata, and create time-limited presigned URLs. Connect with a saved [AWS credential](/integrations/aws-credentials). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Upload, download, copy, and delete objects (individually or in batches), inspect object metadata, generate time-limited presigned URLs, list bucket contents, and create, list, or delete buckets. Requires AWS access key and secret access key. +Upload, download, copy, and delete objects (individually or in batches), inspect object metadata, generate time-limited presigned URLs, list bucket contents, and create, list, or delete buckets. ## Actions @@ -29,8 +29,6 @@ Upload a file to an AWS S3 bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | | `bucketName` | string | Yes | S3 bucket name \(e.g., my-bucket\) | | `objectKey` | string | Yes | Object key/path in S3 \(e.g., folder/filename.ext\) | @@ -59,8 +57,6 @@ Retrieve an object from an AWS S3 bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | No | Optional region override when URL does not include region \(e.g., us-east-1, eu-west-1\) | | `s3Uri` | string | Yes | S3 Object URL \(e.g., https://bucket.s3.region.amazonaws.com/path/to/file) | @@ -84,8 +80,6 @@ List objects in an AWS S3 bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | | `bucketName` | string | Yes | S3 bucket name \(e.g., my-bucket\) | | `prefix` | string | No | Prefix to filter objects \(e.g., folder/, images/2024/\) | @@ -115,8 +109,6 @@ Delete an object from an AWS S3 bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | | `bucketName` | string | Yes | S3 bucket name \(e.g., my-bucket\) | | `objectKey` | string | Yes | Object key/path to delete \(e.g., folder/file.txt\) | @@ -140,8 +132,6 @@ Copy an object within or between AWS S3 buckets | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | | `sourceBucket` | string | Yes | Source bucket name \(e.g., my-bucket\) | | `sourceKey` | string | Yes | Source object key/path \(e.g., folder/file.txt\) | @@ -169,8 +159,6 @@ List the S3 buckets owned by the authenticated AWS account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region to address the request to \(e.g., us-east-1\) | | `prefix` | string | No | Limit the response to bucket names that begin with this prefix | | `maxBuckets` | number | No | Maximum number of buckets to return \(1-10000\) | @@ -198,8 +186,6 @@ Retrieve metadata for an S3 object without downloading its body | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | | `bucketName` | string | Yes | S3 bucket name \(e.g., my-bucket\) | | `objectKey` | string | Yes | Object key/path to inspect \(e.g., folder/file.txt\) | @@ -224,8 +210,6 @@ Create a new AWS S3 bucket in the specified region | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region to create the bucket in \(e.g., us-east-1\) | | `bucketName` | string | Yes | Name for the new S3 bucket \(must be globally unique\) | | `acl` | string | No | Canned ACL for the bucket \(e.g., private, public-read\) | @@ -248,8 +232,6 @@ Delete an empty AWS S3 bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region where the bucket is located \(e.g., us-east-1\) | | `bucketName` | string | Yes | Name of the S3 bucket to delete \(must be empty\) | @@ -272,8 +254,6 @@ Generate a time-limited presigned URL to download or upload an S3 object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region where the bucket is located \(e.g., us-east-1\) | | `bucketName` | string | Yes | S3 bucket name \(e.g., my-bucket\) | | `objectKey` | string | Yes | Object key/path for the presigned URL \(e.g., folder/file.txt\) | @@ -300,8 +280,6 @@ Delete multiple objects from an AWS S3 bucket in a single batch request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | Your AWS Access Key ID | -| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | | `bucketName` | string | Yes | S3 bucket name \(e.g., my-bucket\) | | `keys` | json | Yes | Array of object keys to delete \(e.g., \["a.txt", "folder/b.txt"\]\). Max 1000. | diff --git a/apps/docs/content/docs/integrations/secrets_manager.mdx b/apps/docs/content/docs/integrations/secrets_manager.mdx index d2f373a90ab..06e16ad4ee9 100644 --- a/apps/docs/content/docs/integrations/secrets_manager.mdx +++ b/apps/docs/content/docs/integrations/secrets_manager.mdx @@ -2,7 +2,7 @@ title: AWS Secrets Manager description: Connect to AWS Secrets Manager integration: - type: "secrets_manager" + type: "secrets_manager_v2" color: "linear-gradient(45deg, #BD0816 0%, #FF5252 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) to retrieve, create, update, list, describe, delete, restore, tag, untag, or rotate secrets. Configure AWS credentials with permission for the target region and secret. +Use [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) to retrieve, create, update, list, describe, delete, restore, tag, untag, or rotate secrets. Connect with a saved [AWS credential](/integrations/aws-credentials) that has permission for the target region and secret. Deletion and rotation affect credentials used by other systems. Review the selected operation's recovery window and rotation settings before running it. Secret values returned by these actions are available to later workflow steps; handle them as credentials. {/* MANUAL-CONTENT-END */} @@ -32,8 +32,6 @@ Retrieve a secret value from AWS Secrets Manager | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `secretId` | string | Yes | The name or ARN of the secret to retrieve | | `versionId` | string | No | The unique identifier of the version to retrieve | | `versionStage` | string | No | The staging label of the version to retrieve \(e.g., AWSCURRENT, AWSPREVIOUS\) | @@ -62,8 +60,6 @@ List secrets stored in AWS Secrets Manager | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `maxResults` | number | No | Maximum number of secrets to return \(1-100, default 100\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -88,8 +84,6 @@ Create a new secret in AWS Secrets Manager | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Name of the secret to create | | `secretValue` | string | Yes | The secret value \(plain text or JSON string\) | | `description` | string | No | Description of the secret | @@ -116,8 +110,6 @@ Update the value of an existing secret in AWS Secrets Manager | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `secretId` | string | Yes | The name or ARN of the secret to update | | `secretValue` | string | Yes | The new secret value \(plain text or JSON string\) | | `description` | string | No | Updated description of the secret | @@ -144,8 +136,6 @@ Delete a secret from AWS Secrets Manager | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `secretId` | string | Yes | The name or ARN of the secret to delete | | `recoveryWindowInDays` | number | No | Number of days before permanent deletion \(7-30, default 30\) | | `forceDelete` | boolean | No | If true, immediately delete without recovery window | @@ -172,8 +162,6 @@ Retrieve full metadata for a secret in AWS Secrets Manager, including rotation c | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `secretId` | string | Yes | The name or ARN of the secret to describe | #### Output @@ -212,8 +200,6 @@ Attach tags to a secret in AWS Secrets Manager | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `secretId` | string | Yes | The name or ARN of the secret to tag | | `tags` | json | Yes | Tags to attach, as an array of \{key, value\} pairs \(max 50\) | @@ -237,8 +223,6 @@ Remove tags from a secret in AWS Secrets Manager | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `secretId` | string | Yes | The name or ARN of the secret to untag | | `tagKeys` | json | Yes | Tag keys to remove, as an array of strings \(max 50\) | @@ -262,8 +246,6 @@ Cancel a scheduled deletion for a secret in AWS Secrets Manager, restoring acces | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `secretId` | string | Yes | The name or ARN of the secret to restore | #### Output @@ -287,8 +269,6 @@ Start or reconfigure rotation for a secret in AWS Secrets Manager | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `secretId` | string | Yes | The name or ARN of the secret to rotate | | `clientRequestToken` | string | No | Idempotency token for the new secret version \(32-64 characters\) | | `rotationLambdaARN` | string | No | ARN of the Lambda function that performs rotation \(omit for managed rotation\) | diff --git a/apps/docs/content/docs/integrations/sendgrid.mdx b/apps/docs/content/docs/integrations/sendgrid.mdx index d1357144a95..869ebd7997b 100644 --- a/apps/docs/content/docs/integrations/sendgrid.mdx +++ b/apps/docs/content/docs/integrations/sendgrid.mdx @@ -2,7 +2,7 @@ title: SendGrid description: Send emails and manage contacts, lists, and templates with SendGrid integration: - type: "sendgrid" + type: "sendgrid_v2" color: "#1A82E2" --- @@ -29,7 +29,6 @@ Send an email using SendGrid API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `from` | string | Yes | Sender email address \(must be verified in SendGrid\) | | `fromName` | string | No | Sender name | | `to` | string | Yes | Recipient email address | @@ -66,7 +65,6 @@ Add a new contact to SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `email` | string | Yes | Contact email address | | `firstName` | string | No | Contact first name | | `lastName` | string | No | Contact last name | @@ -95,7 +93,6 @@ Get a specific contact by ID from SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `contactId` | string | Yes | Contact ID | #### Output @@ -123,7 +120,6 @@ Search for contacts in SendGrid using a query | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `query` | string | Yes | Search query \(e.g., "email LIKE '%example.com%' AND CONTAINS\(list_ids, 'list-id'\)"\) | #### Output @@ -145,7 +141,6 @@ Delete one or more contacts from SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `contactIds` | string | Yes | Comma-separated contact IDs to delete | #### Output @@ -166,7 +161,6 @@ Create a new contact list in SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `name` | string | Yes | List name | #### Output @@ -189,7 +183,6 @@ Get a specific list by ID from SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `listId` | string | Yes | List ID | #### Output @@ -212,7 +205,6 @@ Get all contact lists from SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `pageSize` | number | No | Number of lists to return per page \(default: 100, max: 1000\) | | `pageToken` | string | No | Page token from a previous response \(nextPageToken\) to fetch the next page | @@ -235,7 +227,6 @@ Delete a contact list from SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `listId` | string | Yes | List ID to delete | #### Output @@ -256,7 +247,6 @@ Add or update contacts and assign them to a list in SendGrid (uses PUT /v3/marke | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `listId` | string | Yes | List ID to add contacts to | | `contacts` | json | Yes | JSON array of contact objects. Each contact must have at least: email \(or phone_number_id/external_id/anonymous_id\). Example: \[\{"email": "user@example.com", "first_name": "John"\}\] | @@ -279,7 +269,6 @@ Remove contacts from a specific list in SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `listId` | string | Yes | List ID | | `contactIds` | string | Yes | Comma-separated contact IDs to remove from the list | @@ -301,7 +290,6 @@ Create a new email template in SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `name` | string | Yes | Template name | | `generation` | string | No | Template generation type \(legacy or dynamic, default: dynamic\) | @@ -327,7 +315,6 @@ Get a specific template by ID from SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `templateId` | string | Yes | Template ID | #### Output @@ -352,7 +339,6 @@ Get all email templates from SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `generations` | string | No | Filter by generation \(legacy, dynamic, or both\) | | `pageSize` | number | No | Number of templates to return per page \(default: 20, max: 200\). When paginating with pageToken, pass the same pageSize used on the first request to keep page boundaries consistent. | | `pageToken` | string | No | Page token from a previous response \(nextPageToken\) to fetch the next page | @@ -376,7 +362,6 @@ Delete an email template from SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `templateId` | string | Yes | Template ID to delete | #### Output @@ -395,7 +380,6 @@ Create a new version of an email template in SendGrid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SendGrid API key | | `templateId` | string | Yes | Template ID | | `name` | string | Yes | Version name | | `subject` | string | Yes | Email subject line | diff --git a/apps/docs/content/docs/integrations/sentry.mdx b/apps/docs/content/docs/integrations/sentry.mdx index 4c7120ad937..eb30a0e5a9a 100644 --- a/apps/docs/content/docs/integrations/sentry.mdx +++ b/apps/docs/content/docs/integrations/sentry.mdx @@ -2,7 +2,7 @@ title: Sentry description: Manage Sentry issues, projects, events, and releases integration: - type: "sentry" + type: "sentry_v2" color: "#362D59" --- @@ -29,7 +29,6 @@ List issues from Sentry for a specific organization and optionally a specific pr | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `projectSlug` | string | No | Filter issues by numeric project ID \(e.g., "4501234"\). This organization-scoped endpoint requires the numeric project ID, not the project slug. | | `query` | string | No | Search query to filter issues. Supports Sentry search syntax \(e.g., "is:unresolved", "level:error"\) | @@ -98,7 +97,6 @@ Retrieve detailed information about a specific Sentry issue by its ID. Returns c | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `issueId` | string | Yes | The unique ID of the issue to retrieve \(e.g., "12345"\) | @@ -158,7 +156,6 @@ Update a Sentry issue by changing its status, assignment, bookmark state, or oth | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `issueId` | string | Yes | The unique ID of the issue to update \(e.g., "12345"\) | | `status` | string | No | New status for the issue: resolved, unresolved, ignored, or resolvedInNextRelease | @@ -199,7 +196,6 @@ List all projects in a Sentry organization. Returns project details including na | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `cursor` | string | No | Pagination cursor for retrieving next page of results | | `limit` | number | No | Number of projects to return per page \(default: 25, max: 100\) | @@ -243,7 +239,6 @@ Retrieve detailed information about a specific Sentry project by its slug. Retur | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `projectSlug` | string | Yes | The slug of the project to retrieve \(e.g., "my-project"\) | @@ -298,7 +293,6 @@ Create a new Sentry project in an organization. Requires a team to associate the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `name` | string | Yes | The name of the project | | `teamSlug` | string | Yes | The slug of the team that will own this project | @@ -349,7 +343,6 @@ Update a Sentry project by changing its name, slug, platform, or other settings. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `projectSlug` | string | Yes | The slug of the project to update \(e.g., "my-project"\) | | `name` | string | No | New name for the project | @@ -390,7 +383,6 @@ List events from a Sentry project. Can be filtered by issue ID, query, or time p | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `projectSlug` | string | Yes | The slug of the project to list events from \(e.g., "my-project"\) | | `issueId` | string | No | Filter events by a specific issue ID \(e.g., "12345"\) | @@ -455,7 +447,6 @@ Retrieve detailed information about a specific Sentry event by its ID. Returns c | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `projectSlug` | string | Yes | The slug of the project \(e.g., "my-project"\) | | `eventId` | string | Yes | The unique ID of the event to retrieve \(e.g., "abc123def456"\) | @@ -513,7 +504,6 @@ List releases for a Sentry organization or project. Returns release details incl | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `projectSlug` | string | No | Filter releases by numeric project ID \(e.g., "4501234"\). This organization-scoped endpoint requires the numeric project ID, not the project slug. | | `query` | string | No | Search query to filter releases \(e.g., "1.0" to match version patterns\) | @@ -581,7 +571,6 @@ Create a new release in Sentry. A release is a version of your code deployed to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `version` | string | Yes | Version identifier for the release \(e.g., "2.0.0", "my-app@1.0.0", or a git commit SHA\) | | `projects` | string | Yes | Comma-separated list of project slugs to associate with this release | @@ -648,7 +637,6 @@ Create a deploy record for a Sentry release in a specific environment. Deploys t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `version` | string | Yes | Version identifier of the release being deployed \(e.g., "1.0.0" or "abc123"\) | | `environment` | string | Yes | Environment name where the release is being deployed \(e.g., "production", "staging"\) | @@ -681,7 +669,6 @@ List all teams in a Sentry organization. Useful for discovering the team slug re | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sentry API authentication token | | `organizationSlug` | string | Yes | The slug of the organization \(e.g., "my-org"\) | | `query` | string | No | Filter teams by name or slug | | `cursor` | string | No | Pagination cursor for retrieving next page of results | diff --git a/apps/docs/content/docs/integrations/ses.mdx b/apps/docs/content/docs/integrations/ses.mdx index 1e5934752a0..3a9719dfd91 100644 --- a/apps/docs/content/docs/integrations/ses.mdx +++ b/apps/docs/content/docs/integrations/ses.mdx @@ -2,7 +2,7 @@ title: AWS SES description: Send emails and manage templates with AWS Simple Email Service integration: - type: "ses" + type: "ses_v2" color: "linear-gradient(45deg, #BD0816 0%, #FF5252 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Amazon SES](https://aws.amazon.com/ses/) to send simple, templated, or bulk emails and manage templates, identities, configuration sets, and suppression lists. Account and identity actions report sending configuration and quotas. +Use [Amazon SES](https://aws.amazon.com/ses/) to send simple, templated, or bulk emails and manage templates, identities, configuration sets, and suppression lists. Account and identity actions report sending configuration and quotas. Connect with a saved [AWS credential](/integrations/aws-credentials). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -30,8 +30,6 @@ Send an email via AWS SES using simple or HTML content | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `fromAddress` | string | Yes | Verified sender email address | | `toAddresses` | string | Yes | Comma-separated list of recipient email addresses | | `subject` | string | Yes | Email subject line | @@ -61,8 +59,6 @@ Send an email using an SES email template with dynamic template data | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `fromAddress` | string | Yes | Verified sender email address | | `toAddresses` | string | Yes | Comma-separated list of recipient email addresses | | `templateName` | string | Yes | Name of the SES email template to use | @@ -90,8 +86,6 @@ Send emails to multiple recipients using an SES template with per-recipient data | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `fromAddress` | string | Yes | Verified sender email address | | `templateName` | string | Yes | Name of the SES email template to use | | `destinations` | string | Yes | JSON array of destination objects with toAddresses \(string\[\]\) and optional templateData \(JSON string\); falls back to defaultTemplateData when omitted | @@ -119,8 +113,6 @@ List all verified email identities (email addresses and domains) in your SES acc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `pageSize` | number | No | Maximum number of identities to return \(1-1000\) | | `nextToken` | string | No | Pagination token from a previous list response | @@ -145,8 +137,6 @@ Get SES account sending quota and status information | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | #### Output @@ -170,8 +160,6 @@ Create a new SES email template for use with templated email sending | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `templateName` | string | Yes | Unique name for the email template | | `subjectPart` | string | Yes | Subject line template \(supports \{\{variable\}\} substitution\) | | `textPart` | string | No | Plain text version of the template body | @@ -196,8 +184,6 @@ Retrieve the content and details of an SES email template | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `templateName` | string | Yes | Name of the template to retrieve | #### Output @@ -222,8 +208,6 @@ List all SES email templates in your account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `pageSize` | number | No | Maximum number of templates to return | | `nextToken` | string | No | Pagination token from a previous list response | @@ -248,8 +232,6 @@ Delete an existing SES email template | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `templateName` | string | Yes | Name of the template to delete | #### Output @@ -271,8 +253,6 @@ Update the subject, HTML, and text content of an existing SES email template | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `templateName` | string | Yes | The name of the template to update | | `subjectPart` | string | Yes | The subject line of the template | | `htmlPart` | string | No | The HTML body of the template | @@ -297,8 +277,6 @@ Add an email address to the account-level SES suppression list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `emailAddress` | string | Yes | The email address to add to the suppression list | | `reason` | string | Yes | The reason the address is suppressed: BOUNCE or COMPLAINT | @@ -321,8 +299,6 @@ Remove an email address from the account-level SES suppression list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `emailAddress` | string | Yes | The email address to remove from the suppression list | #### Output @@ -344,8 +320,6 @@ Retrieve details for a specific email address on the SES suppression list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `emailAddress` | string | Yes | The suppressed email address to look up | #### Output @@ -371,8 +345,6 @@ List email addresses on the account-level SES suppression list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `reasons` | string | No | Comma-separated suppression reasons to filter by: BOUNCE, COMPLAINT | | `startDate` | string | No | Only include addresses suppressed after this ISO 8601 date | | `endDate` | string | No | Only include addresses suppressed before this ISO 8601 date | @@ -400,8 +372,6 @@ Start verification of a new SES email address or domain identity | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `emailIdentity` | string | Yes | The email address or domain to verify | | `dkimSigningAttributes` | json | No | Bring-your-own-DKIM signing attributes as JSON \(domainSigningSelector, domainSigningPrivateKey, nextSigningKeyLength\). Domain identities only. | | `tags` | json | No | JSON array of tags to associate with the identity: \[\{"key":"","value":""\}\] | @@ -428,8 +398,6 @@ Delete a verified SES email address or domain identity | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `emailIdentity` | string | Yes | The email address or domain identity to delete | #### Output @@ -451,8 +419,6 @@ Retrieve verification status, DKIM, Mail-From, and policy details for an SES ide | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `emailIdentity` | string | Yes | The email address or domain identity to look up | #### Output @@ -483,8 +449,6 @@ Create an SES configuration set to control tracking, delivery, reputation, sendi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `configurationSetName` | string | Yes | Name of the configuration set \(letters, numbers, hyphens, underscores\) | | `customRedirectDomain` | string | No | Custom domain to use for open/click tracking links | | `httpsPolicy` | string | No | HTTPS policy for tracking links: REQUIRE, REQUIRE_OPEN_ONLY, or OPTIONAL | @@ -514,8 +478,6 @@ Send a branded custom verification email to an address using a custom verificati | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `emailAddress` | string | Yes | The email address to verify | | `templateName` | string | Yes | The name of the custom verification email template to use | | `configurationSetName` | string | No | Configuration set to use when sending the verification email | diff --git a/apps/docs/content/docs/integrations/sharepoint.mdx b/apps/docs/content/docs/integrations/sharepoint.mdx index a6aa7dd4bad..5d1384a8484 100644 --- a/apps/docs/content/docs/integrations/sharepoint.mdx +++ b/apps/docs/content/docs/integrations/sharepoint.mdx @@ -11,6 +11,8 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} Use [SharePoint](https://www.microsoft.com/en-us/microsoft-365/sharepoint/collaboration) in Sim to list sites, manage pages and lists, update list items, and upload or download files. Connect a Microsoft account through OAuth, then choose the target site or resource for each operation. + +To run as an app registration instead of a signed-in person, use a [Microsoft app credential](/integrations/microsoft-app-credentials). {/* MANUAL-CONTENT-END */} ## OAuth scopes [#oauth-scopes] diff --git a/apps/docs/content/docs/integrations/similarweb.mdx b/apps/docs/content/docs/integrations/similarweb.mdx index cf74d3d35ca..42007a6aa8d 100644 --- a/apps/docs/content/docs/integrations/similarweb.mdx +++ b/apps/docs/content/docs/integrations/similarweb.mdx @@ -2,7 +2,7 @@ title: Similarweb description: Website traffic and analytics data integration: - type: "similarweb" + type: "similarweb_v2" color: "#000922" --- @@ -31,7 +31,6 @@ Get website traffic, rankings, engagement, and traffic sources. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SimilarWeb API key | | `domain` | string | Yes | Website domain to analyze \(e.g., "example.com" without www or protocol\) | #### Output @@ -71,7 +70,6 @@ Get total website visits over time (desktop and mobile combined) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SimilarWeb API key | | `domain` | string | Yes | Website domain to analyze \(e.g., "example.com" without www or protocol\) | | `country` | string | Yes | 2-letter ISO country code \(e.g., "us", "gb", "de"\) or "world" for worldwide data | | `granularity` | string | Yes | Data granularity: daily, weekly, or monthly | @@ -103,7 +101,6 @@ Get website bounce rate over time (desktop and mobile combined) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SimilarWeb API key | | `domain` | string | Yes | Website domain to analyze \(e.g., "example.com" without www or protocol\) | | `country` | string | Yes | 2-letter ISO country code \(e.g., "us", "gb", "de"\) or "world" for worldwide data | | `granularity` | string | Yes | Data granularity: daily, weekly, or monthly | @@ -135,7 +132,6 @@ Get average pages per visit over time (desktop and mobile combined) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SimilarWeb API key | | `domain` | string | Yes | Website domain to analyze \(e.g., "example.com" without www or protocol\) | | `country` | string | Yes | 2-letter ISO country code \(e.g., "us", "gb", "de"\) or "world" for worldwide data | | `granularity` | string | Yes | Data granularity: daily, weekly, or monthly | @@ -167,7 +163,6 @@ Get average desktop visit duration over time (in seconds) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SimilarWeb API key | | `domain` | string | Yes | Website domain to analyze \(e.g., "example.com" without www or protocol\) | | `country` | string | Yes | 2-letter ISO country code \(e.g., "us", "gb", "de"\) or "world" for worldwide data | | `granularity` | string | Yes | Data granularity: daily, weekly, or monthly | @@ -199,7 +194,6 @@ Get total page views over time (desktop and mobile combined) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | SimilarWeb API key | | `domain` | string | Yes | Website domain to analyze \(e.g., "example.com" without www or protocol\) | | `country` | string | Yes | 2-letter ISO country code \(e.g., "us", "gb", "de"\) or "world" for worldwide data | | `granularity` | string | Yes | Data granularity: daily, weekly, or monthly | diff --git a/apps/docs/content/docs/integrations/sixtyfour.mdx b/apps/docs/content/docs/integrations/sixtyfour.mdx index 39dd2922e65..0be83662025 100644 --- a/apps/docs/content/docs/integrations/sixtyfour.mdx +++ b/apps/docs/content/docs/integrations/sixtyfour.mdx @@ -2,7 +2,7 @@ title: Sixtyfour AI description: Enrich leads and companies with AI-powered research integration: - type: "sixtyfour" + type: "sixtyfour_v2" color: "#000000" --- @@ -29,7 +29,6 @@ Find phone numbers for a lead using Sixtyfour AI. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sixtyfour API key | | `name` | string | Yes | Full name of the person | | `company` | string | No | Company name | | `linkedinUrl` | string | No | LinkedIn profile URL | @@ -57,7 +56,6 @@ Find email addresses for a lead using Sixtyfour AI. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sixtyfour API key | | `name` | string | Yes | Full name of the person | | `company` | string | No | Company name | | `linkedinUrl` | string | No | LinkedIn profile URL | @@ -96,7 +94,6 @@ Enrich lead information with contact details, social profiles, and company data | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sixtyfour API key | | `leadInfo` | string | Yes | Lead information as JSON object with key-value pairs \(e.g. name, company, title, linkedin\) | | `struct` | string | Yes | Fields to collect as JSON object. Keys are field names, values are descriptions \(e.g. \{"email": "The individual's email address", "phone": "Phone number"\}\) | | `researchPlan` | string | No | Optional research plan to guide enrichment strategy | @@ -122,7 +119,6 @@ Enrich company data with additional information and find associated people using | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sixtyfour API key | | `targetCompany` | string | Yes | Company data as JSON object \(e.g. \{"name": "Acme Inc", "domain": "acme.com"\}\) | | `struct` | string | Yes | Fields to collect as JSON object. Keys are field names, values are descriptions \(e.g. \{"website": "Company website URL", "num_employees": "Employee count"\}\) | | `findPeople` | boolean | No | Whether to find people associated with the company | diff --git a/apps/docs/content/docs/integrations/smartlead.mdx b/apps/docs/content/docs/integrations/smartlead.mdx index 4489b89fad6..648f73cc62d 100644 --- a/apps/docs/content/docs/integrations/smartlead.mdx +++ b/apps/docs/content/docs/integrations/smartlead.mdx @@ -2,7 +2,7 @@ title: Smartlead description: Manage Smartlead cold email campaigns, sequences, and leads integration: - type: "smartlead" + type: "smartlead_v2" color: "#000000" --- @@ -33,7 +33,6 @@ Retrieves all Smartlead campaigns for the authenticated account. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `clientId` | number | No | Only return campaigns for this client \(agency accounts\) | | `includeTags` | boolean | No | Include campaign tags in the response | @@ -148,7 +147,6 @@ Retrieves a single Smartlead campaign by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | #### Output @@ -262,7 +260,6 @@ Creates a Smartlead campaign. The campaign starts in DRAFTED status; add sequenc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `name` | string | Yes | Campaign name | | `clientId` | number | No | Client to own the campaign \(agency accounts\) | @@ -377,7 +374,6 @@ Starts, pauses, or stops a Smartlead campaign. START requires the campaign to al | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `status` | string | Yes | Target status: START, PAUSED, STOPPED | @@ -492,7 +488,6 @@ Sets the sending window, timezone, and throughput limits for a Smartlead campaig | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `timezone` | string | Yes | IANA timezone for the sending window, e.g. America/Los_Angeles | | `daysOfTheWeek` | array | Yes | Sending days as ISO weekday numbers, where 1 is Monday and 7 is Sunday | @@ -613,7 +608,6 @@ Updates tracking, stop-on-activity, and sending settings for a Smartlead campaig | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `trackSettings` | array | No | Tracking to disable. Allowed values: DONT_TRACK_EMAIL_OPEN, DONT_TRACK_LINK_CLICK, DONT_TRACK_REPLY_TO_AN_EMAIL | | `stopLeadSettings` | string | No | Lead activity that stops the sequence: REPLY_TO_AN_EMAIL, CLICK_ON_A_LINK, OPEN_AN_EMAIL | @@ -733,7 +727,6 @@ Retrieves lifetime performance totals for a Smartlead campaign — sends, opens, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | #### Output @@ -847,7 +840,6 @@ Retrieves Smartlead campaign performance totals for a date range. Smartlead reje | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `startDate` | string | Yes | Range start date in YYYY-MM-DD format | | `endDate` | string | Yes | Range end date in YYYY-MM-DD format | @@ -963,7 +955,6 @@ Retrieves the email sequence steps for a Smartlead campaign, including subjects, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | #### Output @@ -1077,7 +1068,6 @@ Replaces the email sequence for a Smartlead campaign. Send every step in one cal | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `sequences` | array | Yes | Ordered sequence steps. Each entry accepts seq_number, delay_in_days, subject, and email_body \(HTML\). Personalize with \{\{first_name\}\} or \{\{company_name\}\}. | @@ -1192,7 +1182,6 @@ Retrieves per-email statistics rows for a Smartlead campaign, filterable by sequ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `offset` | number | No | Pagination offset \(default 0\) | | `limit` | number | No | Rows to return \(default 100\) | @@ -1312,7 +1301,6 @@ Adds leads to a Smartlead campaign, up to 400 per call. Returns per-reason count | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `leads` | array | Yes | Leads to add \(max 400\). Each entry requires email and accepts first_name, last_name, phone_number, company_name, website, location, linkedin_profile, company_url, and custom_fields. | | `ignoreGlobalBlockList` | boolean | No | Add leads even if they are on the global block list | @@ -1431,7 +1419,6 @@ Retrieves the leads in a Smartlead campaign with their per-campaign status. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `offset` | number | No | Pagination offset \(default 0\) | | `limit` | number | No | Leads to return per page \(default 100\) | @@ -1547,7 +1534,6 @@ Looks up a Smartlead lead by email address and returns the lead record plus ever | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `email` | string | Yes | Lead email address to look up | #### Output @@ -1661,7 +1647,6 @@ Updates a lead in a Smartlead campaign. Smartlead requires the email field even | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id | | `email` | string | Yes | Lead email address — required by Smartlead even when unchanged | @@ -1786,7 +1771,6 @@ Sets the category of a lead in a Smartlead campaign, such as Interested or Not I | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id | | `categoryId` | number | Yes | Lead category ID to apply | @@ -1903,7 +1887,6 @@ Pauses a lead in a Smartlead campaign so it stops receiving sequence emails. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id | @@ -2018,7 +2001,6 @@ Resumes a paused lead in a Smartlead campaign, optionally after a delay. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id | | `resumeLeadWithDelayDays` | number | No | Days to wait before the next email; 0 resumes immediately | @@ -2134,7 +2116,6 @@ Retrieves the lead categories configured on the Smartlead account, with the cate | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | #### Output @@ -2247,7 +2228,6 @@ Retrieves the sent-and-received message history for a lead in a Smartlead campai | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id | @@ -2362,7 +2342,6 @@ Retrieves the webhooks registered on a Smartlead campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | #### Output @@ -2476,7 +2455,6 @@ Creates a webhook on a Smartlead campaign, or updates an existing one when a web | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `name` | string | Yes | Webhook name | | `webhookUrl` | string | Yes | HTTPS URL Smartlead should post events to | @@ -2595,7 +2573,6 @@ Deletes a webhook from a Smartlead campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `webhookId` | number | Yes | ID of the webhook to delete, from List Campaign Webhooks | @@ -2710,7 +2687,6 @@ Retrieves webhook delivery counts for a Smartlead campaign over a time window, f | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `fromTime` | string | Yes | Start of the window as an ISO 8601 timestamp | | `toTime` | string | Yes | End of the window as an ISO 8601 timestamp | @@ -2826,7 +2802,6 @@ Copies a Smartlead campaign, including its sequences and settings. The copy is n | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | #### Output @@ -2940,7 +2915,6 @@ Permanently deletes a Smartlead campaign along with its sequences, leads, and we | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | #### Output @@ -3054,7 +3028,6 @@ Exports every lead in a Smartlead campaign as CSV, including engagement counts a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | #### Output @@ -3168,7 +3141,6 @@ Retrieves the sending email accounts attached to a Smartlead campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | #### Output @@ -3282,7 +3254,6 @@ Attaches sending email accounts to a Smartlead campaign. A campaign needs at lea | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `emailAccountIds` | array | Yes | IDs of the email accounts to attach, from List Email Accounts | @@ -3397,7 +3368,6 @@ Detaches sending email accounts from a Smartlead campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `emailAccountIds` | array | Yes | IDs of the email accounts to detach | @@ -3512,7 +3482,6 @@ Retrieves the sending email accounts on the Smartlead account, including their I | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `offset` | number | No | Pagination offset \(default 0\) | | `limit` | number | No | Accounts to return per page \(default 100\) | | `clientId` | number | No | Only return accounts for this client \(agency accounts\) | @@ -3628,7 +3597,6 @@ Retrieves per-lead engagement statistics for a Smartlead campaign. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `limit` | number | No | Rows to return \(default 100\) | | `offset` | number | No | Rows to skip \(default 0\) | @@ -3744,7 +3712,6 @@ Retrieves per-mailbox sending statistics for a Smartlead campaign, for spotting | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | #### Output @@ -3858,7 +3825,6 @@ Retrieves top-level Smartlead campaign counts for a date range, including positi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `startDate` | string | Yes | Range start date in YYYY-MM-DD format | | `endDate` | string | Yes | Range end date in YYYY-MM-DD format | @@ -3974,7 +3940,6 @@ Retrieves recent lead activity across all Smartlead campaigns — opens, clicks, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `offset` | number | No | Pagination offset \(default 0\) | | `limit` | number | No | Rows to return per page | @@ -4089,7 +4054,6 @@ Looks up a Smartlead lead by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id | #### Output @@ -4203,7 +4167,6 @@ Unsubscribes a lead from a single Smartlead campaign, leaving it active in other | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id | @@ -4318,7 +4281,6 @@ Unsubscribes a lead across every Smartlead campaign and adds it to the account-w | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id | #### Output @@ -4432,7 +4394,6 @@ Marks a lead as completed in a Smartlead campaign so it stops receiving further | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `campaignLeadMapId` | number | Yes | The campaign_lead_map_id from List Campaign Leads — this endpoint takes the map ID, NOT lead.id | @@ -4547,7 +4508,6 @@ Removes a lead from a Smartlead campaign. The lead record itself remains on the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `campaignId` | number | Yes | Smartlead campaign ID | | `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id | @@ -4662,7 +4622,6 @@ Retrieves replies from the Smartlead master inbox across all campaigns, optional | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `unreadOnly` | boolean | No | Return only unread replies | | `offset` | number | No | Pagination offset \(default 0\) | | `limit` | number | No | Replies to return per page | @@ -4778,7 +4737,6 @@ Retrieves the lead lists on the Smartlead account with their lead counts. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `offset` | number | No | Pagination offset \(default 0\) | | `limit` | number | No | Lists to return per page | @@ -4893,7 +4851,6 @@ Retrieves a single Smartlead lead list by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `leadListId` | number | Yes | Lead list ID | #### Output @@ -5007,7 +4964,6 @@ Creates a lead list on the Smartlead account. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `listName` | string | Yes | Name for the new lead list | #### Output @@ -5121,7 +5077,6 @@ Renames a Smartlead lead list. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `leadListId` | number | Yes | Lead list ID to update | | `listName` | string | Yes | New name for the lead list | @@ -5236,7 +5191,6 @@ Deletes a Smartlead lead list. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | | `leadListId` | number | Yes | Lead list ID to delete | #### Output @@ -5350,7 +5304,6 @@ Retrieves the clients on a Smartlead agency account, including the client IDs us | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Smartlead API key | #### Output diff --git a/apps/docs/content/docs/integrations/snowflake-service-account.mdx b/apps/docs/content/docs/integrations/snowflake-service-account.mdx index a32fbee19d8..425bdf98a12 100644 --- a/apps/docs/content/docs/integrations/snowflake-service-account.mdx +++ b/apps/docs/content/docs/integrations/snowflake-service-account.mdx @@ -1,16 +1,64 @@ --- -title: Snowflake programmatic access tokens -description: Create a Snowflake programmatic access token and connect it to Sim so workflows can query your account +title: Snowflake connections +description: Connect Snowflake to Sim with a keyless service user that trusts Sim through OpenID Connect, or with a programmatic access token --- import { Callout } from '@/components/ui/callout' import { Step, Steps } from '@/components/ui/steps' -Connect Snowflake with a programmatic access token (PAT) and account host. The token belongs to one user; use `ROLE_RESTRICTION` to limit it to a role with the access your workflows need. +A Snowflake connection holds your account host and how Sim signs in to it: -Sim stores the token alongside your account host as one credential. Once it is added, every Snowflake block picks it from a dropdown — and the block's database, schema, table, warehouse, role, file-format, and procedure fields become pickers that list what the token can actually see. +| Option | What Sim stores | When to use it | +| --- | --- | --- | +| **Service user (keyless, via Sim OIDC)** | Nothing secret: the account host only | Recommended where available. No token to rotate, expire, or leak. | +| **Programmatic access token** | A programmatic access token (PAT) | Any account, and deployments without keyless support. The token belongs to one user; use `ROLE_RESTRICTION` to limit it to a role with the access your workflows need. | + +Sim stores the connection as one credential. Once it is added, every Snowflake block picks it from a dropdown — and the block's database, schema, table, warehouse, role, file-format, and procedure fields become pickers that list what the token can actually see. + +## Set up a keyless service user [#keyless-service-user] + +Snowflake's [workload identity federation](https://docs.snowflake.com/en/user-guide/workload-identity-federation) lets a service user trust an OpenID Connect issuer instead of holding a password or token. Sim signs a short-lived token for your connection on every run, and Snowflake accepts it only for the service user created with Sim's issuer and this connection's subject. + + + + In Sim, add a **Snowflake** connection, choose **Service user (keyless, via Sim OIDC)**, and enter the account host (`myorg-myaccount.snowflakecomputing.com`). The dialog shows the issuer, audience, and subject for this connection, and the statement to run. + + + A user whose role can create users (for example USERADMIN) runs the statement from the dialog. If SIM_WORKFLOWS already exists, change the name in the statement you run and in the follow-up GRANT and ALTER commands below. It looks like this: + +```sql +CREATE USER SIM_WORKFLOWS + WORKLOAD_IDENTITY = ( + TYPE = OIDC + ISSUER = '' + SUBJECT = '' + ) + TYPE = SERVICE; +``` + + The audience is `snowflakecomputing.com`, Snowflake's default, so the statement needs no `OIDC_AUDIENCE_LIST`. + + + Grant the user a role with the access your workflows need and make it the default, for example `GRANT ROLE SIM_WORKFLOW_ROLE TO USER SIM_WORKFLOWS;` and `ALTER USER SIM_WORKFLOWS SET DEFAULT_ROLE = SIM_WORKFLOW_ROLE;`. If a network policy or authentication policy applies to the user, it must allow Sim's egress and workload identity. + + + Save. Sim signs a token, sends it to your account's SQL API, and runs the same identity statement as for a token. Nothing is saved unless Snowflake accepts it. + + + +Snowflake requires each issuer and subject pair to be unique in an account, and matches the subject exactly, so the service user trusts this connection and no other Sim credential. Reconnecting a keyless connection keeps its subject by default, so the service user keeps working; if you choose **Issue a new subject**, update the user with the new one before saving. + +### From the API or CLI [#keyless-api-cli] + +Run `sim credentials keyless-subject snowflake-service-account`, trust the printed subject as above, then add `host` to its `keylessConfig` and pass it to `sim credentials create snowflake-service-account`. See [the CLI credentials reference](/cli/credentials) for the full two-step example; the proof expires after 24 hours and works only for the user who requested it. + +### Self-hosted deployments + +Keyless connections need `WORKLOAD_IDENTITY_SIGNING_KEY` set and an `https` issuer Snowflake can reach over the public internet. See [AWS credentials](/integrations/aws-credentials#self-hosted-deployments) for the settings. Snowflake caches an issuer's signing keys for up to **60 minutes**, so when rotating the signing key, publish the new key (`WORKLOAD_IDENTITY_PREVIOUS_KEY`) at least 60 minutes before Sim starts signing with it. A shorter wait fails runs until Snowflake refreshes its cache. + +## Use a programmatic access token [#programmatic-access-token] -## Prerequisites +### Prerequisites - A Snowflake user you can generate a token for. For another user or a service user, your role needs `OWNERSHIP` or `MODIFY PROGRAMMATIC AUTHENTICATION METHODS` on that user. - Your account host — the `.snowflakecomputing.com` hostname, for example `myorg-myaccount.snowflakecomputing.com`. Snowsight shows it under **Account details**. @@ -26,9 +74,9 @@ Snowflake's **network policy** requirement varies by user type, and getting it w If your account has no network policy, either create one (allowing Sim's egress) or set `NETWORK_POLICY_EVALUATION = ENFORCED_NOT_REQUIRED` on an authentication policy applied to the user. -## Create the token [#creating-the-token] +### Create the token [#creating-the-token] -### Option 1 — Snowsight +#### Option 1 — Snowsight @@ -45,7 +93,7 @@ If your account has no network policy, either create one (allowing Sim's egress) -### Option 2 — SQL +#### Option 2 — SQL ```sql ALTER USER my_service_user ADD PROGRAMMATIC ACCESS TOKEN sim_workflows @@ -59,11 +107,11 @@ Service users (`TYPE = SERVICE`, `LEGACY_SERVICE`, or `SERVICE_AGENT`) **must** If an authentication policy applies to the user, `'PROGRAMMATIC_ACCESS_TOKEN'` must appear in its `AUTHENTICATION_METHODS` list, otherwise the token is refused. -## Add the credential to Sim [#adding-the-credential-to-sim] +### Add the credential to Sim [#adding-the-credential-to-sim] - Add a **Snowflake** block to a workflow, open the credential dropdown, and choose to add a programmatic access token + Add a **Snowflake** block to a workflow, open the credential dropdown, add a connection, and choose **Programmatic access token** Enter the **account host** (`myorg-myaccount.snowflakecomputing.com`) and paste the **token** @@ -73,7 +121,7 @@ If an authentication policy applies to the user, `'PROGRAMMATIC_ACCESS_TOKEN'` m -The host and the token are encrypted before being stored, and the token is never returned to the browser — the block sends a credential id and Sim resolves it server-side. +The host and the token are encrypted before being stored, and the token is never returned to the browser — the block sends a credential id and Sim resolves it server-side. Keyless tokens are signed on Sim's servers at run time and never stored. ## Use the credential in workflows [#using-the-credential-in-workflows] @@ -91,7 +139,7 @@ With a credential selected, these fields become pickers backed by metadata-only | Named file format | `SHOW FILE FORMATS IN SCHEMA` | database, schema | | Procedure | `SHOW PROCEDURES IN SCHEMA` | database, schema | -Pickers use the token's effective role: its restricted role when set, otherwise the user's default role. They do not apply the execution role selected on the block. An empty list can indicate a privilege gap. Switch any field to advanced mode to type a name directly or reference an upstream block's output instead. +Pickers use the connection's effective role: its restricted role when set, otherwise the user's default role. They do not apply the execution role selected on the block. An empty list can indicate a privilege gap. Switch any field to advanced mode to type a name directly or reference an upstream block's output instead. **Unload Data requires a table or view.** To export a query result, create a view or use `CREATE TABLE AS SELECT` through Execute SQL, then unload that object. @@ -103,6 +151,10 @@ A token's expiry is fixed at creation. To rotate, generate a new token on the sa | Problem | What to check | | --- | --- | +| Keyless: "Snowflake did not accept Sim's token" | No user in this account was created with exactly the issuer and subject from the dialog, the user is not `TYPE = SERVICE`, or a network or authentication policy blocks it. Run `SHOW USER WORKLOAD IDENTITY AUTHENTICATION METHODS FOR USER SIM_WORKFLOWS` to compare. | +| Keyless: error 099706 when creating the user | Another user in the account already has this issuer and subject. Find it with `SHOW USER WORKLOAD IDENTITY AUTHENTICATION METHODS` and remove it, or reconnect with a new subject. | +| Keyless: runs fail for up to an hour after a signing-key rotation | Snowflake had not yet seen the new key. Publish a new key at least 60 minutes before signing with it (see [Self-hosted deployments](#self-hosted-deployments)). | +| Keyless option missing | The deployment has no workload-identity signing key, or the connection belongs to an organization rather than a workspace. Use a programmatic access token. | | Lost token secret | Snowflake shows it only at creation. Generate a replacement and reconnect the credential in Sim. | | Authentication fails | Check expiry or revocation, the user's network policy and Sim's egress, permitted authentication methods, role restriction, and SQL API access. | | Wrong host | Use the account's Snowflake hostname. Sim reports a host error separately when the verification endpoint returns `404`. | diff --git a/apps/docs/content/docs/integrations/snowflake.mdx b/apps/docs/content/docs/integrations/snowflake.mdx index 0883b45f500..dd3a2e53afe 100644 --- a/apps/docs/content/docs/integrations/snowflake.mdx +++ b/apps/docs/content/docs/integrations/snowflake.mdx @@ -23,7 +23,7 @@ Use the block to execute SQL, synchronize rows, move staged data, manage warehou - Snowflake now recommends Cortex Agents for new builds; Cortex Analyst remains available. {/* MANUAL-CONTENT-END */} -Connection guides: [Programmatic access tokens](/integrations/snowflake-service-account). +Connection guides: [Snowflake connections](/integrations/snowflake-service-account). ## Use the block [#usage-instructions] @@ -41,7 +41,7 @@ Execute one parameterized SQL statement through the Snowflake SQL API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -92,7 +92,7 @@ Check a running or completed statement and retrieve exactly one result partition | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `statementHandle` | string | Yes | Statement handle returned by Snowflake | | `partition` | number | No | Zero-based result partition to retrieve; defaults to 0 | | `partitionCount` | number | No | Total number of result partitions, taken from the partitionCount of the first partition. Snowflake omits metadata from every later partition response, so supply this when fetching partition 1 or higher to keep truncated and nextPartition accurate | @@ -137,7 +137,7 @@ Cancel a running Snowflake SQL API statement. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `statementHandle` | string | Yes | Statement handle returned by Snowflake | #### Output @@ -180,7 +180,7 @@ Insert structured JSON rows using bound values. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -229,7 +229,7 @@ Update matching rows with a bound MERGE statement without inserting new rows. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -279,7 +279,7 @@ Update matching rows and insert unmatched rows with a bound MERGE statement. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -329,7 +329,7 @@ Delete rows matching a required set of bound column filters. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -378,7 +378,7 @@ Load files from an existing Snowflake stage with COPY INTO. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -434,7 +434,7 @@ Export a Snowflake table to files in a stage with COPY INTO. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -489,7 +489,7 @@ List the databases the credential can access. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `nameLike` | string | No | Optional SQL LIKE pattern for object names | @@ -535,7 +535,7 @@ List the schemas in a Snowflake database. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -582,7 +582,7 @@ List the tables in a Snowflake schema. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -630,7 +630,7 @@ List warehouses visible to the active Snowflake role. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `maxRows` | number | No | Maximum result rows; defaults to 1000 with a Sim safety limit of 10000 | @@ -676,7 +676,7 @@ Get the full details for a Snowflake virtual warehouse. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouseName` | string | Yes | Warehouse name | @@ -721,7 +721,7 @@ Resume a Snowflake virtual warehouse if it is suspended. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouseName` | string | Yes | Warehouse name | @@ -766,7 +766,7 @@ Suspend a Snowflake virtual warehouse. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouseName` | string | Yes | Warehouse name | @@ -811,7 +811,7 @@ Resize a Snowflake warehouse or change its auto-suspend and auto-resume settings | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouseName` | string | Yes | Warehouse name | @@ -859,7 +859,7 @@ List tasks in a Snowflake schema. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -907,7 +907,7 @@ Describe a Snowflake task. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -954,7 +954,7 @@ Run a Snowflake task immediately, optionally retrying its last failed graph. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -1002,7 +1002,7 @@ Resume a suspended Snowflake task so its schedule runs again. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -1049,7 +1049,7 @@ Suspend a Snowflake task so its schedule stops triggering runs. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -1096,7 +1096,7 @@ Query up to seven days of Snowflake task history, capped at 10000 rows. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1146,7 +1146,7 @@ Find one task history record by query ID within Snowflake’s seven-day window a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1195,7 +1195,7 @@ Cancel one running task query by query ID with SYSTEM$CANCEL_QUERY. Task runs al | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1241,7 +1241,7 @@ Read a task query result with RESULT_SCAN during Snowflake’s 24-hour retention | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1288,7 +1288,7 @@ List queries that completed in the last seven days, optionally filtered. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1339,7 +1339,7 @@ List staged-file load results for a table over the last fourteen days. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1390,7 +1390,7 @@ Inspect table and column metadata through Snowflake INFORMATION_SCHEMA views. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1440,7 +1440,7 @@ Call a stored procedure with explicitly typed Snowflake bindings. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1490,7 +1490,7 @@ Ask Snowflake Cortex Analyst a question in natural language against a semantic v | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `question` | string | Yes | The question to ask about your data | | `semanticView` | string | No | Fully qualified semantic view name \(for example MY_DB.MY_SCHEMA.MY_VIEW\). Provide exactly one semantic source. | | `semanticModelFile` | string | No | Stage path to a semantic model YAML file \(for example @MY_DB.MY_SCHEMA.MY_STAGE/model.yaml\). Provide exactly one semantic source. | @@ -1566,7 +1566,7 @@ Rate a Cortex Analyst answer thumbs up or down, with an optional comment. Feedba | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | +| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token or keyless sign-in\) | | `requestId` | string | Yes | Request ID returned by Cortex Analyst Ask | | `positive` | boolean | Yes | true for positive \(thumbs up\) feedback, false for negative \(thumbs down\) | | `feedbackMessage` | string | No | Optional feedback comment | diff --git a/apps/docs/content/docs/integrations/sportmonks.mdx b/apps/docs/content/docs/integrations/sportmonks.mdx index b1dbdc09be3..ae46b0ad55f 100644 --- a/apps/docs/content/docs/integrations/sportmonks.mdx +++ b/apps/docs/content/docs/integrations/sportmonks.mdx @@ -2,7 +2,7 @@ title: Sportmonks description: Access Sportmonks football, motorsport, odds, and reference data integration: - type: "sportmonks" + type: "sportmonks_v2" color: "#171534" --- @@ -38,7 +38,6 @@ Retrieve lineup-level expected goals (xG) values per player from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;player;team;type\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -71,7 +70,6 @@ Retrieve fixture-level expected goals (xG) values per team from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;participant;type\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -103,7 +101,6 @@ Retrieve all textual commentaries available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;player\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -136,7 +133,6 @@ Retrieve all football fixtures available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -181,7 +177,6 @@ Retrieve all football players available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. nationality;position\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -224,7 +219,6 @@ Retrieve all teams with their rivals information from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team;rival\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -252,7 +246,6 @@ Retrieve all football teams available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -289,7 +282,6 @@ Retrieve all transfer rumours available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -328,7 +320,6 @@ Retrieve all transfers available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | | `filters` | string | No | Filters to apply \(e.g. transferTypes:219,220\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -365,7 +356,6 @@ Retrieve the knockout-stage tournament bracket (stages and progression edges) fo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response | @@ -387,7 +377,6 @@ Retrieve a single football coach by their ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `coachId` | string | Yes | The unique id of the coach | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams;statistics\) | | `filters` | string | No | Filters to apply | @@ -426,7 +415,6 @@ Retrieve all football coaches available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | | `filters` | string | No | Filters to apply \(e.g. coachCountries:462\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -467,7 +455,6 @@ Retrieve all coaches for a country ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `countryId` | string | Yes | The unique id of the country | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;nationality\) | | `filters` | string | No | Filters to apply | @@ -509,7 +496,6 @@ Retrieve textual commentary for a fixture by fixture ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;relatedPlayer\) | @@ -539,7 +525,6 @@ Retrieve all current leagues for a team ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | | `filters` | string | No | Filters to apply | @@ -576,7 +561,6 @@ Retrieve the premium expected lineups for a player ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `playerId` | string | Yes | The unique id of the player | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fixture\) | | `filters` | string | No | Filters to apply | @@ -614,7 +598,6 @@ Retrieve the premium expected lineups for a team ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fixture\) | | `filters` | string | No | Filters to apply | @@ -652,7 +635,6 @@ Retrieve all squad entries for a team (based on current seasons) by team ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;position\) | | `filters` | string | No | Filters to apply | @@ -684,7 +666,6 @@ Retrieve a single football fixture by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events;lineups;statistics\) | | `filters` | string | No | Filters to apply \(e.g. eventTypes:14\) | @@ -727,7 +708,6 @@ Retrieve all football fixtures on a specific date (YYYY-MM-DD) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `date` | string | Yes | The date to fetch fixtures for, in YYYY-MM-DD format | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;league\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501,271\) | @@ -773,7 +753,6 @@ Retrieve football fixtures between two dates (YYYY-MM-DD) from Sportmonks. Max r | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `startDate` | string | Yes | Start date in YYYY-MM-DD format | | `endDate` | string | Yes | End date in YYYY-MM-DD format \(max 100 days after start\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | @@ -820,7 +799,6 @@ Retrieve fixtures for a team within a date range (YYYY-MM-DD) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `startDate` | string | Yes | Start date in YYYY-MM-DD format | | `endDate` | string | Yes | End date in YYYY-MM-DD format | | `teamId` | string | Yes | The unique id of the team | @@ -868,7 +846,6 @@ Retrieve multiple football fixtures by a comma-separated list of IDs (max 50) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `ids` | string | Yes | Comma-separated fixture IDs \(e.g. 18535517,18535518\). Maximum of 50 IDs | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | @@ -911,7 +888,6 @@ Retrieve the standing table for a round ID grouped by group where applicable fro | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `roundId` | string | Yes | The unique id of the round | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | | `filters` | string | No | Filters to apply \(e.g. standingGroups:246697\) | @@ -934,7 +910,6 @@ Retrieve the head-to-head fixtures between two teams from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `team1` | string | Yes | The id of the first team | | `team2` | string | Yes | The id of the second team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | @@ -981,7 +956,6 @@ Retrieve all fixtures that are currently being played (in-play) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | @@ -1023,7 +997,6 @@ Retrieve all coaches that have received updates in the past two hours | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;nationality\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -1064,7 +1037,6 @@ Retrieve all fixtures that have received updates within the last 10 seconds | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | @@ -1106,7 +1078,6 @@ Retrieve all livescores that have received updates within the last 10 seconds | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | @@ -1148,7 +1119,6 @@ Retrieve all players that have received updates in the past two hours | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. nationality;position\) | | `filters` | string | No | Filters to apply | @@ -1188,7 +1158,6 @@ Retrieve the latest Team of the Week (TOTW) for a league ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `leagueId` | string | Yes | The unique id of the league | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;team;player;round\) | @@ -1218,7 +1187,6 @@ Retrieve the latest transfers available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | | `filters` | string | No | Filters to apply \(e.g. transferTypes:219,220\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -1255,7 +1223,6 @@ Retrieve a single football league by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `leagueId` | string | Yes | The unique id of the league | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason;seasons\) | | `filters` | string | No | Filters to apply | @@ -1289,7 +1256,6 @@ Retrieve all football leagues available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -1325,7 +1291,6 @@ Retrieve all leagues for a country ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `countryId` | string | Yes | The unique id of the country | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | | `filters` | string | No | Filters to apply | @@ -1362,7 +1327,6 @@ Retrieve all leagues with fixtures on a given date (YYYY-MM-DD) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `date` | string | Yes | The fixture date in YYYY-MM-DD format | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | | `filters` | string | No | Filters to apply | @@ -1399,7 +1363,6 @@ Retrieve all current and historical leagues for a team ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | | `filters` | string | No | Filters to apply | @@ -1436,7 +1399,6 @@ Retrieve all leagues that have fixtures currently being played from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -1472,7 +1434,6 @@ Retrieve all live (in-play) prediction probabilities from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -1502,7 +1463,6 @@ Retrieve all live (in-play) prediction probabilities for a fixture ID from Sport | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -1533,7 +1493,6 @@ Retrieve the live standing table for a league ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `leagueId` | string | Yes | The unique id of the league | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | | `filters` | string | No | Filters to apply \(e.g. standingGroups:246697\) | @@ -1568,7 +1527,6 @@ Retrieve fixtures starting within 15 minutes and currently in progress from Spor | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores;events\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | @@ -1610,7 +1568,6 @@ Retrieve all available match facts within your Sportmonks subscription (beta) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;sport;fixture\) | | `filters` | string | No | Filters to apply \(e.g. matchFactTypes:76088\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -1645,7 +1602,6 @@ Retrieve match facts within a date range (YYYY-MM-DD) from Sportmonks (beta) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `startDate` | string | Yes | Start date in YYYY-MM-DD format | | `endDate` | string | Yes | End date in YYYY-MM-DD format | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;sport;fixture\) | @@ -1682,7 +1638,6 @@ Retrieve match facts for a fixture ID from Sportmonks (beta) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;sport;fixture\) | | `filters` | string | No | Filters to apply \(e.g. matchFactTypes:76088\) | @@ -1718,7 +1673,6 @@ Retrieve match facts for a league ID from Sportmonks (beta) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `leagueId` | string | Yes | The unique id of the league | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;sport;fixture\) | | `filters` | string | No | Filters to apply \(e.g. matchFactTypes:76088\) | @@ -1754,7 +1708,6 @@ Retrieve all past fixtures that were available for a TV station ID from Sportmon | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `tvStationId` | string | Yes | The unique id of the TV station | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | @@ -1800,7 +1753,6 @@ Retrieve a single football player by their ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `playerId` | string | Yes | The unique id of the player | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;position;teams.team;statistics\) | | `filters` | string | No | Filters to apply | @@ -1841,7 +1793,6 @@ Retrieve all players for a country ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `countryId` | string | Yes | The unique id of the country | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. nationality;position\) | | `filters` | string | No | Filters to apply | @@ -1885,7 +1836,6 @@ Retrieve all post-match news articles available within your Sportmonks subscript | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | | `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -1915,7 +1865,6 @@ Retrieve all post-match news articles for a season ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | | `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | @@ -1946,7 +1895,6 @@ Retrieve the predictions model performance for a league ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `leagueId` | string | Yes | The unique id of the league | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;league\) | | `filters` | string | No | Filters to apply \(e.g. predictabilityTypes:245\) | @@ -1976,7 +1924,6 @@ Retrieve all pre-match news articles available within your Sportmonks subscripti | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | | `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -2006,7 +1953,6 @@ Retrieve all pre-match news articles for a season ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | | `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | @@ -2037,7 +1983,6 @@ Retrieve all pre-match news articles for upcoming fixtures from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;league\) | | `filters` | string | No | Filters to apply \(e.g. newsitemLeagues:8\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -2067,7 +2012,6 @@ Retrieve all prediction probabilities available within your Sportmonks subscript | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | | `filters` | string | No | Filters to apply \(e.g. predictionTypes:236\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -2096,7 +2040,6 @@ Retrieve prediction probabilities for a fixture by fixture ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | | `filters` | string | No | Filters to apply \(e.g. predictionTypes:236\) | @@ -2126,7 +2069,6 @@ Retrieve a single football referee by their ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `refereeId` | string | Yes | The unique id of the referee | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;statistics\) | | `filters` | string | No | Filters to apply | @@ -2164,7 +2106,6 @@ Retrieve all football referees available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;statistics\) | | `filters` | string | No | Filters to apply \(e.g. refereeCountries:44\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -2204,7 +2145,6 @@ Retrieve all referees for a country ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `countryId` | string | Yes | The unique id of the country | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;nationality\) | | `filters` | string | No | Filters to apply | @@ -2245,7 +2185,6 @@ Retrieve all referees for a season ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;nationality\) | | `filters` | string | No | Filters to apply | @@ -2286,7 +2225,6 @@ Retrieve rival teams for a team by team ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team;rival\) | @@ -2311,7 +2249,6 @@ Retrieve a single football round by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `roundId` | string | Yes | The unique id of the round | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;stage;fixtures\) | | `filters` | string | No | Filters to apply | @@ -2345,7 +2282,6 @@ Retrieve all available statistics for a round ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `roundId` | string | Yes | The unique id of the round | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant\) | | `filters` | string | No | Filters to apply \(e.g. seasonstatisticTypes:52,88\) | @@ -2376,7 +2312,6 @@ Retrieve all football rounds available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;stage\) | | `filters` | string | No | Filters to apply \(e.g. roundSeasons:19735\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -2412,7 +2347,6 @@ Retrieve all rounds for a season ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stage\) | | `filters` | string | No | Filters to apply | @@ -2446,7 +2380,6 @@ Retrieve the full schedule (stages, rounds and fixtures) for a season by season | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | ##### Output @@ -2467,7 +2400,6 @@ Retrieve the full season schedule for a specific team by season ID and team ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `teamId` | string | Yes | The unique id of the team | @@ -2489,7 +2421,6 @@ Retrieve the full schedule (stages, rounds and fixtures) for a team by team ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | ##### Output @@ -2510,7 +2441,6 @@ Retrieve a single football season by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages;fixtures\) | | `filters` | string | No | Filters to apply | @@ -2546,7 +2476,6 @@ Retrieve all football seasons available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages\) | | `filters` | string | No | Filters to apply \(e.g. seasonLeagues:501\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -2584,7 +2513,6 @@ Retrieve all seasons for a team ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages\) | | `filters` | string | No | Filters to apply | @@ -2620,7 +2548,6 @@ Retrieve a single football stage by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `stageId` | string | Yes | The unique id of the stage | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;rounds\) | | `filters` | string | No | Filters to apply | @@ -2655,7 +2582,6 @@ Retrieve all available statistics for a stage ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `stageId` | string | Yes | The unique id of the stage | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant\) | | `filters` | string | No | Filters to apply \(e.g. seasonstatisticTypes:52,88\) | @@ -2686,7 +2612,6 @@ Retrieve all football stages available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;rounds\) | | `filters` | string | No | Filters to apply \(e.g. stageSeasons:19735\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -2723,7 +2648,6 @@ Retrieve all stages for a season ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;rounds\) | | `filters` | string | No | Filters to apply | @@ -2758,7 +2682,6 @@ Retrieve point corrections (awarded or deducted) for a season ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;stage\) | | `filters` | string | No | Filters to apply | @@ -2791,7 +2714,6 @@ Retrieve all standings available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;league;season\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -2828,7 +2750,6 @@ Retrieve the full standing table for a round ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `roundId` | string | Yes | The unique id of the round | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | | `filters` | string | No | Filters to apply \(e.g. standingGroups:246697\) | @@ -2863,7 +2784,6 @@ Retrieve the full league standings table for a season by season ID from Sportmon | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details;form\) | | `filters` | string | No | Filters to apply \(e.g. standingStages:77453568\) | @@ -2898,7 +2818,6 @@ Retrieve a single fixture state by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `stateId` | string | Yes | The unique id of the state | ##### Output @@ -2924,7 +2843,6 @@ Retrieve all fixture states (e.g. Not Started, 1st Half, Full Time) from Sportmo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | | `order` | string | No | Order direction \(asc or desc\) | @@ -2952,7 +2870,6 @@ Retrieve a single football team by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue;coaches;players.player\) | | `filters` | string | No | Filters to apply | @@ -2987,7 +2904,6 @@ Retrieve all team rankings available within your Sportmonks subscription (beta) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -3016,7 +2932,6 @@ Retrieve team rankings for a given date (YYYY-MM-DD) from Sportmonks (beta) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `date` | string | Yes | The ranking date in YYYY-MM-DD format | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -3046,7 +2961,6 @@ Retrieve team rankings for a team ID from Sportmonks (beta) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. team\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -3076,7 +2990,6 @@ Retrieve the current domestic squad for a team by team ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;position\) | | `filters` | string | No | Filters to apply | @@ -3108,7 +3021,6 @@ Retrieve the (historical) squad for a team in a specific season from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;position\) | @@ -3141,7 +3053,6 @@ Retrieve all teams for a country ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `countryId` | string | Yes | The unique id of the country | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | | `filters` | string | No | Filters to apply | @@ -3179,7 +3090,6 @@ Retrieve all teams for a season ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | | `filters` | string | No | Filters to apply | @@ -3217,7 +3127,6 @@ Retrieve the topscorers (goals, assists, cards) for a season by season ID from S | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;participant;type\) | | `filters` | string | No | Filters to apply \(e.g. seasontopscorerTypes:208\) | @@ -3252,7 +3161,6 @@ Retrieve topscorers for a stage by stage ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `stageId` | string | Yes | The unique id of the stage | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;participant;type\) | | `filters` | string | No | Filters to apply \(e.g. stageTopscorerTypes:208\) | @@ -3287,7 +3195,6 @@ Retrieve all available Team of the Week (TOTW) entries from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;team;player;round\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -3319,7 +3226,6 @@ Retrieve the Team of the Week (TOTW) for a round ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `roundId` | string | Yes | The unique id of the round | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixture;team;player;round\) | @@ -3349,7 +3255,6 @@ Retrieve a single transfer by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `transferId` | string | Yes | The unique id of the transfer | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | | `filters` | string | No | Filters to apply | @@ -3384,7 +3289,6 @@ Retrieve a single transfer rumour by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `rumourId` | string | Yes | The unique id of the transfer rumour | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | | `filters` | string | No | Filters to apply | @@ -3421,7 +3325,6 @@ Retrieve transfer rumours within a date range (YYYY-MM-DD) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `startDate` | string | Yes | Start date in YYYY-MM-DD format | | `endDate` | string | Yes | End date in YYYY-MM-DD format | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | @@ -3462,7 +3365,6 @@ Retrieve transfer rumours for a player ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `playerId` | string | Yes | The unique id of the player | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | | `filters` | string | No | Filters to apply | @@ -3502,7 +3404,6 @@ Retrieve transfer rumours for a team ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | | `filters` | string | No | Filters to apply | @@ -3542,7 +3443,6 @@ Retrieve transfers within a date range (YYYY-MM-DD) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `startDate` | string | Yes | Start date in YYYY-MM-DD format | | `endDate` | string | Yes | End date in YYYY-MM-DD format | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | @@ -3581,7 +3481,6 @@ Retrieve transfers for a player by player ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `playerId` | string | Yes | The unique id of the player | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fromTeam;toTeam;type\) | | `filters` | string | No | Filters to apply | @@ -3619,7 +3518,6 @@ Retrieve transfers for a team by team ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. player;fromTeam;toTeam\) | | `filters` | string | No | Filters to apply \(e.g. transferTypes:219,220\) | @@ -3657,7 +3555,6 @@ Retrieve a single TV station by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `tvStationId` | string | Yes | The unique id of the TV station | | `include` | string | No | Semicolon-separated relations to enrich the response | | `filters` | string | No | Filters to apply | @@ -3686,7 +3583,6 @@ Retrieve all TV stations available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -3717,7 +3613,6 @@ Retrieve broadcasting TV stations for a fixture by fixture ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. fixtures;countries\) | | `filters` | string | No | Filters to apply | @@ -3749,7 +3644,6 @@ Retrieve all upcoming fixtures for a market ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `marketId` | string | Yes | The unique id of the market | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;odds\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | @@ -3795,7 +3689,6 @@ Retrieve all upcoming fixtures available for a TV station ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `tvStationId` | string | Yes | The unique id of the TV station | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | @@ -3841,7 +3734,6 @@ Retrieve all value bets available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -3869,7 +3761,6 @@ Retrieve value bet predictions for a fixture by fixture ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. type;fixture\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -3898,7 +3789,6 @@ Retrieve a single football venue by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `venueId` | string | Yes | The unique id of the venue | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city;fixtures\) | | `filters` | string | No | Filters to apply | @@ -3934,7 +3824,6 @@ Retrieve all football venues available within your Sportmonks subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | | `filters` | string | No | Filters to apply \(e.g. venueCountries:98\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -3972,7 +3861,6 @@ Retrieve all venues for a season ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | | `filters` | string | No | Filters to apply | @@ -4008,7 +3896,6 @@ Search for football coaches by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The coach name to search for \(e.g. Gerrard\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | | `filters` | string | No | Filters to apply | @@ -4050,7 +3937,6 @@ Search for football fixtures by name (participants) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The fixture name to search for \(e.g. Celtic\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;scores\) | | `filters` | string | No | Filters to apply \(e.g. fixtureLeagues:501\) | @@ -4096,7 +3982,6 @@ Search for football leagues by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The league name to search for \(e.g. Premier\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;currentSeason\) | | `filters` | string | No | Filters to apply | @@ -4133,7 +4018,6 @@ Search for football players by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The player name to search for \(e.g. Tavernier\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;position;teams.team\) | | `filters` | string | No | Filters to apply | @@ -4177,7 +4061,6 @@ Search for football referees by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The referee name to search for | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country\) | | `filters` | string | No | Filters to apply | @@ -4218,7 +4101,6 @@ Search for football rounds by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The round name to search for \(e.g. 5\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season\) | | `filters` | string | No | Filters to apply | @@ -4255,7 +4137,6 @@ Search for football seasons by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The season name to search for \(e.g. 2023/2024\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league\) | | `filters` | string | No | Filters to apply | @@ -4294,7 +4175,6 @@ Search for football stages by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The stage name to search for \(e.g. Group\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season\) | | `filters` | string | No | Filters to apply | @@ -4332,7 +4212,6 @@ Search for football teams by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The team name to search for \(e.g. Celtic\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;venue\) | | `filters` | string | No | Filters to apply | @@ -4370,7 +4249,6 @@ Search for football venues by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The venue name to search for \(e.g. Celtic Park\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | | `filters` | string | No | Filters to apply | @@ -4411,7 +4289,6 @@ Retrieve all motorsport fixtures (sessions) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -4456,7 +4333,6 @@ Retrieve the current motorsport leagues for a team by team ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team \(constructor\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | | `filters` | string | No | Filters to apply | @@ -4494,7 +4370,6 @@ Retrieve a single motorsport driver by their ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `driverId` | string | Yes | The unique id of the driver | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | | `filters` | string | No | Filters to apply | @@ -4535,7 +4410,6 @@ Retrieve all driver championship standings from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -4572,7 +4446,6 @@ Retrieve the drivers championship standings for a season by season ID from Sport | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | | `filters` | string | No | Filters to apply | @@ -4610,7 +4483,6 @@ Retrieve all motorsport drivers from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -4653,7 +4525,6 @@ Retrieve all motorsport drivers for a country by country ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `countryId` | string | Yes | The unique id of the country | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | | `filters` | string | No | Filters to apply | @@ -4697,7 +4568,6 @@ Retrieve all motorsport drivers for a season by season ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | | `filters` | string | No | Filters to apply | @@ -4741,7 +4611,6 @@ Retrieve a single motorsport fixture (session) by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results;latestLaps;pitstops\) | | `filters` | string | No | Filters to apply | @@ -4784,7 +4653,6 @@ Retrieve motorsport fixtures (sessions) on a specific date (YYYY-MM-DD) from Spo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `date` | string | Yes | The date to fetch fixtures for, in YYYY-MM-DD format | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;venue\) | | `filters` | string | No | Filters to apply | @@ -4830,7 +4698,6 @@ Retrieve motorsport fixtures (sessions) between two dates (YYYY-MM-DD, max 100 d | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `startDate` | string | Yes | The start date of the range, in YYYY-MM-DD format | | `endDate` | string | Yes | The end date of the range, in YYYY-MM-DD format | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;venue\) | @@ -4877,7 +4744,6 @@ Retrieve multiple motorsport fixtures (sessions) by their IDs (max 50) from Spor | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureIds` | string | Yes | Comma-separated list of fixture ids \(max 50, e.g. 19408487,19408480\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | | `filters` | string | No | Filters to apply | @@ -4923,7 +4789,6 @@ Retrieve all laps for a motorsport fixture (session) by fixture ID from Sportmon | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | | `filters` | string | No | Filters to apply | @@ -4952,7 +4817,6 @@ Retrieve all laps for a motorsport fixture and driver from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `driverId` | string | Yes | The unique id of the driver | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | @@ -4982,7 +4846,6 @@ Retrieve all laps for a motorsport fixture and lap number from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `lapNumber` | string | Yes | The lap number to retrieve | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | @@ -5012,7 +4875,6 @@ Retrieve the latest laps for a motorsport fixture (session) by fixture ID from S | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | | `filters` | string | No | Filters to apply | @@ -5041,7 +4903,6 @@ Retrieve the latest pitstops for a motorsport fixture (session) by fixture ID fr | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | | `filters` | string | No | Filters to apply | @@ -5070,7 +4931,6 @@ Retrieve the latest tyre stints for a motorsport fixture (session) by fixture ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | | `filters` | string | No | Filters to apply | @@ -5099,7 +4959,6 @@ Retrieve the most recently updated motorsport drivers from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -5142,7 +5001,6 @@ Retrieve the most recently updated motorsport fixtures (sessions) from Sportmonk | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -5187,7 +5045,6 @@ Retrieve a single motorsport league by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `leagueId` | string | Yes | The unique id of the league | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | | `filters` | string | No | Filters to apply | @@ -5222,7 +5079,6 @@ Retrieve all motorsport leagues from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -5259,7 +5115,6 @@ Retrieve all motorsport leagues for a country by country ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `countryId` | string | Yes | The unique id of the country | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | | `filters` | string | No | Filters to apply | @@ -5297,7 +5152,6 @@ Retrieve all motorsport leagues with fixtures on a specific date (YYYY-MM-DD) fr | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `date` | string | Yes | The date to fetch leagues for, in YYYY-MM-DD format | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | | `filters` | string | No | Filters to apply | @@ -5335,7 +5189,6 @@ Retrieve all motorsport leagues that currently have live fixtures from Sportmonk | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -5372,7 +5225,6 @@ Retrieve all current and historical motorsport leagues for a team by team ID fro | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team \(constructor\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | | `filters` | string | No | Filters to apply | @@ -5410,7 +5262,6 @@ Retrieve all live motorsport fixtures (sessions) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participants;results\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -5455,7 +5306,6 @@ Retrieve all pitstops for a motorsport fixture (session) by fixture ID from Spor | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | | `filters` | string | No | Filters to apply | @@ -5484,7 +5334,6 @@ Retrieve all pitstops for a motorsport fixture and driver from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `driverId` | string | Yes | The unique id of the driver | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | @@ -5514,7 +5363,6 @@ Retrieve all pitstops for a motorsport fixture and lap number from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `lapNumber` | string | Yes | The lap number to retrieve | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | @@ -5544,7 +5392,6 @@ Retrieve race results (stages with fixtures, lineups and lineup details) for a s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `driverId` | string | Yes | The unique id of the driver | | `include` | string | No | Semicolon-separated relations to enrich the response | @@ -5584,7 +5431,6 @@ Retrieve race results (stages with fixtures, lineups and lineup details) for a s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `teamId` | string | Yes | The unique id of the team \(constructor\) | | `include` | string | No | Semicolon-separated relations to enrich the response | @@ -5624,7 +5470,6 @@ Retrieve the full schedule (stages with nested fixtures and venues) for a season | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response | | `filters` | string | No | Filters to apply | @@ -5663,7 +5508,6 @@ Retrieve a single motorsport season by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages\) | | `filters` | string | No | Filters to apply | @@ -5698,7 +5542,6 @@ Retrieve all motorsport seasons from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;stages\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -5735,7 +5578,6 @@ Retrieve a single motorsport stage (race weekend) by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `stageId` | string | Yes | The unique id of the stage \(race weekend\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;fixtures\) | | `filters` | string | No | Filters to apply | @@ -5771,7 +5613,6 @@ Retrieve all motorsport stages (race weekends) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;fixtures\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -5809,7 +5650,6 @@ Retrieve all motorsport stages (race weekends) for a season by season ID from Sp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;fixtures\) | | `filters` | string | No | Filters to apply | @@ -5848,7 +5688,6 @@ Retrieve a single motorsport fixture state by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `stateId` | string | Yes | The unique id of the state | | `include` | string | No | Semicolon-separated relations to enrich the response | | `filters` | string | No | Filters to apply | @@ -5876,7 +5715,6 @@ Retrieve all possible motorsport fixture states from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -5906,7 +5744,6 @@ Retrieve all tyre stints for a motorsport fixture (session) by fixture ID from S | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | | `filters` | string | No | Filters to apply | @@ -5935,7 +5772,6 @@ Retrieve all tyre stints for a motorsport fixture and driver from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `driverId` | string | Yes | The unique id of the driver | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | @@ -5965,7 +5801,6 @@ Retrieve all tyre stints for a motorsport fixture and stint number from Sportmon | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture \(session\) | | `stintNumber` | string | Yes | The stint number to retrieve | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;details\) | @@ -5995,7 +5830,6 @@ Retrieve a single motorsport team (constructor) by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `teamId` | string | Yes | The unique id of the team \(constructor\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | | `filters` | string | No | Filters to apply | @@ -6030,7 +5864,6 @@ Retrieve all team (constructor) championship standings from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -6067,7 +5900,6 @@ Retrieve the constructors championship standings for a season by season ID from | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. participant;season\) | | `filters` | string | No | Filters to apply | @@ -6105,7 +5937,6 @@ Retrieve all motorsport teams (constructors) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -6142,7 +5973,6 @@ Retrieve all motorsport teams (constructors) for a country by country ID from Sp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `countryId` | string | Yes | The unique id of the country | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | | `filters` | string | No | Filters to apply | @@ -6180,7 +6010,6 @@ Retrieve all motorsport teams (constructors) for a season by season ID from Spor | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | | `filters` | string | No | Filters to apply | @@ -6218,7 +6047,6 @@ Retrieve a single motorsport venue (racing track) by its ID from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `venueId` | string | Yes | The unique id of the venue \(track\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | | `filters` | string | No | Filters to apply | @@ -6254,7 +6082,6 @@ Retrieve all motorsport venues (racing tracks) from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -6292,7 +6119,6 @@ Retrieve all motorsport venues (racing tracks) for a season by season ID from Sp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `seasonId` | string | Yes | The unique id of the season | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | | `filters` | string | No | Filters to apply | @@ -6331,7 +6157,6 @@ Search for motorsport drivers by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The driver name to search for \(e.g. Verstappen\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;teams\) | | `filters` | string | No | Filters to apply | @@ -6375,7 +6200,6 @@ Search for motorsport leagues by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The league name to search for \(e.g. Formula\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;seasons\) | | `filters` | string | No | Filters to apply | @@ -6413,7 +6237,6 @@ Search for motorsport stages (race weekends) by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The stage name to search for \(e.g. Monaco\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. league;season;fixtures\) | | `filters` | string | No | Filters to apply | @@ -6452,7 +6275,6 @@ Search for motorsport teams (constructors) by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The team name to search for \(e.g. Bull\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;drivers\) | | `filters` | string | No | Filters to apply | @@ -6490,7 +6312,6 @@ Search for motorsport venues (racing tracks) by name from Sportmonks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The venue name to search for \(e.g. Hungaroring\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;city\) | | `filters` | string | No | Filters to apply | @@ -6531,7 +6352,6 @@ Retrieve all available historical (premium) pre-match odd values from the Sportm | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. odd\) | | `filters` | string | No | Filters to apply \(e.g. winningOdds\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -6564,7 +6384,6 @@ Retrieve all available live (in-play) odds from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | | `filters` | string | No | Filters to apply \(e.g. markets:1,12, bookmakers:2,14, IdAfter:oddID\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -6609,7 +6428,6 @@ Retrieve all available pre-match odds from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | | `filters` | string | No | Filters to apply \(e.g. markets:1,12, bookmakers:2,14, winningOdds, IdAfter:oddID\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -6653,7 +6471,6 @@ Retrieve all available premium (historical) pre-match odds from the Sportmonks O | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | | `filters` | string | No | Filters to apply \(e.g. markets:1,12, bookmakers:2,14, IdAfter:oddID\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -6697,7 +6514,6 @@ Retrieve a single bookmaker by its ID from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `bookmakerId` | string | Yes | The unique id of the bookmaker | ##### Output @@ -6721,7 +6537,6 @@ Retrieve bookmakers' own event ids mapped to a Sportmonks fixture via the Sportm | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -6749,7 +6564,6 @@ Retrieve all bookmakers from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `filters` | string | No | Filters to apply \(e.g. IdAfter:bookmakerID\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -6776,7 +6590,6 @@ Retrieve all bookmakers available for a fixture from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -6803,7 +6616,6 @@ Retrieve live (in-play) odds for a fixture by fixture ID from the Sportmonks Odd | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | | `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | @@ -6849,7 +6661,6 @@ Retrieve live (in-play) odds for a fixture from a specific bookmaker via the Spo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `bookmakerId` | string | Yes | The unique id of the bookmaker | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | @@ -6893,7 +6704,6 @@ Retrieve live (in-play) odds for a fixture on a specific market via the Sportmon | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `marketId` | string | Yes | The unique id of the market | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | @@ -6937,7 +6747,6 @@ Retrieve in-play odds updated in the last 10 seconds from the Sportmonks Odds AP | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | | `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14\) | @@ -6979,7 +6788,6 @@ Retrieve pre-match odds updated in the last 10 seconds from the Sportmonks Odds | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | | `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | @@ -7020,7 +6828,6 @@ Retrieve a single betting market by its ID from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `marketId` | string | Yes | The unique id of the market | ##### Output @@ -7044,7 +6851,6 @@ Retrieve all betting markets from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `filters` | string | No | Filters to apply \(e.g. IdAfter:marketID\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -7071,7 +6877,6 @@ Retrieve pre-match odds for a fixture by fixture ID from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | | `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14 or winningOdds\) | @@ -7116,7 +6921,6 @@ Retrieve pre-match odds for a fixture from a specific bookmaker via the Sportmon | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `bookmakerId` | string | Yes | The unique id of the bookmaker | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | @@ -7159,7 +6963,6 @@ Retrieve pre-match odds for a fixture on a specific market via the Sportmonks Od | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `marketId` | string | Yes | The unique id of the market | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | @@ -7202,7 +7005,6 @@ Retrieve premium (historical) pre-match odds for a fixture from the Sportmonks O | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | | `filters` | string | No | Filters to apply \(e.g. markets:1,12 or bookmakers:2,14\) | @@ -7244,7 +7046,6 @@ Retrieve premium pre-match odds for a fixture from a specific bookmaker via the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `bookmakerId` | string | Yes | The unique id of the bookmaker | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | @@ -7287,7 +7088,6 @@ Retrieve premium pre-match odds for a fixture on a specific market via the Sport | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fixtureId` | string | Yes | The unique id of the fixture | | `marketId` | string | Yes | The unique id of the market | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | @@ -7330,7 +7130,6 @@ Retrieve historical (premium) odds updated between two UNIX timestamps (max 5 mi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fromTimestamp` | string | Yes | Start of the range as a UNIX timestamp \(e.g. 1767225600\) | | `toTimestamp` | string | Yes | End of the range as a UNIX timestamp \(max 5 minutes after the start\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. odd\) | @@ -7365,7 +7164,6 @@ Retrieve premium odds updated between two UNIX timestamps (max 5 minutes) from t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `fromTimestamp` | string | Yes | Start of the range as a UNIX timestamp \(e.g. 1767225600\) | | `toTimestamp` | string | Yes | End of the range as a UNIX timestamp \(max 5 minutes after the start\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. market;bookmaker\) | @@ -7411,7 +7209,6 @@ Search for bookmakers by name from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The bookmaker name to search for \(e.g. bet365\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -7438,7 +7235,6 @@ Search for betting markets by name from the Sportmonks Odds API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The market name to search for \(e.g. Over/Under\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -7467,7 +7263,6 @@ Retrieve all cities from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -7499,7 +7294,6 @@ Retrieve a single city by its ID from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `cityId` | string | Yes | The unique id of the city | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | @@ -7528,7 +7322,6 @@ Retrieve a single continent by its ID from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `continentId` | string | Yes | The unique id of the continent | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. countries\) | @@ -7553,7 +7346,6 @@ Retrieve all continents from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. countries\) | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | @@ -7580,7 +7372,6 @@ Retrieve all countries from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. continent;regions\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -7617,7 +7408,6 @@ Retrieve a single country by its ID from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `countryId` | string | Yes | The unique id of the country | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. continent;regions\) | @@ -7651,7 +7441,6 @@ Retrieve all available filters grouped per entity from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | ##### Output @@ -7671,7 +7460,6 @@ Retrieve your Sportmonks API usage aggregated per 5 minutes | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | | `order` | string | No | Order direction \(asc or desc\) | @@ -7701,7 +7489,6 @@ Retrieve a single region by its ID from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `regionId` | string | Yes | The unique id of the region | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;cities\) | @@ -7726,7 +7513,6 @@ Retrieve all regions from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;cities\) | | `filters` | string | No | Filters to apply | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | @@ -7754,7 +7540,6 @@ Retrieve all supported time zones (IANA names) from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | ##### Output @@ -7774,7 +7559,6 @@ Retrieve a single type by its ID from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `typeId` | string | Yes | The unique id of the type | ##### Output @@ -7802,7 +7586,6 @@ Retrieve the available types grouped per entity from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | ##### Output @@ -7822,7 +7605,6 @@ Retrieve all types (reference data describing events, statistics, positions, etc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `per_page` | string | No | Number of results per page \(max 50, default 25\) | | `page` | string | No | Page number to retrieve | | `order` | string | No | Order direction \(asc or desc\) | @@ -7852,7 +7634,6 @@ Search for cities by name from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The city name to search for \(e.g. London\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. region\) | | `filters` | string | No | Filters to apply | @@ -7885,7 +7666,6 @@ Search for countries by name from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The country name to search for \(e.g. Brazil\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. continent;regions\) | | `filters` | string | No | Filters to apply | @@ -7923,7 +7703,6 @@ Search for regions by name from the Sportmonks Core API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Sportmonks API token | | `query` | string | Yes | The region name to search for \(e.g. Utrecht\) | | `include` | string | No | Semicolon-separated relations to enrich the response \(e.g. country;cities\) | | `filters` | string | No | Filters to apply | diff --git a/apps/docs/content/docs/integrations/sqs.mdx b/apps/docs/content/docs/integrations/sqs.mdx index 020a3f757bb..3088d1f1c00 100644 --- a/apps/docs/content/docs/integrations/sqs.mdx +++ b/apps/docs/content/docs/integrations/sqs.mdx @@ -2,7 +2,7 @@ title: Amazon SQS description: Connect to Amazon SQS integration: - type: "sqs" + type: "sqs_v2" color: "linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Amazon SQS](https://aws.amazon.com/sqs/) to send, receive, and delete messages, extend visibility timeouts, and manage queues, attributes, and tags. Batch message actions accept up to 10 entries. Dead-letter actions list source queues and start, inspect, or cancel message move tasks. +Use [Amazon SQS](https://aws.amazon.com/sqs/) to send, receive, and delete messages, extend visibility timeouts, and manage queues, attributes, and tags. Batch message actions accept up to 10 entries. Dead-letter actions list source queues and start, inspect, or cancel message move tasks. Connect with a saved [AWS credential](/integrations/aws-credentials). A workflow can long-poll for work, process each message, and delete it after success. Receiving a message does not remove it: if processing fails and it is not deleted, it becomes available again after the visibility timeout. {/* MANUAL-CONTENT-END */} @@ -32,8 +32,6 @@ Send a message to an Amazon SQS queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `data` | json | Yes | Message body to send as JSON object \(e.g., \{ "action": "process", "payload": \{...\} \}\) | | `delaySeconds` | number | No | Seconds to delay delivery of this message, 0-900. Not supported per-message on FIFO queues | @@ -64,8 +62,6 @@ Send up to 10 messages to an Amazon SQS queue in a single request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `entries` | array | Yes | Up to 10 entries, each \{ "id": "unique-id", "data": \{ ... \}, "delaySeconds"?, "messageGroupId"?, "messageDeduplicationId"?, "messageAttributes"? \} | @@ -101,8 +97,6 @@ Receive up to 10 messages from an Amazon SQS queue, with optional long polling | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `maxNumberOfMessages` | number | No | Maximum number of messages to return, 1-10 \(default 1\) | | `waitTimeSeconds` | number | No | Long-poll duration in seconds, 0-20. Waits for a message to arrive before returning \(default 0, short poll\) | @@ -138,8 +132,6 @@ Delete a received message from an Amazon SQS queue using its receipt handle | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `receiptHandle` | string | Yes | Receipt handle returned by sqs_receive_message for the message to delete | @@ -162,8 +154,6 @@ Delete up to 10 received messages from an Amazon SQS queue in a single request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `entries` | array | Yes | Up to 10 entries, each \{ "id": "unique-id", "receiptHandle": "..." \}. Receipt handles come from sqs_receive_message | @@ -195,8 +185,6 @@ Change how long a received Amazon SQS message stays hidden from other consumers | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `receiptHandle` | string | Yes | Receipt handle returned by sqs_receive_message for the message to update | | `visibilityTimeout` | number | Yes | New visibility timeout in seconds, 0-43200 \(12 hours\). 0 makes the message immediately visible again | @@ -220,8 +208,6 @@ Change the visibility timeout of up to 10 received Amazon SQS messages at once | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `entries` | array | Yes | Up to 10 entries, each \{ "id": "unique-id", "receiptHandle": "...", "visibilityTimeout": 0-43200 \}. Receipt handles come from sqs_receive_message | @@ -253,8 +239,6 @@ List Amazon SQS queue URLs in a region, optionally filtered by name prefix | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueNamePrefix` | string | No | Return only queues whose name begins with this string \(case-sensitive\) | | `maxResults` | number | No | Maximum queues to return, 1-1000. Must be set to receive a nextToken \(default returns up to 1000\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -280,8 +264,6 @@ Resolve an Amazon SQS queue name to its queue URL | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueName` | string | Yes | Queue name, up to 80 characters of letters, digits, hyphens and underscores. A FIFO queue name ends in .fifo | | `queueOwnerAwsAccountId` | string | No | 12-digit AWS account ID of the queue owner, when the queue belongs to another account | @@ -304,8 +286,6 @@ Read configuration and message-count attributes of an Amazon SQS queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `attributeNames` | array | No | Attributes to return, e.g. \["All"\], \["ApproximateNumberOfMessages"\], \["QueueArn"\], \["VisibilityTimeout"\], \["RedrivePolicy"\]. Omitting this returns no attributes | @@ -328,8 +308,6 @@ Update configuration attributes of an existing Amazon SQS queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `attributes` | json | Yes | Attributes to set as string values, e.g. \{ "VisibilityTimeout": "60", "MessageRetentionPeriod": "345600", "RedrivePolicy": "\{...\}" \}. FifoQueue can only be set at creation | @@ -352,8 +330,6 @@ Create a standard or FIFO Amazon SQS queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueName` | string | Yes | Queue name, up to 80 characters of letters, digits, hyphens and underscores. A FIFO queue name must end in .fifo | | `attributes` | json | No | Queue attributes as string values, e.g. \{ "FifoQueue": "true", "VisibilityTimeout": "30", "DelaySeconds": "0", "RedrivePolicy": "\{...\}" \} | | `tags` | json | No | Cost-allocation tags to apply to the new queue, as \{ "key": "value" \} pairs | @@ -378,8 +354,6 @@ Delete an Amazon SQS queue and every message still in it | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | #### Output @@ -401,8 +375,6 @@ Delete every message in an Amazon SQS queue while keeping the queue itself | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | #### Output @@ -424,8 +396,6 @@ List the Amazon SQS queues that use a given queue as their dead-letter queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | URL of the dead-letter queue whose source queues should be listed | | `maxResults` | number | No | Maximum source queues to return, 1-1000. Must be set to receive a nextToken \(default returns up to 1000\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -451,8 +421,6 @@ List the cost-allocation tags attached to an Amazon SQS queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | #### Output @@ -474,8 +442,6 @@ Add or overwrite cost-allocation tags on an Amazon SQS queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `tags` | json | Yes | Tags to apply as \{ "key": "value" \} pairs. An existing key is overwritten. AWS recommends no more than 50 tags per queue | @@ -498,8 +464,6 @@ Remove cost-allocation tags from an Amazon SQS queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `queueUrl` | string | Yes | SQS queue URL \(e.g., https://sqs.us-east-1.amazonaws.com/123456789012/my-queue) | | `tagKeys` | array | Yes | Tag keys to remove, e.g. \["env", "team"\] | @@ -522,8 +486,6 @@ Start redriving messages out of an Amazon SQS dead-letter queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `sourceArn` | string | Yes | ARN of the dead-letter queue to move messages out of \(e.g., arn:aws:sqs:us-east-1:123456789012:my-dlq\) | | `destinationArn` | string | No | ARN of the queue to move messages into. Omit to redrive each message to its original source queue | | `maxNumberOfMessagesPerSecond` | number | No | Throttle the move to at most this many messages per second, up to 500. Omit to move as fast as possible | @@ -548,8 +510,6 @@ List the most recent message move tasks for an Amazon SQS source queue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `sourceArn` | string | Yes | ARN of the queue whose move tasks should be listed \(e.g., arn:aws:sqs:us-east-1:123456789012:my-dlq\) | | `maxResults` | number | No | Maximum move tasks to return, 1-10 \(default 1\) | @@ -582,8 +542,6 @@ Cancel an in-progress Amazon SQS message move task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `taskHandle` | string | Yes | Task handle returned by sqs_start_message_move_task or sqs_list_message_move_tasks. Only a RUNNING task can be cancelled | #### Output diff --git a/apps/docs/content/docs/integrations/square.mdx b/apps/docs/content/docs/integrations/square.mdx index e4a3ca773cd..8c74f46f439 100644 --- a/apps/docs/content/docs/integrations/square.mdx +++ b/apps/docs/content/docs/integrations/square.mdx @@ -2,7 +2,7 @@ title: Square description: Process payments and manage Square commerce data integration: - type: "square" + type: "square_v2" color: "#000000" --- @@ -12,12 +12,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} Use [Square](https://squareup.com/) to take, retrieve, list, cancel, complete, or refund payments; manage customer profiles; create, retrieve, search, or pay for orders; and create, publish, cancel, or delete invoices. -The actions also manage catalog objects and images, read inventory counts, and look up locations. Configure a Square access token and the target environment. Payment and refund actions change live commerce records; use the correct environment and the action's idempotency fields before running them. +The actions also manage catalog objects and images, read inventory counts, and look up locations. Connect with a saved Square credential holding a production access token. Payment and refund actions change live commerce records; use the action's idempotency fields before running them. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Take and refund payments, manage customers, build catalog items and images, create and search orders, and issue invoices. Authenticate with a Square access token (personal access token). +Take and refund payments, manage customers, build catalog items and images, create and search orders, and issue invoices. ## Actions @@ -31,7 +31,6 @@ Take a payment using a payment source such as a card nonce or a card on file | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `sourceId` | string | Yes | ID of the payment source \(card nonce, card-on-file ID, or wallet token\) | | `amount` | number | Yes | Amount in the smallest currency denomination \(e.g. 1000 = $10.00\) | | `currency` | string | Yes | Three-letter ISO 4217 currency code \(e.g. USD\) | @@ -99,7 +98,6 @@ Retrieve details for a single payment by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `paymentId` | string | Yes | ID of the payment to retrieve | #### Output @@ -158,7 +156,6 @@ List payments taken by the account, optionally filtered by location and time ran | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `locationId` | string | No | Filter payments by location ID | | `beginTime` | string | No | RFC 3339 timestamp for the beginning of the reporting period | | `endTime` | string | No | RFC 3339 timestamp for the end of the reporting period | @@ -220,7 +217,6 @@ Cancel (void) an authorized payment that has not been captured | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `paymentId` | string | Yes | ID of the payment to cancel | #### Output @@ -279,7 +275,6 @@ Capture (complete) a payment that was authorized with delayed capture | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `paymentId` | string | Yes | ID of the payment to complete | | `versionToken` | string | No | Optional version token for optimistic concurrency control | @@ -339,7 +334,6 @@ Refund all or part of a completed payment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `paymentId` | string | Yes | ID of the payment to refund | | `amount` | number | Yes | Amount to refund in the smallest currency denomination \(e.g. 100 = $1.00\) | | `currency` | string | Yes | Three-letter ISO 4217 currency code \(e.g. USD\) | @@ -380,7 +374,6 @@ Retrieve a single payment refund by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `refundId` | string | Yes | ID of the refund to retrieve | #### Output @@ -417,7 +410,6 @@ List payment refunds, optionally filtered by location, status, and time range | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `locationId` | string | No | Filter refunds by location ID | | `status` | string | No | Filter by refund status \(PENDING, COMPLETED, REJECTED, or FAILED\) | | `beginTime` | string | No | RFC 3339 timestamp for the beginning of the reporting period | @@ -458,7 +450,6 @@ Create a new customer profile in the Square customer directory | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `givenName` | string | No | First name of the customer | | `familyName` | string | No | Last name of the customer | | `companyName` | string | No | Business name of the customer | @@ -522,7 +513,6 @@ Retrieve a single customer profile by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `customerId` | string | Yes | ID of the customer to retrieve | #### Output @@ -576,7 +566,6 @@ List customer profiles in the Square customer directory | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `limit` | number | No | Maximum number of results to return per page \(max 100\) | | `cursor` | string | No | Pagination cursor from a previous response | | `sortField` | string | No | Field to sort by \(DEFAULT or CREATED_AT\) | @@ -631,7 +620,6 @@ Search customer profiles using filters such as email, phone, or creation date | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `query` | json | No | Square customer query object with optional filter and sort \(e.g. \{"filter":\{"email_address":\{"exact":"a@b.com"\}\}\}\) | | `limit` | number | No | Maximum number of results to return per page | | `cursor` | string | No | Pagination cursor from a previous response | @@ -685,7 +673,6 @@ Update fields on an existing customer profile | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `customerId` | string | Yes | ID of the customer to update | | `givenName` | string | No | First name of the customer | | `familyName` | string | No | Last name of the customer | @@ -749,7 +736,6 @@ Delete a customer profile from the Square customer directory | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `customerId` | string | Yes | ID of the customer to delete | #### Output @@ -771,7 +757,6 @@ List all locations associated with the Square account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | #### Output @@ -819,7 +804,6 @@ Retrieve a single location by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `locationId` | string | Yes | ID of the location to retrieve \(use "main" for the main location\) | #### Output @@ -869,7 +853,6 @@ Create an order with line items, taxes, discounts, and fulfillments | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `order` | json | Yes | Square order object including location_id and line_items \(e.g. \{"location_id":"L1","line_items":\[\{"name":"Coffee","quantity":"1","base_price_money":\{"amount":250,"currency":"USD"\}\}\]\}\) | | `idempotencyKey` | string | No | Unique key to make the request idempotent \(auto-generated if omitted\) | @@ -924,7 +907,6 @@ Retrieve a single order by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `orderId` | string | Yes | ID of the order to retrieve | #### Output @@ -978,7 +960,6 @@ Search orders across one or more locations using filters and sorting | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `locationIds` | array | Yes | Array of location IDs to search within | | `query` | json | No | Square order query object with optional filter and sort \(e.g. \{"filter":\{"state_filter":\{"states":\["OPEN"\]\}\}\}\) | | `limit` | number | No | Maximum number of results to return per page | @@ -1034,7 +1015,6 @@ Pay for an order using one or more already-approved payments | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `orderId` | string | Yes | ID of the order to pay for | | `paymentIds` | array | No | IDs of approved payments to apply to the order | | `orderVersion` | number | No | Version of the order being paid \(for optimistic concurrency\) | @@ -1091,7 +1071,6 @@ Create a draft invoice for an existing order and customer | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `invoice` | json | Yes | Square invoice object including location_id, order_id, primary_recipient, and payment_requests \(e.g. \{"location_id":"L1","order_id":"O1","primary_recipient":\{"customer_id":"C1"\},"payment_requests":\[\{"request_type":"BALANCE","due_date":"2026-07-01"\}\]\}\) | | `idempotencyKey` | string | No | Unique key to make the request idempotent \(auto-generated if omitted\) | @@ -1136,7 +1115,6 @@ Retrieve a single invoice by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `invoiceId` | string | Yes | ID of the invoice to retrieve | #### Output @@ -1180,7 +1158,6 @@ List invoices for a specific location | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `locationId` | string | Yes | ID of the location to list invoices for | | `limit` | number | No | Maximum number of results to return per page | | `cursor` | string | No | Pagination cursor from a previous response | @@ -1225,7 +1202,6 @@ Search invoices across one or more locations | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `locationId` | string | Yes | ID of the location to search within \(Square allows one location per search\) | | `limit` | number | No | Maximum number of results to return per page | | `cursor` | string | No | Pagination cursor from a previous response | @@ -1270,7 +1246,6 @@ Publish a draft invoice so it is sent to the customer and becomes payable | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `invoiceId` | string | Yes | ID of the invoice to publish | | `version` | number | Yes | Current version of the invoice \(use the version returned by Create Invoice\) | | `idempotencyKey` | string | No | Unique key to make the request idempotent \(auto-generated if omitted\) | @@ -1316,7 +1291,6 @@ Cancel a published invoice that is unpaid or partially paid | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `invoiceId` | string | Yes | ID of the invoice to cancel | | `version` | number | Yes | Current version of the invoice | @@ -1361,7 +1335,6 @@ Delete a draft invoice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `invoiceId` | string | Yes | ID of the draft invoice to delete | | `version` | number | No | Current version of the invoice \(required if the invoice has been updated\) | @@ -1384,7 +1357,6 @@ Create or update a catalog object such as an item, variation, or category | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `object` | json | Yes | Square catalog object to create or update. Use ID "#name" for new objects \(e.g. \{"type":"ITEM","id":"#Coffee","item_data":\{"name":"Coffee"\}\}\) | | `idempotencyKey` | string | No | Unique key to make the request idempotent \(auto-generated if omitted\) | @@ -1420,7 +1392,6 @@ Retrieve a single catalog object by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `objectId` | string | Yes | ID of the catalog object to retrieve | | `includeRelatedObjects` | boolean | No | Whether to include related objects such as an item variations | @@ -1456,7 +1427,6 @@ List catalog objects, optionally filtered by type | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `types` | string | No | Comma-separated catalog object types to return \(e.g. ITEM,CATEGORY\). Defaults to all top-level types | | `cursor` | string | No | Pagination cursor from a previous response | @@ -1491,7 +1461,6 @@ Search catalog objects by type and query filters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `objectTypes` | array | No | Array of catalog object types to search \(e.g. \["ITEM","CATEGORY"\]\) | | `query` | json | No | Square catalog query object \(e.g. \{"text_query":\{"keywords":\["coffee"\]\}\} or \{"prefix_query":\{...\}\}\) | | `limit` | number | No | Maximum number of results to return per page | @@ -1528,7 +1497,6 @@ Upload an image and attach it to the catalog, optionally to a specific item | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `file` | file | Yes | The image file to upload \(UserFile object\) | | `fileName` | string | No | Optional filename override for the image | | `objectId` | string | No | ID of the catalog object \(e.g. an item\) to attach the image to | @@ -1567,7 +1535,6 @@ Delete a catalog object and its children (e.g. an item and its variations) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `objectId` | string | Yes | ID of the catalog object to delete | #### Output @@ -1590,7 +1557,6 @@ Retrieve current inventory counts for catalog items across locations | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Square access token \(personal access token\) | | `catalogObjectIds` | array | No | IDs of the catalog item variations to retrieve counts for | | `locationIds` | array | No | IDs of the locations to retrieve counts for \(defaults to all locations\) | | `states` | array | No | Inventory states to filter by \(e.g. IN_STOCK, SOLD, IN_TRANSIT\) | diff --git a/apps/docs/content/docs/integrations/ssm.mdx b/apps/docs/content/docs/integrations/ssm.mdx index 0db4eaf06af..1513d8b48b1 100644 --- a/apps/docs/content/docs/integrations/ssm.mdx +++ b/apps/docs/content/docs/integrations/ssm.mdx @@ -2,7 +2,7 @@ title: AWS Systems Manager description: Run commands, manage parameters, and audit managed nodes integration: - type: "ssm" + type: "ssm_v2" color: "#E7157B" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [AWS Systems Manager](https://aws.amazon.com/systems-manager/) to run commands on managed nodes, read and write Parameter Store values, inspect inventory and compliance, and manage Automation runbooks. +Use [AWS Systems Manager](https://aws.amazon.com/systems-manager/) to run commands on managed nodes, read and write Parameter Store values, inspect inventory and compliance, and manage Automation runbooks. Connect with a saved [AWS credential](/integrations/aws-credentials). The following notes explain how secret values appear in workflow inputs, outputs, and run logs. @@ -40,8 +40,6 @@ Run an SSM document on managed nodes with AWS Systems Manager Run Command | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `documentName` | string | Yes | Name of the SSM document to run \(e.g., AWS-RunShellScript\) | | `instanceIds` | json | No | Managed node IDs to target, as an array of strings \(e.g., \["i-0123456789abcdef0"\]\). Provide instanceIds or targets | | `targets` | json | No | Tag or resource-group targets, as an array of \{Key, Values\} objects. Provide instanceIds or targets | @@ -94,8 +92,6 @@ List Run Command executions in an AWS account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `commandId` | string | No | Return only the command with this ID | | `instanceId` | string | No | Return only commands sent to this managed node | | `filters` | json | No | Filters, as an array of \{key, value\} objects. Valid keys: InvokedAfter, InvokedBefore, Status, ExecutionStage, DocumentName | @@ -123,8 +119,6 @@ List the per-node invocations of Run Command executions | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `commandId` | string | No | Return only invocations of this command | | `instanceId` | string | No | Return only invocations on this managed node | | `filters` | json | No | Filters, as an array of \{key, value\} objects. Valid keys: InvokedAfter, InvokedBefore, Status, DocumentName | @@ -153,8 +147,6 @@ Read the output and status of a Run Command execution on one managed node | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `commandId` | string | Yes | ID of the command, as returned by ssm_send_command | | `instanceId` | string | Yes | Managed node the command ran on \(e.g., i-0123456789abcdef0\) | | `pluginName` | string | No | Name of the document plugin to read output for; required for documents with more than one plugin | @@ -193,8 +185,6 @@ Cancel an in-flight Run Command execution | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `commandId` | string | Yes | ID of the command to cancel, as returned by ssm_send_command | | `instanceIds` | array | No | Managed node IDs to cancel on \(e.g., \["i-0123456789abcdef0"\]\); omit to cancel on every targeted node | @@ -218,8 +208,6 @@ Read one parameter from AWS Systems Manager Parameter Store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Name of the parameter, optionally with a :version or :label suffix | | `withDecryption` | boolean | No | Return the decrypted value of a SecureString parameter; ignored for String and StringList parameters | @@ -250,8 +238,6 @@ Read up to ten parameters from AWS Systems Manager Parameter Store by name | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `names` | json | Yes | Parameter names to read, as an array of at most 10 strings | | `withDecryption` | boolean | No | Return decrypted values for SecureString parameters; ignored for String and StringList parameters | @@ -276,8 +262,6 @@ Read parameters under a Parameter Store hierarchy path | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `path` | string | Yes | Hierarchy path to read, starting with a slash \(e.g., /prod/app\) | | `recursive` | boolean | No | Include parameters in nested paths below the given path | | `withDecryption` | boolean | No | Return decrypted values for SecureString parameters; ignored for String and StringList parameters | @@ -306,8 +290,6 @@ Create or update a parameter in AWS Systems Manager Parameter Store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Name of the parameter, optionally using a slash-separated hierarchy | | `value` | string | Yes | Value to store | | `type` | string | No | Parameter type \(String, StringList, or SecureString\); required when creating a new parameter | @@ -341,8 +323,6 @@ Delete a parameter from AWS Systems Manager Parameter Store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Name of the parameter to delete | #### Output @@ -365,8 +345,6 @@ List Parameter Store parameter metadata without reading any values | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `parameterFilters` | json | No | Filters, as an array of \{Key, Option, Values\} objects. Valid keys: Name, Type, KeyId, Path, Tier, DataType, or tag:<key> | | `shared` | boolean | No | Return parameters shared with this account instead of parameters it owns | | `maxResults` | number | No | Maximum number of parameters to return \(1-50\) | @@ -393,8 +371,6 @@ List managed nodes registered with AWS Systems Manager and their agent status | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `filters` | json | No | Filters, as an array of \{Key, Values\} objects. Valid keys: InstanceIds, AgentVersion, PingStatus, PlatformTypes, ActivationIds, IamRole, ResourceType, AssociationStatus, SourceIds, SourceTypes, tag-key, or tag:<key> | | `maxResults` | number | No | Maximum number of nodes to return \(5-50\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -420,8 +396,6 @@ List the patches reported for one managed node | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `instanceId` | string | Yes | Managed node to report patches for \(e.g., i-0123456789abcdef0\) | | `filters` | json | No | Filters, as an array of \{Key, Values\} objects. Valid keys: Classification, KBId, Severity, State | | `maxResults` | number | No | Maximum number of patches to return \(10-100\) | @@ -448,8 +422,6 @@ Read patch compliance summaries for a set of managed nodes | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `instanceIds` | json | Yes | Managed node IDs to summarize, as an array of at most 50 strings | | `maxResults` | number | No | Maximum number of patch states to return \(10-100\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -475,8 +447,6 @@ List individual compliance findings reported to AWS Systems Manager | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `resourceIds` | json | No | Resource to report on, as an array holding a single managed node ID | | `resourceTypes` | json | No | Resource type to report on, as an array holding a single value; currently only ManagedInstance is supported | | `filters` | json | No | Filters, as an array of \{Key, Values, Type\} objects. Type is one of EQUAL, NOT_EQUAL, BEGIN_WITH, LESS_THAN, GREATER_THAN | @@ -504,8 +474,6 @@ Read compliant and non-compliant counts per compliance type | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `filters` | json | No | Filters, as an array of \{Key, Values, Type\} objects. Type is one of EQUAL, NOT_EQUAL, BEGIN_WITH, LESS_THAN, GREATER_THAN | | `maxResults` | number | No | Maximum number of summaries to return \(1-50\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -531,8 +499,6 @@ Start an AWS Systems Manager Automation runbook execution | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `documentName` | string | Yes | Name of the Automation runbook to run \(e.g., AWS-RestartEC2Instance\) | | `documentVersion` | string | No | Runbook version to run \($LATEST, $DEFAULT, or a version number\) | | `parameters` | json | No | Runbook parameters, as an object mapping each parameter name to an array of string values | @@ -562,8 +528,6 @@ List Automation runbook executions in an AWS account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `filters` | json | No | Filters, as an array of \{Key, Values\} objects. Valid keys: DocumentNamePrefix, ExecutionStatus, ExecutionId, ParentExecutionId, CurrentAction, StartTimeBefore, StartTimeAfter, AutomationType, TagKey, TargetResourceGroup, AutomationSubtype, OpsItemId | | `maxResults` | number | No | Maximum number of executions to return \(1-50\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -589,8 +553,6 @@ Read the status, outputs, and step results of one Automation execution | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `automationExecutionId` | string | Yes | ID of the execution, as returned by ssm_start_automation_execution | #### Output @@ -631,8 +593,6 @@ Stop a running AWS Systems Manager Automation execution | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `automationExecutionId` | string | Yes | ID of the execution to stop, as returned by ssm_start_automation_execution | | `stopType` | string | No | How to stop the execution: Cancel to stop it immediately, or Complete to let the current step finish | @@ -656,8 +616,6 @@ List SSM documents and runbooks available to an AWS account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `filters` | json | No | Filters, as an array of \{Key, Values\} objects. Valid keys: Name, Owner, DocumentType, PlatformTypes, TargetType, or tag:<key> | | `maxResults` | number | No | Maximum number of documents to return \(1-50\) | | `nextToken` | string | No | Pagination token from a previous request | @@ -683,8 +641,6 @@ Read the content of an SSM document or Automation runbook | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `name` | string | Yes | Name of the document to read, as returned by ssm_list_documents | | `documentVersion` | string | No | Document version to read \($LATEST, $DEFAULT, or a version number\) | | `versionName` | string | No | User-defined version name to read | diff --git a/apps/docs/content/docs/integrations/stripe.mdx b/apps/docs/content/docs/integrations/stripe.mdx index df414b2abc2..7b996eee532 100644 --- a/apps/docs/content/docs/integrations/stripe.mdx +++ b/apps/docs/content/docs/integrations/stripe.mdx @@ -2,7 +2,7 @@ title: Stripe description: Process payments and manage Stripe data integration: - type: "stripe" + type: "stripe_v2" color: "#635BFF" --- @@ -29,7 +29,6 @@ Create a new Payment Intent to process a payment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `amount` | number | Yes | Amount in the currency’s smallest unit \(e.g., 2000 for 20.00 USD\) | | `currency` | string | Yes | Three-letter ISO currency code \(e.g., usd, eur\) | | `customer` | string | No | Customer ID to associate with this payment | @@ -108,7 +107,6 @@ Retrieve an existing Payment Intent by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | #### Output @@ -180,7 +178,6 @@ Update an existing Payment Intent | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | | `amount` | number | No | Updated amount in the currency’s smallest unit | | `currency` | string | No | Three-letter ISO currency code | @@ -257,7 +254,6 @@ Confirm a Payment Intent and inspect its resulting status. Confirmation can requ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | | `payment_method` | string | No | Payment method ID to confirm with | @@ -330,7 +326,6 @@ Capture an authorized Payment Intent | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | | `amount_to_capture` | number | No | Amount to capture in the currency’s smallest unit \(defaults to the full amount\) | @@ -403,7 +398,6 @@ Cancel a Payment Intent | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | | `cancellation_reason` | string | No | Reason for cancellation \(duplicate, fraudulent, requested_by_customer, abandoned\) | @@ -476,7 +470,6 @@ List all Payment Intents | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | | `customer` | string | No | Filter by customer ID | | `created` | json | No | Filter by creation date \(e.g., \{"gt": 1633024800\}\) | @@ -548,7 +541,6 @@ Search for Payment Intents using query syntax | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `query` | string | Yes | Search query \(e.g., "status:'succeeded' AND currency:'usd'"\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | @@ -619,7 +611,6 @@ Create a new customer object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `email` | string | No | Customer email address | | `name` | string | No | Customer full name | | `phone` | string | No | Customer phone number | @@ -687,7 +678,6 @@ Retrieve an existing customer by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Customer ID \(e.g., cus_1234567890\) | #### Output @@ -749,7 +739,6 @@ Update an existing customer | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Customer ID \(e.g., cus_1234567890\) | | `email` | string | No | Updated email address | | `name` | string | No | Updated name | @@ -817,7 +806,6 @@ Permanently delete a customer | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Customer ID \(e.g., cus_1234567890\) | #### Output @@ -839,7 +827,6 @@ List all customers | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | | `email` | string | No | Filter by email address | | `created` | json | No | Filter by creation date | @@ -902,7 +889,6 @@ Search for customers using query syntax | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `query` | string | Yes | Search query \(e.g., "email:'customer@example.com'"\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | @@ -964,7 +950,6 @@ Create a new subscription for a customer | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `customer` | string | Yes | Customer ID to subscribe | | `items` | json | Yes | Array of items with price IDs \(e.g., \[\{"price": "price_xxx", "quantity": 1\}\]\) | | `trial_period_days` | number | No | Number of trial days | @@ -1050,7 +1035,6 @@ Retrieve an existing subscription by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Subscription ID \(e.g., sub_1234567890\) | #### Output @@ -1131,7 +1115,6 @@ Update an existing subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Subscription ID \(e.g., sub_1234567890\) | | `items` | json | No | Updated array of items with price IDs | | `cancel_at_period_end` | boolean | No | Cancel subscription at period end | @@ -1215,7 +1198,6 @@ Cancel a subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Subscription ID \(e.g., sub_1234567890\) | | `prorate` | boolean | No | Whether to prorate the cancellation | | `invoice_now` | boolean | No | Whether to invoice immediately | @@ -1298,7 +1280,6 @@ Resume a paused subscription | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Subscription ID \(e.g., sub_1234567890\) | #### Output @@ -1379,7 +1360,6 @@ List one page of subscriptions. Follow starting_after while metadata.has_more is | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | | `customer` | string | No | Filter by customer ID | | `status` | string | No | Filter by status \(active, past_due, unpaid, canceled, incomplete, incomplete_expired, trialing, all\) | @@ -1464,7 +1444,6 @@ Search one page of subscriptions using query syntax. Pass metadata.next_page as | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `query` | string | Yes | Search query \(e.g., "status:'active' AND metadata\['order_id'\]:'6735'"\) | | `page` | string | No | Pagination token from metadata.next_page. Omit for the first page and keep the same query. | | `limit` | number | No | Number of results to return \(default 10, max 100\) | @@ -1547,7 +1526,6 @@ Create a new invoice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `customer` | string | Yes | Customer ID \(e.g., cus_1234567890\) | | `description` | string | No | Description of the invoice | | `metadata` | json | No | Set of key-value pairs | @@ -1711,7 +1689,6 @@ Retrieve an existing invoice by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | #### Output @@ -1871,7 +1848,6 @@ Update an existing invoice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | | `description` | string | No | Description of the invoice | | `metadata` | json | No | Set of key-value pairs | @@ -2034,7 +2010,6 @@ Permanently delete a draft invoice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | #### Output @@ -2056,7 +2031,6 @@ Finalize a draft invoice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | | `auto_advance` | boolean | No | Auto-advance the invoice | @@ -2217,7 +2191,6 @@ Pay an invoice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | | `paid_out_of_band` | boolean | No | Mark invoice as paid out of band | @@ -2378,7 +2351,6 @@ Void an invoice | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | #### Output @@ -2538,7 +2510,6 @@ Send an invoice to the customer | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | #### Output @@ -2564,7 +2535,6 @@ List all invoices | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | | `customer` | string | No | Filter by customer ID | | `status` | string | No | Filter by invoice status | @@ -2590,7 +2560,6 @@ Search for invoices using query syntax | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `query` | string | Yes | Search query \(e.g., "customer:'cus_1234567890'"\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | @@ -2615,7 +2584,6 @@ Create a new charge to process a payment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `amount` | number | Yes | Amount in the currency’s smallest unit \(e.g., 2000 for 20.00 USD\) | | `currency` | string | Yes | Three-letter ISO currency code \(e.g., usd, eur\) | | `customer` | string | No | Customer ID to associate with this charge | @@ -2648,7 +2616,6 @@ Retrieve an existing charge by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Charge ID \(e.g., ch_1234567890\) | #### Output @@ -2675,7 +2642,6 @@ Update an existing charge | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Charge ID \(e.g., ch_1234567890\) | | `description` | string | No | Updated description | | `metadata` | json | No | Updated metadata | @@ -2704,7 +2670,6 @@ Capture an uncaptured charge | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Charge ID \(e.g., ch_1234567890\) | | `amount` | number | No | Amount to capture in the currency’s smallest unit \(defaults to the full amount\) | @@ -2732,7 +2697,6 @@ List all charges | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | | `customer` | string | No | Filter by customer ID | | `created` | json | No | Filter by creation date \(e.g., \{"gt": 1633024800\}\) | @@ -2758,7 +2722,6 @@ Search for charges using query syntax | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `query` | string | Yes | Search query \(e.g., "status:'succeeded' AND currency:'usd'"\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | @@ -2783,7 +2746,6 @@ Create a new product object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `name` | string | Yes | Product name | | `description` | string | No | Product description | | `active` | boolean | No | Whether the product is active | @@ -2812,7 +2774,6 @@ Retrieve an existing product by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Product ID \(e.g., prod_1234567890\) | #### Output @@ -2837,7 +2798,6 @@ Update an existing product | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Product ID \(e.g., prod_1234567890\) | | `name` | string | No | Updated product name | | `description` | string | No | Updated product description | @@ -2867,7 +2827,6 @@ Permanently delete a product | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Product ID \(e.g., prod_1234567890\) | #### Output @@ -2889,7 +2848,6 @@ List all products | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | | `active` | boolean | No | Filter by active status | @@ -2914,7 +2872,6 @@ Search for products using query syntax | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `query` | string | Yes | Search query \(e.g., "name:'shirt'"\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | @@ -2939,7 +2896,6 @@ Create a new price for a product | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `product` | string | Yes | Product ID \(e.g., prod_1234567890\) | | `currency` | string | Yes | Three-letter ISO currency code \(e.g., usd, eur\) | | `unit_amount` | number | No | Amount in the currency’s smallest unit \(e.g., 1000 for 10.00 USD\) | @@ -2970,7 +2926,6 @@ Retrieve an existing price by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Price ID \(e.g., price_1234567890\) | #### Output @@ -2996,7 +2951,6 @@ Update an existing price | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Price ID \(e.g., price_1234567890\) | | `active` | boolean | No | Whether the price is active | | `metadata` | json | No | Updated metadata | @@ -3024,7 +2978,6 @@ List all prices | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | | `product` | string | No | Filter by product ID | | `active` | boolean | No | Filter by active status | @@ -3050,7 +3003,6 @@ Search for prices using query syntax | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `query` | string | Yes | Search query \(e.g., "active:'true' AND currency:'usd'"\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | @@ -3075,7 +3027,6 @@ Retrieve an existing Event by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `id` | string | Yes | Event ID \(e.g., evt_1234567890\) | #### Output @@ -3100,7 +3051,6 @@ List all Events | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Stripe API key \(secret key\) | | `limit` | number | No | Number of results to return \(default 10, max 100\) | | `type` | string | No | Filter by event type \(e.g., payment_intent.created\) | | `created` | json | No | Filter by creation date \(e.g., \{"gt": 1633024800\}\) | diff --git a/apps/docs/content/docs/integrations/sts.mdx b/apps/docs/content/docs/integrations/sts.mdx index 3d3be97dae7..d758e051a3f 100644 --- a/apps/docs/content/docs/integrations/sts.mdx +++ b/apps/docs/content/docs/integrations/sts.mdx @@ -2,7 +2,7 @@ title: AWS STS description: Connect to AWS Security Token Service integration: - type: "sts" + type: "sts_v2" color: "linear-gradient(45deg, #BD0816 0%, #FF5252 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [AWS STS](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) to request temporary credentials, assume roles, verify the calling identity, and identify the AWS account associated with an access key. +Use [AWS STS](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) to request temporary credentials, assume roles, verify the calling identity, and identify the AWS account associated with an access key. Connect with a saved [AWS credential](/integrations/aws-credentials). {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -30,8 +30,6 @@ Assume an IAM role and receive temporary security credentials | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `roleArn` | string | Yes | ARN of the IAM role to assume | | `roleSessionName` | string | Yes | Identifier for the assumed role session | | `durationSeconds` | number | No | Duration of the session in seconds \(900-43200, default 3600\) | @@ -144,8 +142,6 @@ Get details about the IAM user or role whose credentials are used to call the AP | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | #### Output @@ -168,8 +164,6 @@ Get temporary security credentials for an IAM user, optionally with MFA | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `durationSeconds` | number | No | Duration of the session in seconds \(900-129600, default 43200\) | | `serialNumber` | string | No | MFA device serial number or ARN | | `tokenCode` | string | No | MFA token code \(6 digits\) | @@ -196,8 +190,6 @@ Get the AWS account ID associated with an access key | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | AWS region \(e.g., us-east-1\) | -| `accessKeyId` | string | Yes | AWS access key ID | -| `secretAccessKey` | string | Yes | AWS secret access key | | `targetAccessKeyId` | string | Yes | The access key ID to look up | #### Output diff --git a/apps/docs/content/docs/integrations/supabase.mdx b/apps/docs/content/docs/integrations/supabase.mdx index 815363c2ab0..1c1f8effb13 100644 --- a/apps/docs/content/docs/integrations/supabase.mdx +++ b/apps/docs/content/docs/integrations/supabase.mdx @@ -2,7 +2,7 @@ title: Supabase description: Use Supabase database integration: - type: "supabase" + type: "supabase_v2" color: "#1C1C1C" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -[Supabase](https://www.supabase.com/) provides Postgres databases and related project services. Use this integration to query, insert, update, and delete table rows, or select the storage, function, and other operations listed below. Configure your Project ID and Service Role Secret; table operations also require a table name. Use [PostgREST filter syntax](https://postgrest.org/en/stable/api.html#operators) when filtering, ordering, or limiting row queries. +[Supabase](https://www.supabase.com/) provides Postgres databases and related project services. Use this integration to query, insert, update, and delete table rows, or select the storage, function, and other operations listed below. Connect a saved Supabase credential (project ID and key); table operations also require a table name. Use [PostgREST filter syntax](https://postgrest.org/en/stable/api.html#operators) when filtering, ordering, or limiting row queries. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -29,7 +29,6 @@ Query data from a Supabase table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to query | | `schema` | string | No | Database schema to query from \(default: public\). Use this to access tables in other schemas. | | `select` | string | No | Columns to return \(comma-separated\). Defaults to * \(all columns\) | @@ -37,7 +36,6 @@ Query data from a Supabase table | `orderBy` | string | No | Column to order by \(add DESC for descending\) | | `limit` | number | No | Maximum number of rows to return | | `offset` | number | No | Number of rows to skip \(for pagination\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -58,11 +56,9 @@ Insert data into a Supabase table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to insert data into | | `schema` | string | No | Database schema to insert into \(default: public\). Use this to access tables in other schemas. | | `data` | array | Yes | The data to insert \(array of objects or a single object\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -83,12 +79,10 @@ Get a single row from a Supabase table based on filter criteria | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to query | | `schema` | string | No | Database schema to query from \(default: public\). Use this to access tables in other schemas. | | `select` | string | No | Columns to return \(comma-separated\). Defaults to * \(all columns\) | | `filter` | string | Yes | PostgREST filter to find the specific row \(e.g., "id=eq.123"\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -109,12 +103,10 @@ Update rows in a Supabase table based on filter criteria | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to update | | `schema` | string | No | Database schema to update in \(default: public\). Use this to access tables in other schemas. | | `filter` | string | Yes | PostgREST filter to identify rows to update \(e.g., "id=eq.123"\) | | `data` | object | Yes | Data to update in the matching rows | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -135,11 +127,9 @@ Delete rows from a Supabase table based on filter criteria | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to delete from | | `schema` | string | No | Database schema to delete from \(default: public\). Use this to access tables in other schemas. | | `filter` | string | Yes | PostgREST filter to identify rows to delete \(e.g., "id=eq.123"\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -160,12 +150,10 @@ Insert or update data in a Supabase table (upsert operation) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to upsert data into | | `schema` | string | No | Database schema to upsert into \(default: public\). Use this to access tables in other schemas. | | `data` | array | Yes | The data to upsert \(insert or update\) - array of objects or a single object | | `onConflict` | string | No | Comma-separated column\(s\) with a unique or primary key constraint to resolve conflicts on \(e.g., "email"\). Defaults to the primary key. | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -186,12 +174,10 @@ Count rows in a Supabase table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to count rows from | | `schema` | string | No | Database schema to count from \(default: public\). Use this to access tables in other schemas. | | `filter` | string | No | PostgREST filter \(e.g., "status=eq.active"\) | | `countType` | string | No | Count type: exact, planned, or estimated \(default: exact\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -212,7 +198,6 @@ Perform full-text search on a Supabase table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to search | | `schema` | string | No | Database schema to search in \(default: public\). Use this to access tables in other schemas. | | `column` | string | Yes | The column to search in | @@ -221,7 +206,6 @@ Perform full-text search on a Supabase table | `language` | string | No | Language for text search configuration \(default: english\) | | `limit` | number | No | Maximum number of rows to return | | `offset` | number | No | Number of rows to skip \(for pagination\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -242,12 +226,10 @@ Perform similarity search using pgvector in a Supabase table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `functionName` | string | Yes | The name of the PostgreSQL function that performs vector search \(e.g., match_documents\) | | `queryEmbedding` | array | Yes | The query vector/embedding to search for similar items | | `matchThreshold` | number | No | Minimum similarity threshold \(0-1\), typically 0.7-0.9 | | `matchCount` | number | No | Maximum number of results to return \(default: 10\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -268,10 +250,8 @@ Call a PostgreSQL function in Supabase | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `functionName` | string | Yes | The name of the PostgreSQL function to call | | `params` | object | No | Parameters to pass to the function as a JSON object | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -292,12 +272,10 @@ Invoke a Supabase Edge Function over HTTP | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `functionName` | string | Yes | The name of the Edge Function to invoke \(e.g., "hello-world"\) | | `method` | string | No | HTTP method to use: GET, POST, PUT, PATCH, or DELETE \(default: POST\) | | `body` | json | No | Request payload to send to the function as a JSON object \(ignored for GET\) | | `headers` | json | No | Additional request headers as a JSON object of header name to value | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -318,9 +296,7 @@ Introspect Supabase database schema from its OpenAPI spec to get table and colum | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `schema` | string | No | Database schema to introspect \(defaults to all user schemas, commonly "public"\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -363,7 +339,6 @@ Upload a file to a Supabase storage bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the storage bucket | | `fileName` | string | Yes | The name of the file \(e.g., "document.pdf", "image.jpg"\) | | `path` | string | No | Optional folder path \(e.g., "folder/subfolder/"\) | @@ -371,7 +346,6 @@ Upload a file to a Supabase storage bucket | `contentType` | string | No | MIME type of the file \(e.g., "image/jpeg", "text/plain"\) | | `cacheControl` | string | No | Cache-Control header value in seconds for the stored object \(e.g., "3600"; default: "3600"\) | | `upsert` | boolean | No | If true, overwrites existing file \(default: false\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -397,11 +371,9 @@ Download a file from a Supabase storage bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the storage bucket | | `path` | string | Yes | The path to the file to download \(e.g., "folder/file.jpg"\) | | `fileName` | string | No | Optional filename override | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -421,7 +393,6 @@ List files in a Supabase storage bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the storage bucket | | `path` | string | No | The folder path to list files from \(default: root\) | | `limit` | number | No | Maximum number of files to return \(default: 100\) | @@ -429,7 +400,6 @@ List files in a Supabase storage bucket | `sortBy` | string | No | Column to sort by: name, created_at, updated_at, last_accessed_at \(default: name\) | | `sortOrder` | string | No | Sort order: asc or desc \(default: asc\) | | `search` | string | No | Search term to filter files by name | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -463,10 +433,8 @@ Delete files from a Supabase storage bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the storage bucket | | `paths` | array | Yes | Array of file paths to delete \(e.g., \["folder/file1.jpg", "folder/file2.jpg"\]\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -494,11 +462,9 @@ Move a file within a Supabase storage bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the storage bucket | | `fromPath` | string | Yes | The current path of the file \(e.g., "folder/old.jpg"\) | | `toPath` | string | Yes | The new path for the file \(e.g., "newfolder/new.jpg"\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -522,11 +488,9 @@ Copy a file within a Supabase storage bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the storage bucket | | `fromPath` | string | Yes | The path of the source file \(e.g., "folder/source.jpg"\) | | `toPath` | string | Yes | The path for the copied file \(e.g., "folder/copy.jpg"\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -549,12 +513,10 @@ Create a new storage bucket in Supabase | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the bucket to create | | `isPublic` | boolean | No | Whether the bucket should be publicly accessible \(default: false\) | | `fileSizeLimit` | number | No | Maximum file size in bytes \(optional\) | | `allowedMimeTypes` | array | No | Array of allowed MIME types \(e.g., \["image/png", "image/jpeg"\]\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -576,12 +538,10 @@ Update the configuration of an existing Supabase storage bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the bucket to update | | `isPublic` | boolean | No | Whether the bucket should be publicly accessible \(leave unset to keep the current value\) | | `fileSizeLimit` | number | No | Maximum file size in bytes \(leave unset to keep the current value\) | | `allowedMimeTypes` | array | No | Array of allowed MIME types \(e.g., \["image/png", "image/jpeg"\]\) — leave unset to keep the current value | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -603,9 +563,7 @@ Delete all objects inside a Supabase storage bucket without deleting the bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the bucket to empty | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -627,8 +585,6 @@ List all storage buckets in Supabase | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -657,9 +613,7 @@ Delete a storage bucket in Supabase | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the bucket to delete | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -681,7 +635,6 @@ Get the public URL for a file in a Supabase storage bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the storage bucket | | `path` | string | Yes | The path to the file \(e.g., "folder/file.jpg"\) | | `download` | boolean | No | If true, forces download instead of inline display \(default: false\) | @@ -705,12 +658,10 @@ Create a temporary signed URL for a file in a Supabase storage bucket | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the storage bucket | | `path` | string | Yes | The path to the file \(e.g., "folder/file.jpg"\) | | `expiresIn` | number | Yes | Number of seconds until the URL expires \(e.g., 3600 for 1 hour\) | | `download` | boolean | No | If true, forces download instead of inline display \(default: false\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output @@ -731,11 +682,9 @@ Create a temporary signed URL a client can use to upload directly to a Supabase | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `bucket` | string | Yes | The name of the storage bucket | | `path` | string | Yes | The destination path for the uploaded file \(e.g., "folder/file.jpg"\) | | `upsert` | boolean | No | If true, allows overwriting an existing file at this path \(default: false\) | -| `apiKey` | string | Yes | Your Supabase service role secret key | #### Output diff --git a/apps/docs/content/docs/integrations/tailscale.mdx b/apps/docs/content/docs/integrations/tailscale.mdx index 0d7e6496d1d..553ba29dc62 100644 --- a/apps/docs/content/docs/integrations/tailscale.mdx +++ b/apps/docs/content/docs/integrations/tailscale.mdx @@ -2,7 +2,7 @@ title: Tailscale description: Manage devices and network settings in your Tailscale tailnet integration: - type: "tailscale" + type: "tailscale_v2" color: "#2E2D2D" --- @@ -14,11 +14,9 @@ Use [Tailscale](https://tailscale.com) to manage devices, users, device posture, ## Authentication -Enter a Tailscale **API access token** in **API Access Token**. The block sends it using Bearer authentication. Create a personal token in the [Keys page of the admin console](https://login.tailscale.com/admin/settings/keys); personal tokens expire after 1–90 days and inherit the user's permissions. +Save a Tailscale **API access token** as a Tailscale credential and select it in the block; Sim sends it using Bearer authentication. Create a personal token in the [Keys page of the admin console](https://login.tailscale.com/admin/settings/keys); personal tokens expire after 1–90 days and inherit the user's permissions. Replace the credential's token before it expires. -An access token generated by a Tailscale OAuth client or federated identity also works. Generate that token using Tailscale's documented [trust credential flow](https://tailscale.com/docs/reference/trust-credentials), then supply the access token to the block. The block does not exchange client secrets or refresh expired tokens. For scheduled workflows, supply a fresh access token from your credential management workflow before it expires. - -Device enrollment keys (`tskey-auth-`) and OAuth client secrets (`tskey-client-`) cannot authenticate these API requests. When using scoped OAuth access tokens, grant the scopes required by the selected endpoint in the [API reference](https://tailscale.com/api). Creating a tailnet-owned auth key with OAuth also requires device tags owned by that credential. +Don't save an access token minted by a Tailscale OAuth client: it expires after about an hour, and Sim does not refresh it. Device enrollment keys (`tskey-auth-`) and OAuth client secrets (`tskey-client-`) cannot authenticate these API requests. ## Tailnet identifier [#tailnet-identifier] @@ -55,7 +53,6 @@ List devices in the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `fields` | string | No | Device fields to return: default or all | | `filters` | string | No | URL query filters for top-level device fields, e.g. isEphemeral=true&tags=tag:server | @@ -128,7 +125,6 @@ Get details of a specific device | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | | `fields` | string | No | Device fields to return: default or all | @@ -198,7 +194,6 @@ Rename a device or reset its name to the OS hostname | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | | `name` | string | No | New device name or FQDN; empty resets it to the OS hostname | @@ -222,7 +217,6 @@ Remove a device from the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | #### Output @@ -244,7 +238,6 @@ Authorize or deauthorize a device on the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | | `authorized` | boolean | Yes | Whether to authorize or deauthorize the device | @@ -268,7 +261,6 @@ Replace the tags assigned to a device | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | | `tags` | string | Yes | Comma-separated device tags; use \[\] to clear tags | @@ -292,7 +284,6 @@ Get the advertised and approved subnet routes for a device | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | #### Output @@ -314,7 +305,6 @@ Replace the enabled subnet routes for a device | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | | `routes` | string | Yes | Comma-separated enabled CIDR routes; use \[\] to disable all routes | @@ -337,7 +327,6 @@ Enable or disable device key expiry | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | | `keyExpiryDisabled` | boolean | Yes | Whether to disable device key expiry | @@ -361,7 +350,6 @@ Immediately expire a device node key so the device must reauthenticate | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | #### Output @@ -383,7 +371,6 @@ Get device posture attributes and their expiry times | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | #### Output @@ -405,7 +392,6 @@ Create or update a custom device posture attribute with an optional expiry | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | | `attributeKey` | string | Yes | Custom posture attribute name, prefixed with custom: | | `attributeValue` | string | Yes | JSON scalar attribute value: a quoted string, integer, or boolean | @@ -431,7 +417,6 @@ Remove a custom posture attribute from a device | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `deviceId` | string | Yes | Device nodeId \(preferred\) or legacy device ID | | `attributeKey` | string | Yes | Custom posture attribute name, prefixed with custom: | @@ -455,7 +440,6 @@ Get the global DNS nameservers configured for the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | #### Output @@ -476,7 +460,6 @@ Replace the global DNS nameservers for the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `dns` | string | Yes | Comma-separated DNS nameserver IP addresses; \[\] clears the nameservers | @@ -499,7 +482,6 @@ Get the MagicDNS preference for the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | #### Output @@ -520,7 +502,6 @@ Enable or disable MagicDNS for the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `magicDNS` | boolean | Yes | Whether MagicDNS is enabled | @@ -542,7 +523,6 @@ Get the DNS search domains configured for the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | #### Output @@ -563,7 +543,6 @@ Replace the DNS search domains for the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `searchPaths` | string | Yes | Comma-separated DNS search domains; \[\] clears the search paths | @@ -585,7 +564,6 @@ Get split DNS resolvers | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | #### Output @@ -606,7 +584,6 @@ Update selected split DNS resolvers | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `splitDns` | string | Yes | JSON map of domains to nameserver arrays; null deletes a domain when updating | @@ -628,7 +605,6 @@ Replace all split DNS resolvers | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `splitDns` | string | Yes | JSON map of domains to nameserver arrays; null deletes a domain when updating | @@ -650,7 +626,6 @@ Get tailnet DNS configuration | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | #### Output @@ -679,7 +654,6 @@ Replace the full tailnet DNS configuration | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `dnsConfiguration` | string | Yes | Complete DNS configuration JSON: nameservers, splitDNS, searchPaths, preferences | @@ -709,7 +683,6 @@ List users in the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `userType` | string | No | User relationship: member \(default\), shared, or all | | `role` | string | No | User role: owner, member, admin, it-admin, network-admin, billing-admin, or auditor | @@ -745,7 +718,6 @@ Get details of a specific user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `userId` | string | Yes | Tailscale user ID | #### Output @@ -777,7 +749,6 @@ Change a tailnet user’s role | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `userId` | string | Yes | Tailscale user ID | | `role` | string | Yes | User role: owner, member, admin, it-admin, network-admin, billing-admin, or auditor | @@ -801,7 +772,6 @@ Approve a pending user to join the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `userId` | string | Yes | Tailscale user ID | #### Output @@ -823,7 +793,6 @@ Suspend a user’s access to the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `userId` | string | Yes | Tailscale user ID | #### Output @@ -845,7 +814,6 @@ Restore a suspended user’s access to the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `userId` | string | Yes | Tailscale user ID | #### Output @@ -867,7 +835,6 @@ Delete a user from the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `userId` | string | Yes | Tailscale user ID | #### Output @@ -889,7 +856,6 @@ Create a device enrollment auth key | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `reusable` | boolean | No | Whether the auth key can register multiple devices \(default false\) | | `ephemeral` | boolean | No | Whether devices registered with the key are ephemeral \(default false\) | @@ -937,7 +903,6 @@ List active auth keys, API access tokens, and trust credentials in the tailnet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `all` | boolean | No | Include all tailnet keys \(default true\); false restricts keys according to the access token owner | @@ -981,7 +946,6 @@ Get a key’s type, owner, scopes, expiry, and device enrollment capabilities | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `keyId` | string | Yes | Tailscale key ID | @@ -1023,7 +987,6 @@ Revoke an auth key, API access token, or trust credential | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `keyId` | string | Yes | Tailscale key ID | @@ -1047,7 +1010,6 @@ Get the tailnet policy file | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `details` | boolean | No | Include the original HuJSON policy and its warnings and parsing errors | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | #### Output @@ -1071,7 +1033,6 @@ Validate a policy file or run ACL tests without changing the tailnet policy | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `acl` | string | Yes | Policy file in JSON or HuJSON, or a JSON array of ACL tests for validation | @@ -1098,7 +1059,6 @@ Replace the tailnet policy file | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `acl` | string | Yes | Complete tailnet policy file in JSON or HuJSON | | `ifMatch` | string | No | ETag from Get ACL, or ts-default to replace only an untouched default policy | @@ -1122,7 +1082,6 @@ Get tailnet settings | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | #### Output @@ -1156,7 +1115,6 @@ Update selected tailnet settings | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Tailscale API access token or OAuth access token \(not an auth key or OAuth client secret\) | | `tailnet` | string | Yes | Tailnet ID, legacy name, or "-" for the token’s default tailnet | | `settings` | string | Yes | JSON object of settings to update; omitted settings remain unchanged | diff --git a/apps/docs/content/docs/integrations/tavily.mdx b/apps/docs/content/docs/integrations/tavily.mdx index cc85ce655d0..154a1f997af 100644 --- a/apps/docs/content/docs/integrations/tavily.mdx +++ b/apps/docs/content/docs/integrations/tavily.mdx @@ -2,7 +2,7 @@ title: Tavily description: Search and extract information integration: - type: "tavily" + type: "tavily_v2" color: "#FFFFFF" --- @@ -15,7 +15,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" ## Use the block [#usage-instructions] -Search the web, extract content from URLs, crawl sites, and map their URLs. Requires an API key. +Search the web, extract content from URLs, crawl sites, and map their URLs. ## Actions @@ -46,7 +46,6 @@ Perform AI-powered web searches using Tavily's search API. Returns structured re | `exclude_domains` | string | No | Comma-separated list of domains to blacklist \(e.g., "pinterest.com,reddit.com"\) | | `country` | string | No | Boost results from specified country \(general topic only\) | | `auto_parameters` | boolean | No | Automatic parameter configuration based on query intent | -| `apiKey` | string | Yes | Tavily API Key | #### Output @@ -84,7 +83,6 @@ Extract raw content from multiple web pages simultaneously using Tavily's extrac | `format` | string | No | Output format: markdown or text \(default: markdown\) | | `include_images` | boolean | No | Incorporate images in extraction output | | `include_favicon` | boolean | No | Add favicon URL for each result | -| `apiKey` | string | Yes | Tavily API Key | #### Output @@ -126,7 +124,6 @@ Systematically crawl and extract content from websites using Tavily's crawl API. | `extract_depth` | string | No | Extraction depth: basic \(1 credit/5 pages\) or advanced \(2 credits/5 pages\) | | `format` | string | No | Output format: markdown or text \(default: markdown\) | | `include_favicon` | boolean | No | Add favicon URL for each result | -| `apiKey` | string | Yes | Tavily API Key | #### Output @@ -162,7 +159,6 @@ Discover and visualize website structure using Tavily's map API. Maps out all ac | `exclude_paths` | string | No | Comma-separated regex patterns to exclude specific URL paths | | `exclude_domains` | string | No | Comma-separated regex patterns to exclude domains | | `allow_external` | boolean | No | Include external domain links in results \(default: true\) | -| `apiKey` | string | Yes | Tavily API Key | #### Output diff --git a/apps/docs/content/docs/integrations/telegram.mdx b/apps/docs/content/docs/integrations/telegram.mdx index 33480512415..0701da3bf4f 100644 --- a/apps/docs/content/docs/integrations/telegram.mdx +++ b/apps/docs/content/docs/integrations/telegram.mdx @@ -2,7 +2,7 @@ title: Telegram description: Interact with Telegram integration: - type: "telegram" + type: "telegram_v2" color: "#FFFFFF" --- @@ -27,7 +27,7 @@ The [webhook setup video](https://www.youtube.com/embed/9oKcJtQ0_IM) also demons 1. Add the bot to the group and grant the permissions needed by the action. 2. Obtain the group's numeric chat ID from a bot message update, such as the trigger's `message.raw.chat.id`. Group IDs can be negative. -3. Add a Telegram action block, select **Send Message**, and set **Bot Token**, **Chat ID**, and **Message**. +3. Add a Telegram action block, select **Send Message**, choose a **Telegram Credential** (a saved bot token), and set **Chat ID** and **Message**. 4. Run the workflow and verify the message in the group. The [group messaging video](https://www.youtube.com/embed/AG55LpUreGI) demonstrates the block configuration. Bot privacy settings affect which group messages Telegram delivers; receiving a command addressed to the bot can help verify the trigger before changing those settings. @@ -49,7 +49,6 @@ Send a text message through the Telegram Bot API. Markdown is converted to Teleg | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `text` | string | Yes | Message text to send | @@ -85,7 +84,6 @@ Delete messages in Telegram channels or chats through the Telegram Bot API. Requ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `messageId` | string | Yes | Telegram message ID \(numeric identifier of the message to delete\) | @@ -110,7 +108,6 @@ Send photos to Telegram channels or users through the Telegram Bot API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `photo` | string | Yes | Photo to send. Pass a file_id or HTTP URL | | `caption` | string | No | Photo caption \(optional\) | @@ -153,7 +150,6 @@ Send videos to Telegram channels or users through the Telegram Bot API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `video` | string | Yes | Video to send. Pass a file_id or HTTP URL | | `caption` | string | No | Video caption \(optional\) | @@ -229,7 +225,6 @@ Send audio files to Telegram channels or users through the Telegram Bot API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `audio` | string | Yes | Audio file to send. Pass a file_id or HTTP URL | | `caption` | string | No | Audio caption \(optional\) | @@ -275,7 +270,6 @@ Send animations (GIFs) to Telegram channels or users through the Telegram Bot AP | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `animation` | string | Yes | Animation to send. Pass a file_id or HTTP URL | | `caption` | string | No | Animation caption \(optional\) | @@ -351,7 +345,6 @@ Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the T | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `files` | file[] | No | Document file to send \(PDF, ZIP, DOC, etc.\). Max size: 50MB | | `caption` | string | No | Document caption \(optional\) | @@ -394,7 +387,6 @@ Edit the text of an existing message in a Telegram chat or channel through the T | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `messageId` | number | Yes | Identifier of the message to edit | | `text` | string | Yes | New text of the message | @@ -421,7 +413,6 @@ Forward a message from one Telegram chat to another through the Telegram Bot API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Destination chat ID \(numeric, can be negative for groups\) | | `fromChatId` | string | Yes | Source chat ID the original message belongs to | | `messageId` | number | Yes | Identifier of the message to forward in the source chat | @@ -448,7 +439,6 @@ Copy a message to another Telegram chat without a forward header through the Tel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Destination chat ID \(numeric, can be negative for groups\) | | `fromChatId` | string | Yes | Source chat ID the original message belongs to | | `messageId` | number | Yes | Identifier of the message to copy in the source chat | @@ -474,7 +464,6 @@ Send a point on the map to a Telegram chat through the Telegram Bot API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `latitude` | number | Yes | Latitude of the location | | `longitude` | number | Yes | Longitude of the location | @@ -500,7 +489,6 @@ Send a phone contact to a Telegram chat through the Telegram Bot API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `phoneNumber` | string | Yes | Contact's phone number | | `firstName` | string | Yes | Contact's first name | @@ -528,7 +516,6 @@ Send a native poll to a Telegram chat through the Telegram Bot API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `question` | string | Yes | Poll question \(1-300 characters\) | | `options` | json | Yes | List of 2-10 answer options as text strings | @@ -556,7 +543,6 @@ Pin a message in a Telegram chat through the Telegram Bot API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `messageId` | number | Yes | Identifier of the message to pin | | `disableNotification` | boolean | No | Pass true to pin silently without notifying chat members | @@ -582,7 +568,6 @@ Unpin a pinned message in a Telegram chat through the Telegram Bot API. Unpins t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `messageId` | number | No | Identifier of the message to unpin \(omit to unpin the most recent one\) | @@ -607,7 +592,6 @@ Set or remove an emoji reaction on a message in a Telegram chat through the Tele | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `messageId` | number | Yes | Identifier of the target message | | `reaction` | string | No | Emoji to react with \(leave empty to remove the reaction\) | @@ -634,7 +618,6 @@ Show a status action such as a typing indicator in a Telegram chat through the T | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID \(numeric, can be negative for groups\) | | `action` | string | Yes | Type of action to broadcast \(e.g. typing, upload_photo, record_video, upload_document, find_location\) | @@ -659,7 +642,6 @@ Get up-to-date information about a Telegram chat through the Telegram Bot API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID or @username \(numeric, can be negative for groups\) | #### Output @@ -691,7 +673,6 @@ Get information about a member of a Telegram chat through the Telegram Bot API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `botToken` | string | Yes | Your Telegram Bot API Token | | `chatId` | string | Yes | Telegram chat ID or @username \(numeric, can be negative for groups\) | | `userId` | number | Yes | Unique identifier of the target user | diff --git a/apps/docs/content/docs/integrations/textract.mdx b/apps/docs/content/docs/integrations/textract.mdx index f00beb21805..c2fce26bce1 100644 --- a/apps/docs/content/docs/integrations/textract.mdx +++ b/apps/docs/content/docs/integrations/textract.mdx @@ -2,7 +2,7 @@ title: AWS Textract description: Extract text, tables, and forms from documents integration: - type: "textract_v2" + type: "textract_v3" color: "linear-gradient(135deg, #055F4E 0%, #56C0A7 100%)" --- @@ -10,7 +10,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [AWS Textract](https://aws.amazon.com/textract/) to extract text, handwriting, tables, forms, key-value pairs, and supported document-analysis features. Expense and identity-document actions return their corresponding structured results. +Use [AWS Textract](https://aws.amazon.com/textract/) to extract text, handwriting, tables, forms, key-value pairs, and supported document-analysis features. Expense and identity-document actions return their corresponding structured results. Connect with a saved [AWS credential](/integrations/aws-credentials). Single-page parsing supports JPEG, PNG, and single-page PDF supplied as a file or URL. Multi-page parsing supports PDF and TIFF stored in S3; provide the document's `s3://bucket/key` URI. Choose the document mode supported by the selected action. {/* MANUAL-CONTENT-END */} @@ -31,8 +31,6 @@ Parse documents using AWS Textract OCR and document analysis | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | AWS Access Key ID | -| `secretAccessKey` | string | Yes | AWS Secret Access Key | | `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) | | `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. | | `file` | file | No | Document to be processed \(JPEG, PNG, or single-page PDF\). | @@ -89,8 +87,6 @@ Extract structured invoice and receipt fields using AWS Textract AnalyzeExpense | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | AWS Access Key ID | -| `secretAccessKey` | string | Yes | AWS Secret Access Key | | `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) | | `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. | | `file` | file | No | Invoice or receipt to be processed \(JPEG, PNG, or single-page PDF\). | @@ -124,8 +120,6 @@ Extract identity document fields using AWS Textract AnalyzeID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | AWS Access Key ID | -| `secretAccessKey` | string | Yes | AWS Secret Access Key | | `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) | | `file` | file | No | Front of the identity document \(JPEG, PNG, or PDF\). | | `filePath` | string | No | URL to the front of the identity document, if not uploaded directly. | diff --git a/apps/docs/content/docs/integrations/thrive.mdx b/apps/docs/content/docs/integrations/thrive.mdx index 6e99a230d23..b261f1884a6 100644 --- a/apps/docs/content/docs/integrations/thrive.mdx +++ b/apps/docs/content/docs/integrations/thrive.mdx @@ -2,7 +2,7 @@ title: Thrive description: Manage users, audiences, learning and CPD on Thrive integration: - type: "thrive" + type: "thrive_v2" color: "#FFFFFF" --- @@ -29,9 +29,6 @@ Create a new user in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `ref` | string | Yes | Your organisation's unique identifier for this individual | | `firstName` | string | Yes | The given name of the individual | | `lastName` | string | Yes | The family name of the individual | @@ -85,9 +82,6 @@ Update an existing user in Thrive by ref. Only the fields provided are changed. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `ref` | string | Yes | The user ref to update | | `firstName` | string | No | The given name of the individual | | `lastName` | string | No | The family name of the individual | @@ -141,9 +135,6 @@ Permanently delete (obfuscate) a user in Thrive by ref while retaining training | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `ref` | string | Yes | The user ref to delete | #### Output @@ -164,9 +155,6 @@ Suspend a user in Thrive by ref, marking the account inactive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `ref` | string | Yes | The user ref to suspend | | `endDate` | string | No | The date this individual left your organisation \(ISO 8601\) | @@ -207,9 +195,6 @@ Search users in Thrive and return basic user information with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `page` | number | No | Page number for pagination \(default 1\) | | `perPage` | number | No | Number of results per page \(1-1000, default 100\) | | `updatedSince` | string | No | Return only users updated on or after this date/time \(ISO 8601\) | @@ -280,9 +265,6 @@ Get a single user in Thrive by their ID and return basic user information. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `id` | string | Yes | The user ID | #### Output @@ -343,9 +325,6 @@ Get a single user in Thrive by their ref and return basic user information. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `ref` | string | Yes | The user ref | #### Output @@ -387,9 +366,6 @@ List audiences and structures in Thrive with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `apiControlled` | boolean | No | Filter to only return audiences which are / are not API controlled | | `updatedSince` | string | No | Return only audiences updated on or after this date/time \(ISO 8601\) | | `page` | number | No | Page number for pagination \(default 1\) | @@ -430,9 +406,6 @@ Create a new audience or structure in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `name` | string | No | The name of the audience \(max 100 characters\) | | `reference` | string | No | The external reference for the audience \(max 100 characters\) | | `parentId` | string | No | The id or reference of the parent audience/structure; leave blank for a parent audience/structure | @@ -468,9 +441,6 @@ Get a single audience or structure in Thrive by id or reference. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | #### Output @@ -503,9 +473,6 @@ Update an audience in Thrive, optionally moving it to a new parent. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | | `name` | string | No | The name of the audience \(max 100 characters\) | | `reference` | string | No | The external reference for the audience \(max 100 characters\) | @@ -541,9 +508,6 @@ Delete an audience in Thrive (only if it has no child audiences). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | #### Output @@ -564,9 +528,6 @@ List the members of a Thrive audience with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | | `page` | number | No | Page number for pagination \(default 1\) | | `perPage` | number | No | Number of results per page \(1-1000, default 100\) | @@ -597,9 +558,6 @@ Add members to a Thrive audience by email, ref, or id. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | | `users` | string | Yes | JSON array of user emails/refs/ids to add \(1-100\). Example: \["user@example.com"\] | @@ -630,9 +588,6 @@ Replace a Thrive audience's entire members list with the given users (does not s | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | | `users` | string | Yes | JSON array of user emails/refs/ids that replaces the whole members list \(1-100, no empty array\). Example: \["user@example.com"\] | @@ -663,9 +618,6 @@ Remove a single member from a Thrive audience. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | | `userRef` | string | Yes | The user email, ref, or id to remove | @@ -687,9 +639,6 @@ List the managers of a Thrive audience. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | #### Output @@ -717,9 +666,6 @@ Add managers to a Thrive audience with their permissions. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | | `managers` | string | Yes | JSON array of manager objects \(1-100\). Each: \{"reference":"user@example.com","permissions":\{"audienceManager":\{"manageContent":true,"assignments":true\},"peopleManager":\{"canViewLearnPage":true,"insights":false,"manage":false\},"administrator":\{"canAddAudienceManagers":false\}\}\} | @@ -750,9 +696,6 @@ Replace a Thrive audience's entire manager list with the given managers (does no | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | | `managers` | string | Yes | JSON array of manager objects that replaces the whole manager list \(1-100, no empty array\). Each: \{"reference":"user@example.com","permissions":\{"audienceManager":\{"manageContent":true,"assignments":true\},"peopleManager":\{"canViewLearnPage":true,"insights":false,"manage":false\},"administrator":\{"canAddAudienceManagers":false\}\}\} | @@ -783,9 +726,6 @@ Remove a single manager from a Thrive audience. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience id or audience reference | | `userId` | string | Yes | The user email, ref, or id to remove as a manager | @@ -807,9 +747,6 @@ List compliance assignments in Thrive, optionally filtered by audience. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | No | Filter by audience ID or audience reference | | `updatedSince` | string | No | Return only items updated on or after this date/time \(ISO 8601\) | | `page` | number | No | Page number for pagination \(default 1\) | @@ -845,9 +782,6 @@ Create a compliance assignment in Thrive for an audience and content item. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceId` | string | Yes | The audience ID | | `contentId` | string | Yes | The content ID for the primary content | | `alternativeContentIds` | string | No | JSON array of content IDs that can also complete the assignment | @@ -885,9 +819,6 @@ Get a single compliance assignment in Thrive by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `assignmentId` | string | Yes | The assignment ID | #### Output @@ -920,9 +851,6 @@ Update a compliance assignment in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `assignmentId` | string | Yes | The assignment ID | | `audienceId` | string | Yes | The audience ID | | `contentId` | string | No | The content ID for the primary content | @@ -960,9 +888,6 @@ Delete a compliance assignment in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `assignmentId` | string | Yes | The assignment ID | | `audienceId` | string | Yes | The audience ID | @@ -984,9 +909,6 @@ List enrolments for a compliance assignment in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `assignmentId` | string | Yes | The assignment ID | | `updatedAtFrom` | string | No | Start date to filter enrolments from \(ISO 8601\) | | `updatedAtTo` | string | No | Date to filter enrolments up to \(ISO 8601\). Requires updatedAtFrom. | @@ -1029,9 +951,6 @@ Get a single enrolment for a compliance assignment in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `assignmentId` | string | Yes | The assignment ID | | `enrolmentId` | string | Yes | The enrolment ID | @@ -1070,9 +989,6 @@ List learning completion records in Thrive, optionally filtered by user or conte | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `contentId` | string | No | Filter by content | | `isRPL` | boolean | No | Filter by completions imported via Recognition of Prior Learning \(RPL\) | | `userId` | string | No | Filter by user | @@ -1109,9 +1025,6 @@ Get a single learning completion record in Thrive by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `id` | string | Yes | The completion ID | #### Output @@ -1142,9 +1055,6 @@ Record a learning completion in Thrive for a user and content item. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `userId` | string | Yes | The user ID | | `contentId` | string | Yes | The content ID for the content completed | | `completedAt` | string | Yes | ISO8601 timestamp when the completion occurred | @@ -1167,9 +1077,6 @@ Get a single content record in Thrive by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `id` | string | Yes | Unique identifier of the content item | #### Output @@ -1208,9 +1115,6 @@ Query content records in Thrive with pagination and filtering options. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `page` | number | No | Page number for pagination \(default 1\) | | `perPage` | number | No | Number of results per page \(1-1000, default 20\) | | `types` | string | No | Comma-separated content types \(article, assessment, broadcast, cmi5, elearning, event, file, pathway, question, quiz, scorm, url, video, mixed\). If both set, omitTypes is ignored. | @@ -1258,9 +1162,6 @@ Get a single activity record in Thrive by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `id` | string | Yes | Unique identifier of the activity | #### Output @@ -1290,9 +1191,6 @@ Query activity records in Thrive with pagination and filtering options. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `page` | number | No | Page number for pagination \(default 1\) | | `perPage` | number | No | Number of results per page \(1-1000, default 20\) | | `actions` | string | No | comma-separated activity types e.g. viewed,completed | @@ -1334,9 +1232,6 @@ Get a single CPD category in Thrive by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `categoryId` | string | Yes | The CPD category ID | #### Output @@ -1359,9 +1254,6 @@ Query CPD categories in Thrive and return results with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `page` | number | No | Page number for pagination \(default 1\) | | `perPage` | number | No | Number of results per page \(1-1000, default 100\) | | `updatedSince` | string | No | Return only items updated on or after this date/time \(ISO 8601\) | @@ -1391,9 +1283,6 @@ Get a single CPD log entry in Thrive by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `logEntryId` | string | Yes | The CPD log entry ID | #### Output @@ -1426,9 +1315,6 @@ Query CPD log entries in Thrive and return results with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `page` | number | No | Page number for pagination \(default 1\) | | `perPage` | number | No | Number of results per page \(1-1000, default 100\) | | `entryDateFrom` | string | No | Filter entries after this date \(format YYYY-MM-DD hh:mm:ss\) | @@ -1469,9 +1355,6 @@ Get a single CPD requirement summary in Thrive by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `audienceRequirementId` | string | Yes | The CPD requirement ID | #### Output @@ -1497,9 +1380,6 @@ Query CPD requirement summaries in Thrive and return results with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `page` | number | No | Page number for pagination \(default 1\) | | `perPage` | number | No | Number of results per page \(1-1000, default 100\) | | `updatedSince` | string | No | Return only items updated on or after this date/time \(ISO 8601\) | @@ -1532,9 +1412,6 @@ Query CPD user log summaries in Thrive and return results with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `entryDateFrom` | string | Yes | Filter entries after this date \(format YYYY-MM-DDThh:mm:ss\) | | `entryDateTo` | string | Yes | Filter entries before this date \(format YYYY-MM-DDThh:mm:ss\) | | `userIds` | string | No | Comma-separated user IDs to filter by | @@ -1566,9 +1443,6 @@ List tags in Thrive and return tag information with pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `page` | number | No | Page number for pagination \(default 1\) | | `perPage` | number | No | Number of results per page \(1-1000, default 100\) | | `updatedSince` | string | No | Return only tags updated on or after this date/time \(ISO 8601\) | @@ -1602,9 +1476,6 @@ Get a single tag in Thrive by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `tagId` | string | Yes | The tag ID | #### Output @@ -1631,9 +1502,6 @@ Add one or more tags to a learner in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `userId` | string | Yes | The learner ID | | `tags` | string | Yes | JSON array of tag names to add \(1-100\). Example: \["leadership"\] | @@ -1656,9 +1524,6 @@ Remove one or more tags from a learner in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `userId` | string | Yes | The learner ID | | `tags` | string | Yes | JSON array of tag names to remove \(1-100\). Example: \["leadership"\] | @@ -1681,9 +1546,6 @@ Update skills and levels for a learner in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | | `userId` | string | Yes | The learner ID | | `skills` | string | Yes | JSON array of skill objects \(1-100\). Each: \{"tagName":"leadership","level":1,"targetLevel":3\}. level/targetLevel optional \(min -1\). | @@ -1706,9 +1568,6 @@ Get the available skill levels configured in Thrive. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `tenantId` | string | Yes | Thrive Tenant ID \(used as the Basic auth username\) | -| `apiKey` | string | Yes | Thrive API key \(used as the Basic auth password\) | -| `host` | string | No | Region-specific API host | #### Output diff --git a/apps/docs/content/docs/integrations/tinybird.mdx b/apps/docs/content/docs/integrations/tinybird.mdx index e97300ca143..7424261d15d 100644 --- a/apps/docs/content/docs/integrations/tinybird.mdx +++ b/apps/docs/content/docs/integrations/tinybird.mdx @@ -2,7 +2,7 @@ title: Tinybird description: Send events, query data, and manage Data Sources with Tinybird integration: - type: "tinybird" + type: "tinybird_v2" color: "#2EF598" --- @@ -29,13 +29,11 @@ Send events to a Tinybird Data Source using the Events API. Supports JSON and ND | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co or https://api.us-east.tinybird.co) | | `datasource` | string | Yes | Name of the Tinybird Data Source to send events to. Example: "events_raw", "user_analytics" | | `data` | string | Yes | Data to send as NDJSON \(newline-delimited JSON\) or JSON string. Each event should be a valid JSON object. Example NDJSON: \{"user_id": 1, "event": "click"\}\n\{"user_id": 2, "event": "view"\} | | `wait` | boolean | No | Wait for database acknowledgment before responding. Enables safer retries but introduces latency. Defaults to false. | | `format` | string | No | Format of the events data: "ndjson" \(default\) or "json" | | `compression` | string | No | Compression format: "none" \(default\) or "gzip" | -| `token` | string | Yes | Tinybird API Token with DATASOURCES:APPEND or DATASOURCES:CREATE scope | #### Output @@ -56,10 +54,8 @@ Execute SQL queries against Tinybird Pipes and Data Sources using the Query API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co) | | `query` | string | Yes | SQL query to execute. Specify your desired output format \(e.g., FORMAT JSON, FORMAT CSV, FORMAT TSV\). JSON format provides structured data, while other formats return raw text. Example: "SELECT * FROM my_datasource LIMIT 100 FORMAT JSON" | | `pipeline` | string | No | Optional pipe name. When provided, enables SELECT * FROM _ syntax. Example: "my_pipe", "analytics_pipe" | -| `token` | string | Yes | Tinybird API Token with PIPES:READ scope | #### Output @@ -85,11 +81,9 @@ Call a published Tinybird Pipe API Endpoint by name, passing dynamic parameters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co) | | `pipe` | string | Yes | Name of the published Pipe API Endpoint to call. Example: "top_pages" | | `parameters` | json | No | Dynamic Pipe parameters as a JSON object, sent as query-string arguments. Example: \{"start_date": "2024-01-01", "limit": 10\} | | `q` | string | No | Optional SQL to run on top of the Pipe result. Use "_" to reference the Pipe. Example: "SELECT count\(\) FROM _" | -| `token` | string | Yes | Tinybird API Token with PIPES:READ scope | #### Output @@ -118,11 +112,9 @@ Append data to a Tinybird Data Source from a remote file URL (CSV, NDJSON, Parqu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co) | | `datasource` | string | Yes | Name of the existing Data Source to append to. Example: "events_raw" | | `url` | string | Yes | Publicly accessible URL of the file to append. Example: "https://example.com/data.csv" | | `format` | string | No | Format of the source file: "csv" \(default\), "ndjson", or "parquet" | -| `token` | string | Yes | Tinybird API Token with DATASOURCES:APPEND or DATASOURCES:CREATE scope | #### Output @@ -148,9 +140,7 @@ Delete all rows from a Tinybird Data Source. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co) | | `datasource` | string | Yes | Name of the Data Source to truncate. Example: "events_raw" | -| `token` | string | Yes | Tinybird API Token with DATASOURCES:CREATE scope | #### Output @@ -171,11 +161,9 @@ Delete rows from a Tinybird Data Source matching a SQL condition. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co) | | `datasource` | string | Yes | Name of the Data Source to delete rows from. Example: "events_raw" | | `delete_condition` | string | Yes | SQL WHERE-clause condition selecting the rows to delete. Example: "country = 'ES'" or "event_date < '2024-01-01'" | | `dry_run` | boolean | No | When true, returns how many rows would be deleted without deleting them. Defaults to false. | -| `token` | string | Yes | Tinybird API Token with DATASOURCES:CREATE scope | #### Output @@ -200,9 +188,7 @@ Check the status of an asynchronous Tinybird job (import, delete, etc.) by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co) | | `job_id` | string | Yes | ID of the job to check, as returned by an append or delete operation | -| `token` | string | Yes | Tinybird API Token with ADMIN scope, or the token that started the job | #### Output diff --git a/apps/docs/content/docs/integrations/trigger_dev.mdx b/apps/docs/content/docs/integrations/trigger_dev.mdx index 0d71dec6a5a..0f46aad2caf 100644 --- a/apps/docs/content/docs/integrations/trigger_dev.mdx +++ b/apps/docs/content/docs/integrations/trigger_dev.mdx @@ -2,7 +2,7 @@ title: Trigger.dev description: Trigger tasks and manage runs and schedules integration: - type: "trigger_dev" + type: "trigger_dev_v2" color: "#000000" --- @@ -33,7 +33,6 @@ Trigger a Trigger.dev task by its identifier with an optional JSON payload. Retu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `taskIdentifier` | string | Yes | Identifier of the task to trigger \(e.g., "send-welcome-email"\) | | `payload` | json | No | JSON payload passed to the task run. Example: \{"userId": "user_123"\} | | `idempotencyKey` | string | No | Idempotency key that ensures the task is only triggered once per key | @@ -62,7 +61,6 @@ Batch trigger a Trigger.dev task with up to 1,000 payloads. All items in the bat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `taskIdentifier` | string | Yes | Identifier of the task to batch trigger \(e.g., "send-welcome-email"\) | | `items` | json | Yes | JSON array of batch items \(max 1,000\). Each item is an object with a "payload" and optional "options" \(queue, concurrencyKey, idempotencyKey, ttl, delay, tags, machine\). Example: \[\{"payload": \{"userId": "user_1"\}\}, \{"payload": \{"userId": "user_2"\}, "options": \{"delay": "1h"\}\}\] | @@ -85,7 +83,6 @@ Retrieve a Trigger.dev batch by its ID, including its status, run IDs, and succe | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `batchId` | string | Yes | ID of the batch to retrieve \(starts with batch_\) | #### Output @@ -119,7 +116,6 @@ Retrieve the execution results of every run in a Trigger.dev batch, including ou | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `batchId` | string | Yes | ID of the batch to retrieve results for \(starts with batch_\) | #### Output @@ -141,7 +137,6 @@ Retrieve a Trigger.dev run by its ID, including status, payload, output, attempt | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `runId` | string | Yes | ID of the run to retrieve \(starts with run_\) | #### Output @@ -222,7 +217,6 @@ Retrieve the result of a Trigger.dev run: whether it succeeded, its output, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `runId` | string | Yes | ID of the run to retrieve the result for \(starts with run_\) | #### Output @@ -303,7 +297,6 @@ Retrieve the log and span events of a Trigger.dev run, including messages, level | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `runId` | string | Yes | ID of the run to retrieve events for \(starts with run_\) | #### Output @@ -340,7 +333,6 @@ Retrieve the OpenTelemetry trace of a Trigger.dev run as a tree of spans with ti | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `runId` | string | Yes | ID of the run to retrieve the trace for \(starts with run_\) | #### Output @@ -362,7 +354,6 @@ List Trigger.dev runs in the environment of the API key, with optional filters f | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `status` | string | No | Comma-separated run statuses to filter by: PENDING_VERSION, DELAYED, QUEUED, EXECUTING, REATTEMPTING, FROZEN, COMPLETED, CANCELED, FAILED, CRASHED, INTERRUPTED, SYSTEM_FAILURE | | `taskIdentifier` | string | No | Comma-separated task identifiers to filter by | | `version` | string | No | Comma-separated worker versions to filter by \(e.g., "20240101.1"\) | @@ -397,7 +388,6 @@ Cancel an in-progress Trigger.dev run. Has no effect if the run is already compl | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `runId` | string | Yes | ID of the run to cancel \(starts with run_\) | #### Output @@ -418,7 +408,6 @@ Replay a Trigger.dev run, creating a new run with the same payload and options a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `runId` | string | Yes | ID of the run to replay \(starts with run_\) | #### Output @@ -439,7 +428,6 @@ Reschedule a delayed Trigger.dev run with a new delay. Only valid while the run | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `runId` | string | Yes | ID of the delayed run to reschedule \(starts with run_\) | | `delay` | string | Yes | New delay for the run, as a duration \("30m", "1h", "2d"\) or an ISO 8601 date to delay until | @@ -521,7 +509,6 @@ Add tags to an existing Trigger.dev run. Runs can have up to 10 tags. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `runId` | string | Yes | ID of the run to tag \(starts with run_\) | | `tags` | string | Yes | Comma-separated tags to add to the run \(max 10 total, each under 128 characters\) | @@ -543,7 +530,6 @@ Replace the metadata of a Trigger.dev run with a new JSON object. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `runId` | string | Yes | ID of the run to update \(starts with run_\) | | `metadata` | json | Yes | JSON object to set as the run metadata. Example: \{"stage": "approved"\} | @@ -565,7 +551,6 @@ Create an imperative cron schedule that triggers a Trigger.dev task on a recurri | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `task` | string | Yes | Identifier of the task to schedule \(e.g., "daily-report"\) | | `cron` | string | Yes | Cron expression defining when the task runs \(e.g., "0 0 * * *"\) | | `timezone` | string | No | IANA timezone the cron expression is evaluated in \(e.g., "America/New_York"\). Defaults to UTC | @@ -650,7 +635,6 @@ Retrieve a Trigger.dev schedule by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `scheduleId` | string | Yes | ID of the schedule to retrieve \(starts with sched_\) | #### Output @@ -731,7 +715,6 @@ List Trigger.dev schedules in the project, with page-based pagination. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `page` | number | No | Page number to return \(default 1\) | | `perPage` | number | No | Number of schedules per page | @@ -771,7 +754,6 @@ Update an imperative Trigger.dev schedule by its ID, replacing its task, cron ex | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `scheduleId` | string | Yes | ID of the schedule to update \(starts with sched_\) | | `task` | string | Yes | Identifier of the task the schedule triggers \(e.g., "daily-report"\) | | `cron` | string | Yes | Cron expression defining when the task runs \(e.g., "0 0 * * *"\) | @@ -856,7 +838,6 @@ Delete an imperative Trigger.dev schedule by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `scheduleId` | string | Yes | ID of the schedule to delete \(starts with sched_\) | #### Output @@ -878,7 +859,6 @@ Activate an imperative Trigger.dev schedule so it resumes triggering its task. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `scheduleId` | string | Yes | ID of the schedule to activate \(starts with sched_\) | #### Output @@ -959,7 +939,6 @@ Deactivate an imperative Trigger.dev schedule so it stops triggering its task. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `scheduleId` | string | Yes | ID of the schedule to deactivate \(starts with sched_\) | #### Output @@ -1040,7 +1019,6 @@ List the environment variables of a Trigger.dev project environment. Values are | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `projectRef` | string | Yes | External ref of the project, from the project settings \(starts with proj_\) | | `environment` | string | Yes | Environment to list variables for: dev, staging, or prod | @@ -1064,7 +1042,6 @@ Create an environment variable in a Trigger.dev project environment. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `projectRef` | string | Yes | External ref of the project, from the project settings \(starts with proj_\) | | `environment` | string | Yes | Environment to create the variable in: dev, staging, or prod | | `name` | string | Yes | Name of the environment variable \(e.g., "SLACK_API_KEY"\) | @@ -1089,7 +1066,6 @@ Retrieve an environment variable from a Trigger.dev project environment. The val | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `projectRef` | string | Yes | External ref of the project, from the project settings \(starts with proj_\) | | `environment` | string | Yes | Environment to read the variable from: dev, staging, or prod | | `name` | string | Yes | Name of the environment variable \(e.g., "SLACK_API_KEY"\) | @@ -1113,7 +1089,6 @@ Update the value of an environment variable in a Trigger.dev project environment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `projectRef` | string | Yes | External ref of the project, from the project settings \(starts with proj_\) | | `environment` | string | Yes | Environment the variable belongs to: dev, staging, or prod | | `name` | string | Yes | Name of the environment variable to update \(e.g., "SLACK_API_KEY"\) | @@ -1138,7 +1113,6 @@ Delete an environment variable from a Trigger.dev project environment. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `projectRef` | string | Yes | External ref of the project, from the project settings \(starts with proj_\) | | `environment` | string | Yes | Environment the variable belongs to: dev, staging, or prod | | `name` | string | Yes | Name of the environment variable to delete \(e.g., "SLACK_API_KEY"\) | @@ -1162,7 +1136,6 @@ Upload multiple environment variables to a Trigger.dev project environment in on | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `projectRef` | string | Yes | External ref of the project, from the project settings \(starts with proj_\) | | `environment` | string | Yes | Environment to upload the variables to: dev, staging, or prod | | `variables` | json | Yes | JSON array of environment variables to upload. Example: \[\{"name": "SLACK_API_KEY", "value": "slack_123"\}\] | @@ -1187,7 +1160,6 @@ Retrieve a Trigger.dev queue by ID, task identifier, or custom queue name, inclu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `queueName` | string | Yes | Queue ID \(starts with queue_\), task identifier, or custom queue name, depending on the queue type | | `queueType` | string | No | How to interpret the queue name: "id" \(default\) for a queue ID, "task" for a task identifier, or "custom" for a custom queue name | @@ -1269,7 +1241,6 @@ List the queues in the environment of the API key, including running and queued | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `page` | number | No | Page number to return \(default 1\) | | `perPage` | number | No | Number of queues per page | @@ -1307,7 +1278,6 @@ Pause a Trigger.dev queue so no new runs start. Runs that are currently executin | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `queueName` | string | Yes | Queue ID \(starts with queue_\), task identifier, or custom queue name, depending on the queue type | | `queueType` | string | No | How to interpret the queue name: "id" \(default\) for a queue ID, "task" for a task identifier, or "custom" for a custom queue name | @@ -1389,7 +1359,6 @@ Resume a paused Trigger.dev queue so new runs can start again. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `queueName` | string | Yes | Queue ID \(starts with queue_\), task identifier, or custom queue name, depending on the queue type | | `queueType` | string | No | How to interpret the queue name: "id" \(default\) for a queue ID, "task" for a task identifier, or "custom" for a custom queue name | @@ -1471,7 +1440,6 @@ Override the concurrency limit of a Trigger.dev queue with a new value. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `queueName` | string | Yes | Queue ID \(starts with queue_\), task identifier, or custom queue name, depending on the queue type | | `queueType` | string | No | How to interpret the queue name: "id" \(default\) for a queue ID, "task" for a task identifier, or "custom" for a custom queue name | | `concurrencyLimit` | number | Yes | New concurrency limit for the queue \(0 to 100000\) | @@ -1554,7 +1522,6 @@ Reset the concurrency limit of a Trigger.dev queue back to its base value, remov | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `queueName` | string | Yes | Queue ID \(starts with queue_\), task identifier, or custom queue name, depending on the queue type | | `queueType` | string | No | How to interpret the queue name: "id" \(default\) for a queue ID, "task" for a task identifier, or "custom" for a custom queue name | @@ -1636,7 +1603,6 @@ List Trigger.dev deployments in the environment of the API key, with optional st | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `status` | string | No | Deployment status to filter by: PENDING, BUILDING, DEPLOYING, DEPLOYED, FAILED, CANCELED, or TIMED_OUT | | `period` | string | No | Only return deployments created in the given period \(e.g., "1h", "7d"\) | | `from` | string | No | Only return deployments created on or after this ISO 8601 timestamp | @@ -1664,7 +1630,6 @@ Retrieve a Trigger.dev deployment by its ID, including its status, version, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `deploymentId` | string | Yes | ID of the deployment to retrieve | #### Output @@ -1745,7 +1710,6 @@ Retrieve the latest Trigger.dev deployment in the environment of the API key. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | #### Output @@ -1825,7 +1789,6 @@ Promote a Trigger.dev deployment version so new runs execute on it (e.g., to rol | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `version` | string | Yes | Deployment version to promote \(e.g., "20250228.1"\) | #### Output @@ -1848,7 +1811,6 @@ Execute a TRQL (SQL-like) query against Trigger.dev run data for reporting and a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `query` | string | Yes | TRQL query to execute \(e.g., "SELECT run_id, status, triggered_at FROM runs WHERE status = 'Failed' LIMIT 10"\) | | `scope` | string | No | Scope of data to query: environment \(default\), project, or organization | | `period` | string | No | Time period shorthand \(e.g., "1h", "7d", "30d"\). Cannot be combined with from/to | @@ -1875,7 +1837,6 @@ Retrieve the TRQL query schema: the tables and columns available for Execute Que | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | #### Output @@ -1905,7 +1866,6 @@ Create a Trigger.dev waitpoint token that a task can wait on until it is complet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `timeout` | string | No | How long before the token times out, as a duration \("30s", "1m", "2h", "3d"\) or an ISO 8601 date | | `idempotencyKey` | string | No | Idempotency key; passing the same key before it expires returns the original token | | `idempotencyKeyTTL` | string | No | How long the idempotency key is valid, as a duration \("30s", "1m", "2h", "3d"\) | @@ -1931,7 +1891,6 @@ Complete a Trigger.dev waitpoint token, resuming any task waiting on it and pass | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `waitpointId` | string | Yes | ID of the waitpoint token to complete \(starts with waitpoint_\) | | `data` | json | No | JSON data passed back to the waiting run as the token result. Example: \{"status": "approved"\} | @@ -1953,7 +1912,6 @@ Retrieve a Trigger.dev waitpoint token by its ID, including its status, timeout, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `waitpointId` | string | Yes | ID of the waitpoint token to retrieve \(starts with waitpoint_\) | #### Output @@ -2034,7 +1992,6 @@ List Trigger.dev waitpoint tokens in the environment of the API key, with option | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `status` | string | No | Waitpoint status to filter by: WAITING, COMPLETED, or TIMED_OUT | | `idempotencyKey` | string | No | Idempotency key to filter by | | `tags` | string | No | Comma-separated tags to filter by | @@ -2066,7 +2023,6 @@ List the IANA timezones supported by Trigger.dev schedules, for use as the timez | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | | `excludeUtc` | string | No | Set to "true" to exclude UTC from the returned timezones | #### Output diff --git a/apps/docs/content/docs/integrations/twilio_sms.mdx b/apps/docs/content/docs/integrations/twilio_sms.mdx index f0d3e00b4b5..39bdd4f6cfb 100644 --- a/apps/docs/content/docs/integrations/twilio_sms.mdx +++ b/apps/docs/content/docs/integrations/twilio_sms.mdx @@ -2,7 +2,7 @@ title: Twilio SMS description: Send SMS messages integration: - type: "twilio_sms" + type: "twilio_sms_v2" color: "#F22F46" --- @@ -31,8 +31,6 @@ Send text messages to single or multiple recipients using the Twilio API. | --------- | ---- | -------- | ----------- | | `phoneNumbers` | string | Yes | Phone numbers to send the message to in E.164 format \(e.g., +15551234567\), separated by newlines | | `message` | string | Yes | Message to send | -| `accountSid` | string | Yes | Twilio Account SID | -| `authToken` | string | Yes | Twilio Auth Token | | `fromNumber` | string | Yes | Twilio phone number to send the message from in E.164 format \(e.g., +15551234567\) | #### Output diff --git a/apps/docs/content/docs/integrations/twilio_voice.mdx b/apps/docs/content/docs/integrations/twilio_voice.mdx index d6b3458e6fe..8e7527ab8ae 100644 --- a/apps/docs/content/docs/integrations/twilio_voice.mdx +++ b/apps/docs/content/docs/integrations/twilio_voice.mdx @@ -2,7 +2,7 @@ title: Twilio Voice description: Make and manage phone calls integration: - type: "twilio_voice" + type: "twilio_voice_v2" color: "#F22F46" --- @@ -35,8 +35,6 @@ Make an outbound phone call using Twilio Voice API. | `twiml` | string | No | TwiML instructions to execute. Use square brackets instead of angle brackets \(e.g., \[Response\]\[Say\]Hello\[/Say\]\[/Response\]\) | | `statusCallback` | string | No | Webhook URL for call status updates | | `statusCallbackMethod` | string | No | HTTP method for status callback \(GET or POST\) | -| `accountSid` | string | Yes | Twilio Account SID | -| `authToken` | string | Yes | Twilio Auth Token | | `record` | boolean | No | Whether to record the call | | `recordingStatusCallback` | string | No | Webhook URL for recording status updates | | `timeout` | number | No | Time to wait for answer before giving up \(seconds, default: 60\) | @@ -69,8 +67,6 @@ Retrieve a list of calls made to and from an account. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accountSid` | string | Yes | Twilio Account SID | -| `authToken` | string | Yes | Twilio Auth Token | | `to` | string | No | Filter by calls to this phone number in E.164 format \(e.g., +14155551234\) | | `from` | string | No | Filter by calls from this phone number in E.164 format \(e.g., +14155559876\) | | `status` | string | No | Filter by call status \(e.g., queued, ringing, in-progress, completed, busy, failed, no-answer, canceled\) | @@ -102,8 +98,6 @@ Retrieve call recording information and transcription (if enabled via TwiML). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `recordingSid` | string | Yes | Recording SID to retrieve \(e.g., RExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\) | -| `accountSid` | string | Yes | Twilio Account SID | -| `authToken` | string | Yes | Twilio Auth Token | #### Output diff --git a/apps/docs/content/docs/integrations/typeform.mdx b/apps/docs/content/docs/integrations/typeform.mdx index 5748dc50521..c7840979332 100644 --- a/apps/docs/content/docs/integrations/typeform.mdx +++ b/apps/docs/content/docs/integrations/typeform.mdx @@ -2,7 +2,7 @@ title: Typeform description: Interact with Typeform integration: - type: "typeform" + type: "typeform_v2" color: "#262627" --- @@ -10,12 +10,12 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} -Use [Typeform](https://www.typeform.com/) in Sim to retrieve form responses, download submitted files, and get form insights. Switch to trigger mode to start a workflow when a form is submitted. The integration requires an API key. +Use [Typeform](https://www.typeform.com/) in Sim to retrieve form responses, download submitted files, and get form insights. Switch to trigger mode to start a workflow when a form is submitted. Actions sign in with a saved Typeform credential (a personal access token with the `forms:read` scope); the trigger takes its own token. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] -Retrieve responses, download files, and get form insights. Use trigger mode to start a workflow when a form is submitted. Requires API Key. +Retrieve responses, download files, and get form insights. Use trigger mode to start a workflow when a form is submitted. ## Actions @@ -30,7 +30,6 @@ Retrieve form responses from Typeform | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `formId` | string | Yes | Typeform form ID \(e.g., "abc123XYZ"\) | -| `apiKey` | string | Yes | Typeform Personal Access Token | | `pageSize` | number | No | Number of responses to retrieve \(e.g., 10, 25, 50\) | | `before` | string | No | Cursor token for fetching the next page of older responses | | `after` | string | No | Cursor token for fetching the next page of newer responses | @@ -63,7 +62,6 @@ Download files uploaded in Typeform responses | `fieldId` | string | Yes | Unique ID of the file upload field | | `filename` | string | Yes | Filename of the uploaded file | | `inline` | boolean | No | Whether to request the file with inline Content-Disposition | -| `apiKey` | string | Yes | Typeform Personal Access Token | #### Output @@ -87,7 +85,6 @@ Retrieve insights and analytics for Typeform forms | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `formId` | string | Yes | Typeform form ID \(e.g., "abc123XYZ"\) | -| `apiKey` | string | Yes | Typeform Personal Access Token | #### Output @@ -128,7 +125,6 @@ Retrieve a list of all forms in your Typeform account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Typeform Personal Access Token | | `search` | string | No | Search query to filter forms by title \(e.g., "Customer Feedback"\) | | `page` | number | No | Page number for pagination \(e.g., 1, 2, 3\) | | `pageSize` | number | No | Number of forms per page \(e.g., 10, 25, 50, max: 200\) | @@ -154,7 +150,6 @@ Retrieve complete details and structure of a specific form | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Typeform Personal Access Token | | `formId` | string | Yes | Form unique identifier \(e.g., "abc123XYZ"\) | #### Output @@ -187,7 +182,6 @@ Create a new form with fields and settings | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Typeform Personal Access Token | | `title` | string | Yes | Form title | | `type` | string | No | Form type \(default: "form"\). Options: "form", "quiz" | | `workspaceId` | string | No | Workspace ID to create the form in \(e.g., "ws_abc123"\) | @@ -222,7 +216,6 @@ Update an existing form using JSON Patch operations | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Typeform Personal Access Token | | `formId` | string | Yes | Form unique identifier to update \(e.g., "abc123XYZ"\) | | `operations` | json | Yes | Array of JSON Patch operations \(RFC 6902\). Each operation needs: op \(add/remove/replace\), path, and value \(for add/replace\) | @@ -244,7 +237,6 @@ Permanently delete a form and all its responses | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Typeform Personal Access Token | | `formId` | string | Yes | Form unique identifier to delete \(e.g., "abc123XYZ"\) | #### Output diff --git a/apps/docs/content/docs/integrations/upstash.mdx b/apps/docs/content/docs/integrations/upstash.mdx index 4e1686b1b7d..26828ab92bd 100644 --- a/apps/docs/content/docs/integrations/upstash.mdx +++ b/apps/docs/content/docs/integrations/upstash.mdx @@ -2,7 +2,7 @@ title: Upstash description: Serverless Redis with Upstash integration: - type: "upstash" + type: "upstash_v2" color: "#181C1E" --- @@ -29,8 +29,6 @@ Get the value of a key from Upstash Redis. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The key to retrieve | #### Output @@ -52,8 +50,6 @@ Set the value of a key in Upstash Redis with an optional expiration time in seco | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The key to set | | `value` | string | Yes | The value to store | | `ex` | number | No | Expiration time in seconds \(optional\) | @@ -77,8 +73,6 @@ Delete a key from Upstash Redis. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The key to delete | #### Output @@ -100,8 +94,6 @@ List keys matching a pattern in Upstash Redis. Defaults to listing all keys (*). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `pattern` | string | No | Pattern to match keys \(e.g., "user:*"\). Defaults to "*" for all keys. | #### Output @@ -124,8 +116,6 @@ Execute an arbitrary Redis command against Upstash Redis. Pass the full command | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `command` | string | Yes | Redis command as a JSON array \(e.g., \["HSET", "myhash", "field1", "value1"\]\) or a simple command string \(e.g., "PING"\) | #### Output @@ -147,8 +137,6 @@ Set a field in a hash stored at a key in Upstash Redis. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The hash key | | `field` | string | Yes | The field name within the hash | | `value` | string | Yes | The value to store in the hash field | @@ -173,8 +161,6 @@ Get the value of a field in a hash stored at a key in Upstash Redis. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The hash key | | `field` | string | Yes | The field name to retrieve | @@ -198,8 +184,6 @@ Get all fields and values of a hash stored at a key in Upstash Redis. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The hash key | #### Output @@ -222,8 +206,6 @@ Atomically increment the integer value of a key by one in Upstash Redis. If the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The key to increment | #### Output @@ -245,8 +227,6 @@ Set a timeout on a key in Upstash Redis. After the timeout, the key is deleted. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The key to set expiration on | | `seconds` | number | Yes | Timeout in seconds | @@ -269,8 +249,6 @@ Get the remaining time to live of a key in Upstash Redis. Returns -1 if the key | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The key to check TTL for | #### Output @@ -292,8 +270,6 @@ Prepend a value to the beginning of a list in Upstash Redis. Creates the list if | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The list key | | `value` | string | Yes | The value to prepend to the list | @@ -316,8 +292,6 @@ Get a range of elements from a list in Upstash Redis. Use 0 and -1 for start and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The list key | | `start` | number | Yes | Start index \(0-based, negative values count from end\) | | `stop` | number | Yes | Stop index \(inclusive, -1 for last element\) | @@ -342,8 +316,6 @@ Check if a key exists in Upstash Redis. Returns true if the key exists, false ot | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The key to check | #### Output @@ -365,8 +337,6 @@ Set the value of a key only if it does not already exist. Returns true if the ke | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The key to set | | `value` | string | Yes | The value to store if the key does not exist | @@ -389,8 +359,6 @@ Increment the integer value of a key by a given amount. Use a negative value to | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `restUrl` | string | Yes | Upstash Redis REST URL | -| `restToken` | string | Yes | Upstash Redis REST Token | | `key` | string | Yes | The key to increment | | `increment` | number | Yes | Amount to increment by \(use negative value to decrement\) | diff --git a/apps/docs/content/docs/integrations/uptimerobot.mdx b/apps/docs/content/docs/integrations/uptimerobot.mdx index 91e1d289e45..8d6c9df01dd 100644 --- a/apps/docs/content/docs/integrations/uptimerobot.mdx +++ b/apps/docs/content/docs/integrations/uptimerobot.mdx @@ -2,7 +2,7 @@ title: UptimeRobot description: Monitor uptime, manage incidents, maintenance windows, and status pages integration: - type: "uptimerobot" + type: "uptimerobot_v2" color: "#111921" --- @@ -31,7 +31,6 @@ List monitors in your UptimeRobot account, with optional filters and pagination | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `limit` | number | No | Number of monitors per page \(1-200, default 50\) | | `status` | string | No | Comma-separated statuses to filter by \(PAUSED, STARTED, UP, LOOKS_DOWN, DOWN\) | | `name` | string | No | Partial friendly-name filter | @@ -96,7 +95,6 @@ Get the details of a single UptimeRobot monitor by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `monitorId` | number | Yes | ID of the monitor to retrieve | #### Output @@ -154,7 +152,6 @@ Create a new monitor in UptimeRobot | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `friendlyName` | string | Yes | Friendly name of the monitor | | `type` | string | Yes | Monitor type: HTTP, KEYWORD, PING, PORT, HEARTBEAT, DNS, API, or UDP | | `url` | string | No | URL or host to monitor \(not required for Heartbeat monitors\) | @@ -235,7 +232,6 @@ Update an existing UptimeRobot monitor. Only the provided fields are changed. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `monitorId` | number | Yes | ID of the monitor to update | | `friendlyName` | string | No | New friendly name | | `url` | string | No | New URL or host to monitor | @@ -314,7 +310,6 @@ Permanently delete an UptimeRobot monitor by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `monitorId` | number | Yes | ID of the monitor to delete | #### Output @@ -336,7 +331,6 @@ Pause an UptimeRobot monitor so it stops running checks | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `monitorId` | number | Yes | ID of the monitor to pause | #### Output @@ -394,7 +388,6 @@ Resume a paused UptimeRobot monitor so it starts running checks again | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `monitorId` | number | Yes | ID of the monitor to start | #### Output @@ -452,7 +445,6 @@ List incidents across your UptimeRobot account (last 24 hours by default), with | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `monitorId` | number | No | Filter incidents by monitor ID | | `monitorName` | string | No | Filter incidents by monitor name | | `startedAfter` | string | No | Only include incidents started after this ISO 8601 timestamp | @@ -490,7 +482,6 @@ Get the details of a single UptimeRobot incident by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `incidentId` | string | Yes | ID of the incident to retrieve | #### Output @@ -522,7 +513,6 @@ List maintenance windows in your UptimeRobot account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `cursor` | string | No | Pagination cursor returned by a previous request | #### Output @@ -556,7 +546,6 @@ Get the details of a single UptimeRobot maintenance window by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `maintenanceWindowId` | number | Yes | ID of the maintenance window to retrieve | #### Output @@ -589,7 +578,6 @@ Create a new maintenance window to suppress alerts during planned downtime | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `name` | string | Yes | Name of the maintenance window | | `interval` | string | Yes | Recurrence interval: once, daily, weekly, or monthly | | `date` | string | Yes | Start date in YYYY-MM-DD format | @@ -629,7 +617,6 @@ Update an existing maintenance window. Only the provided fields are changed. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `maintenanceWindowId` | number | Yes | ID of the maintenance window to update | | `name` | string | No | New name of the maintenance window | | `interval` | string | No | Recurrence interval: once, daily, weekly, or monthly | @@ -670,7 +657,6 @@ Permanently delete an UptimeRobot maintenance window by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `maintenanceWindowId` | number | Yes | ID of the maintenance window to delete | #### Output @@ -692,7 +678,6 @@ List the personal alert contacts in your UptimeRobot account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `cursor` | number | No | Pagination cursor returned by a previous request | #### Output @@ -722,7 +707,6 @@ Get the details of a single UptimeRobot alert contact by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `alertContactId` | number | Yes | ID of the alert contact to retrieve | #### Output @@ -751,7 +735,6 @@ Create an email alert contact in UptimeRobot. The contact must be confirmed via | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `value` | string | Yes | Email address for the alert contact | | `friendlyName` | string | No | Display name for the alert contact | | `enableNotificationsFor` | number | No | Which monitor events to notify for: 0, 1, 2, or 3 | @@ -782,7 +765,6 @@ Permanently delete an UptimeRobot alert contact by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `alertContactId` | number | Yes | ID of the alert contact to delete | #### Output @@ -804,7 +786,6 @@ List the public status pages in your UptimeRobot account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `cursor` | number | No | Pagination cursor returned by a previous request | #### Output @@ -842,7 +823,6 @@ Get the details of a single UptimeRobot public status page by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `pspId` | number | Yes | ID of the status page to retrieve | #### Output @@ -879,7 +859,6 @@ Create a public status page in UptimeRobot, optionally with a logo and icon imag | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `friendlyName` | string | Yes | Name of the public status page | | `monitorIds` | string | No | Comma-separated monitor IDs to display on the page | | `status` | string | No | Status of the page: ENABLED \(published\) or PAUSED \(unpublished\) | @@ -924,7 +903,6 @@ Update a public status page in UptimeRobot. Only the provided fields are changed | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `pspId` | number | Yes | ID of the status page to update | | `friendlyName` | string | No | New name of the public status page | | `monitorIds` | string | No | Comma-separated monitor IDs to display on the page | @@ -970,7 +948,6 @@ Permanently delete an UptimeRobot public status page by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | | `pspId` | number | Yes | ID of the status page to delete | #### Output @@ -992,7 +969,6 @@ Get details about the authenticated UptimeRobot account, including plan and limi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | UptimeRobot API key | #### Output diff --git a/apps/docs/content/docs/integrations/vercel.mdx b/apps/docs/content/docs/integrations/vercel.mdx index 86904e29cbc..97878eafb04 100644 --- a/apps/docs/content/docs/integrations/vercel.mdx +++ b/apps/docs/content/docs/integrations/vercel.mdx @@ -2,7 +2,7 @@ title: Vercel description: Manage Vercel deployments, projects, and infrastructure integration: - type: "vercel" + type: "vercel_v2" color: "#171717" --- @@ -29,7 +29,6 @@ List deployments for a Vercel project or team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | No | Filter deployments by project ID or name | | `target` | string | No | Filter by environment: production or staging | | `state` | string | No | Filter by state: BUILDING, ERROR, INITIALIZING, QUEUED, READY, CANCELED, BLOCKED | @@ -77,7 +76,6 @@ Get details of a specific Vercel deployment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | The unique deployment identifier or hostname | | `withGitRepoInfo` | string | No | Whether to add in gitRepo information \(true/false\) | | `teamId` | string | No | Team ID to scope the request | @@ -136,7 +134,6 @@ Create a new deployment or redeploy an existing one | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `name` | string | Yes | Project name for the deployment | | `project` | string | No | Project ID \(overrides name for project lookup\) | | `deploymentId` | string | No | Existing deployment ID to redeploy | @@ -175,7 +172,6 @@ Cancel a running Vercel deployment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | The deployment ID to cancel | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -204,7 +200,6 @@ Delete a Vercel deployment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | The deployment ID or URL to delete | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -228,7 +223,6 @@ Get build and runtime events for a Vercel deployment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | The unique deployment identifier or hostname | | `direction` | string | No | Order of events by timestamp: backward or forward \(default: forward\) | | `follow` | number | No | When set to 1, returns live events as they happen | @@ -266,7 +260,6 @@ List files in a Vercel deployment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | The deployment ID to list files for | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -299,7 +292,6 @@ Promote a deployment by pointing the production deployment to the given deployme | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `deploymentId` | string | Yes | The ID of the deployment to promote to production | | `teamId` | string | No | Team ID to scope the request | @@ -323,7 +315,6 @@ List all projects in a Vercel team or account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `search` | string | No | Search projects by name | | `limit` | number | No | Maximum number of projects to return | | `from` | string | No | Continuation token for pagination, taken from the previous response's pagination.next value. Query only projects updated after this timestamp or continuation token. | @@ -358,7 +349,6 @@ Get details of a specific Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -391,7 +381,6 @@ Create a new Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `name` | string | Yes | Project name | | `framework` | string | No | Project framework \(e.g. nextjs, remix, vite\) | | `gitRepository` | json | No | Git repository connection object with type and repo | @@ -426,7 +415,6 @@ Update an existing Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `name` | string | No | New project name | | `framework` | string | No | Project framework \(e.g. nextjs, remix, vite\) | @@ -460,7 +448,6 @@ Delete a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -483,7 +470,6 @@ Pause a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -508,7 +494,6 @@ Unpause a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -533,7 +518,6 @@ List all domains for a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -573,7 +557,6 @@ Add a domain to a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `domain` | string | Yes | Domain name to add | | `redirect` | string | No | Target domain for redirect | @@ -613,7 +596,6 @@ Remove a domain from a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `domain` | string | Yes | Domain name to remove | | `teamId` | string | No | Team ID to scope the request | @@ -637,7 +619,6 @@ Update a project domain's configuration on Vercel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `domain` | string | Yes | Domain name to update | | `redirect` | string | No | Target destination domain for redirect | @@ -677,7 +658,6 @@ Verify a Vercel project domain by checking its verification challenge | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `domain` | string | Yes | Domain name to verify | | `teamId` | string | No | Team ID to scope the request | @@ -709,7 +689,6 @@ Retrieve environment variables for a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `decrypt` | boolean | No | If true, decrypted variable values are returned instead of ciphertext | | `gitBranch` | string | No | Filter results to the environment variables for this git branch \(must have target=preview\) | @@ -744,7 +723,6 @@ Create an environment variable for a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `key` | string | Yes | Environment variable name | | `value` | string | Yes | Environment variable value | @@ -781,7 +759,6 @@ Update an environment variable for a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `envId` | string | Yes | Environment variable ID to update | | `key` | string | No | New variable name | @@ -819,7 +796,6 @@ Delete an environment variable from a Vercel project | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | Yes | Project ID or name | | `envId` | string | Yes | Environment variable ID to delete | | `teamId` | string | No | Team ID to scope the request | @@ -843,7 +819,6 @@ List all domains in a Vercel account or team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `limit` | number | No | Maximum number of domains to return \(default 20\) | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -887,7 +862,6 @@ Get information about a specific domain in a Vercel account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `domain` | string | Yes | The domain name to retrieve | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -928,7 +902,6 @@ Add a new domain to a Vercel account or team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `name` | string | Yes | The domain name to add | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -966,7 +939,6 @@ Delete a domain from a Vercel account or team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `domain` | string | Yes | The domain name to delete | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -990,7 +962,6 @@ Get the configuration for a domain in a Vercel account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `domain` | string | Yes | The domain name to get configuration for | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -1021,7 +992,6 @@ List all DNS records for a domain in a Vercel account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `domain` | string | Yes | The domain name to list records for | | `limit` | number | No | Maximum number of records to return | | `teamId` | string | No | Team ID to scope the request | @@ -1059,7 +1029,6 @@ Create a DNS record for a domain in a Vercel account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `domain` | string | Yes | The domain name to create the record for | | `recordName` | string | Yes | The subdomain or record name | | `recordType` | string | Yes | DNS record type \(A, AAAA, ALIAS, CAA, CNAME, HTTPS, MX, SRV, TXT, NS\) | @@ -1096,7 +1065,6 @@ Update an existing DNS record for a domain in a Vercel account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `recordId` | string | Yes | The ID of the DNS record to update | | `name` | string | No | The name of the DNS record | | `value` | string | No | The value of the DNS record | @@ -1141,7 +1109,6 @@ Delete a DNS record for a domain in a Vercel account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `domain` | string | Yes | The domain name the record belongs to | | `recordId` | string | Yes | The ID of the DNS record to delete | | `teamId` | string | No | Team ID to scope the request | @@ -1165,7 +1132,6 @@ List aliases for a Vercel project or team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | No | Filter aliases by project ID | | `domain` | string | No | Filter aliases by domain | | `limit` | number | No | Maximum number of aliases to return | @@ -1203,7 +1169,6 @@ Get details about a specific alias by ID or hostname | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `aliasId` | string | Yes | Alias ID or hostname to look up | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -1236,7 +1201,6 @@ Assign an alias (domain/subdomain) to a deployment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | Deployment ID to assign the alias to | | `alias` | string | Yes | The domain or subdomain to assign as an alias | | `redirect` | string | No | Hostname to 307-redirect the alias to instead of serving the deployment directly | @@ -1264,7 +1228,6 @@ Delete an alias by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `aliasId` | string | Yes | Alias ID to delete | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -1287,7 +1250,6 @@ List all Edge Config stores for a team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `teamId` | string | No | Team ID to scope the request | #### Output @@ -1317,7 +1279,6 @@ Get details about a specific Edge Config store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `edgeConfigId` | string | Yes | Edge Config ID to look up | | `teamId` | string | No | Team ID to scope the request | @@ -1346,7 +1307,6 @@ Create a new Edge Config store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `slug` | string | Yes | The name/slug for the new Edge Config | | `teamId` | string | No | Team ID to scope the request | @@ -1375,7 +1335,6 @@ Get all items in an Edge Config store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `edgeConfigId` | string | Yes | Edge Config ID to get items from | | `teamId` | string | No | Team ID to scope the request | @@ -1404,7 +1363,6 @@ Create, update, upsert, or delete items in an Edge Config store | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `edgeConfigId` | string | Yes | Edge Config ID to update items in | | `items` | json | Yes | Array of operations: \[\{operation: "create"\|"update"\|"upsert"\|"delete", key: string, value?: any\}\] | | `teamId` | string | No | Team ID to scope the request | @@ -1427,7 +1385,6 @@ Delete an Edge Config store by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `edgeConfigId` | string | Yes | Edge Config ID to delete | | `teamId` | string | No | Team ID to scope the request | @@ -1449,7 +1406,6 @@ List webhooks for a Vercel project or team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `projectId` | string | No | Filter webhooks by project ID | | `teamId` | string | No | Team ID to scope the request | @@ -1480,7 +1436,6 @@ Get details about a specific Vercel webhook | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `webhookId` | string | Yes | Webhook ID to look up | | `teamId` | string | No | Team ID to scope the request | @@ -1508,7 +1463,6 @@ Create a new webhook for a Vercel team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `url` | string | Yes | Webhook URL \(must be https\) | | `events` | string | Yes | Comma-separated event names to subscribe to | | `projectIds` | string | No | Comma-separated project IDs to scope the webhook to | @@ -1539,7 +1493,6 @@ Delete a webhook from a Vercel team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `webhookId` | string | Yes | The webhook ID to delete | | `teamId` | string | No | Team ID to scope the request | @@ -1561,7 +1514,6 @@ Create a new deployment check | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | Deployment ID to create the check for | | `name` | string | Yes | Name of the check \(max 100 characters\) | | `blocking` | boolean | Yes | Whether the check blocks the deployment | @@ -1605,7 +1557,6 @@ Get details of a specific deployment check | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | Deployment ID the check belongs to | | `checkId` | string | Yes | Check ID to retrieve | | `teamId` | string | No | Team ID to scope the request | @@ -1644,7 +1595,6 @@ List all checks for a deployment | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | Deployment ID to list checks for | | `teamId` | string | No | Team ID to scope the request | | `slug` | string | No | Team slug to scope the request \(alternative to teamId\) | @@ -1684,7 +1634,6 @@ Update an existing deployment check | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | Deployment ID the check belongs to | | `checkId` | string | Yes | Check ID to update | | `name` | string | No | Updated name of the check | @@ -1730,7 +1679,6 @@ Rerequest a deployment check | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `deploymentId` | string | Yes | Deployment ID the check belongs to | | `checkId` | string | Yes | Check ID to rerequest | | `teamId` | string | No | Team ID to scope the request | @@ -1755,7 +1703,6 @@ List all teams in a Vercel account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `limit` | number | No | Maximum number of teams to return | | `since` | number | No | Timestamp in milliseconds to only include teams created since then | | `until` | number | No | Timestamp in milliseconds to only include teams created until then | @@ -1798,7 +1745,6 @@ Get information about a specific Vercel team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `teamId` | string | Yes | The team ID to retrieve | #### Output @@ -1837,7 +1783,6 @@ List all members of a Vercel team | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | | `teamId` | string | Yes | The team ID to list members for | | `limit` | number | No | Maximum number of members to return | | `role` | string | No | Filter by role \(OWNER, MEMBER, DEVELOPER, SECURITY, BILLING, VIEWER, VIEWER_FOR_PLUS, CONTRIBUTOR\) | @@ -1881,7 +1826,6 @@ Get information about the authenticated Vercel user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Vercel Access Token | #### Output diff --git a/apps/docs/content/docs/integrations/whatsapp.mdx b/apps/docs/content/docs/integrations/whatsapp.mdx index 1376f6d7014..73c43736eb5 100644 --- a/apps/docs/content/docs/integrations/whatsapp.mdx +++ b/apps/docs/content/docs/integrations/whatsapp.mdx @@ -2,7 +2,7 @@ title: WhatsApp description: Send WhatsApp messages integration: - type: "whatsapp" + type: "whatsapp_v2" color: "#25D366" --- diff --git a/apps/docs/content/docs/integrations/youcom.mdx b/apps/docs/content/docs/integrations/youcom.mdx index cabdc84d4f2..b591747eab7 100644 --- a/apps/docs/content/docs/integrations/youcom.mdx +++ b/apps/docs/content/docs/integrations/youcom.mdx @@ -2,7 +2,7 @@ title: You.com description: Search, read, and research the web with You.com integration: - type: "youcom" + type: "youcom_v2" color: "#FFFFFF" --- @@ -12,7 +12,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} Use [You.com](https://you.com/) to search the web and news, retrieve page content, get cited answers, or run deep and financial research. Search supports date, country, language, and domain filters; research can return structured fields or run in the background for later status checks. -Image search requires early-access permission. Account Balance reports remaining API credits. Create a key on the [You.com platform](https://you.com/platform) and provide it to the selected operation. +Image search requires early-access permission. Account Balance reports remaining API credits. Create a key on the [You.com platform](https://you.com/platform) and add it as a You.com credential. {/* MANUAL-CONTENT-END */} ## Use the block [#usage-instructions] @@ -46,7 +46,6 @@ Search the web and news with You.com, returning LLM-ready results with optional | `extractionSource` | string | No | Where full_page content comes from: blend \(cache, then live crawl\), cache, or fetch. Defaults to blend | | `extractionFormats` | string | No | Comma-separated full_page formats: markdown, html, or both. Defaults to markdown | | `crawlTimeout` | number | No | Seconds to wait for page content when crawling, from 1 to 60. Defaults to 10 | -| `apiKey` | string | Yes | You.com API key | #### Output @@ -99,7 +98,6 @@ Fetch the HTML, Markdown, and page metadata of one or more web pages with You.co | `formats` | string | No | Comma-separated formats to return: markdown, html, metadata \(JSON-LD and OpenGraph site info\) | | `crawlTimeout` | number | No | Seconds to wait for page content, from 1 to 60. Defaults to 10 | | `maxAge` | number | No | Maximum age in seconds of cached content before the page is re-fetched. Defaults to no limit | -| `apiKey` | string | Yes | You.com API key | #### Output @@ -133,7 +131,6 @@ Get a synthesized answer to a question with numbered citations and the web resul | `includeDomains` | string | No | Comma-separated domains to restrict results to \(up to 500\). Cannot be combined with exclude or boost domains | | `excludeDomains` | string | No | Comma-separated domains to remove from results \(up to 500\) | | `boostDomains` | string | No | Comma-separated domains to rank higher without filtering others \(up to 500\) | -| `apiKey` | string | Yes | You.com API key | #### Output @@ -172,7 +169,6 @@ Run multi-step web research on a question and get a thorough, cited answer, or s | `boostDomains` | string | No | Comma-separated domains to rank higher without filtering others \(up to 500\) | | `freshness` | string | No | Source recency: day, week, month, year, or a range like 2025-01-01to2025-12-31 | | `country` | string | No | Two-letter country code that focuses web results \(e.g., US, GB, DE\) | -| `apiKey` | string | Yes | You.com API key | #### Output @@ -203,7 +199,6 @@ Get the status of a background research task and, once completed, its cited answ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `taskId` | string | Yes | ID of the background research task | -| `apiKey` | string | Yes | You.com API key | #### Output @@ -245,7 +240,6 @@ Research a financial question across earnings reports, SEC filings, analyst cove | --------- | ---- | -------- | ----------- | | `input` | string | Yes | Financial research question \(up to 40,000 characters\) | | `researchEffort` | string | No | Depth of research: deep \(<120s\) or exhaustive \(<300s\). Defaults to deep | -| `apiKey` | string | Yes | You.com API key | #### Output @@ -273,7 +267,6 @@ Find image URLs for a query with You.com. Beta, and limited to You.com early-acc | --------- | ---- | -------- | ----------- | | `query` | string | Yes | Image search query. Supports site:, filetype: \(e.g., filetype:png\), and OR | | `count` | number | No | Maximum number of images to return, from 1 to 100. Defaults to 10 | -| `apiKey` | string | Yes | You.com API key | #### Output @@ -300,7 +293,6 @@ Get the remaining You.com API credit balance for the account or organization tha | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | You.com API key | #### Output diff --git a/apps/docs/content/docs/integrations/youtube.mdx b/apps/docs/content/docs/integrations/youtube.mdx index f3e0c31663e..b555729f028 100644 --- a/apps/docs/content/docs/integrations/youtube.mdx +++ b/apps/docs/content/docs/integrations/youtube.mdx @@ -2,7 +2,7 @@ title: YouTube description: Interact with YouTube videos, channels, and playlists integration: - type: "youtube" + type: "youtube_v2" color: "#FF0000" --- @@ -33,7 +33,6 @@ Get detailed information about a YouTube channel including statistics, branding, | --------- | ---- | -------- | ----------- | | `channelId` | string | No | YouTube channel ID starting with "UC" \(24-character string, use either channelId or username\) | | `username` | string | No | YouTube channel username \(use either channelId or username\) | -| `apiKey` | string | Yes | YouTube API Key | #### Output @@ -68,7 +67,6 @@ Get all public playlists from a specific YouTube channel. | `channelId` | string | Yes | YouTube channel ID starting with "UC" \(24-character string\) to get playlists from | | `maxResults` | number | No | Maximum number of playlists to return \(1-50\) | | `pageToken` | string | No | Page token for pagination \(from previous response nextPageToken\) | -| `apiKey` | string | Yes | YouTube API Key | #### Output @@ -101,7 +99,6 @@ Search for videos from a specific YouTube channel with sorting options. For comp | `maxResults` | number | No | Maximum number of videos to return \(1-50\) | | `order` | string | No | Sort order: "date" \(newest first, default\), "rating", "relevance", "title", "viewCount" | | `pageToken` | string | No | Page token for pagination \(from previous response nextPageToken\) | -| `apiKey` | string | Yes | YouTube API Key | #### Output @@ -133,7 +130,6 @@ Get top-level comments from a YouTube video with author details and engagement. | `maxResults` | number | No | Maximum number of comments to return \(1-100\) | | `order` | string | No | Order of comments: "time" \(newest first\) or "relevance" \(most relevant first\) | | `pageToken` | string | No | Page token for pagination \(from previous response nextPageToken\) | -| `apiKey` | string | Yes | YouTube API Key | #### Output @@ -168,7 +164,6 @@ Get videos from a YouTube playlist. Can be used with a channel uploads playlist | `playlistId` | string | Yes | YouTube playlist ID starting with "PL" or "UU" \(34-character string\). Use uploadsPlaylistId from channel_info to get all channel videos. | | `maxResults` | number | No | Maximum number of videos to return \(1-50\) | | `pageToken` | string | No | Page token for pagination \(from previous response nextPageToken\) | -| `apiKey` | string | Yes | YouTube API Key | #### Output @@ -202,7 +197,6 @@ Search for videos on YouTube using the YouTube Data API. Supports advanced filte | `query` | string | Yes | Search query for YouTube videos | | `maxResults` | number | No | Maximum number of videos to return \(1-50\) | | `pageToken` | string | No | Page token for pagination \(from previous response nextPageToken\) | -| `apiKey` | string | Yes | YouTube API Key | | `channelId` | string | No | Filter results to a specific YouTube channel ID starting with "UC" \(24-character string\) | | `publishedAfter` | string | No | Only return videos published after this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) | | `publishedBefore` | string | No | Only return videos published before this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) | @@ -248,7 +242,6 @@ Get the most popular/trending videos on YouTube. Can filter by region and video | `videoCategoryId` | string | No | Filter by video category ID \(e.g., "10" for Music, "20" for Gaming, "17" for Sports\) | | `maxResults` | number | No | Maximum number of trending videos to return \(1-50\) | | `pageToken` | string | No | Page token for pagination \(from previous response nextPageToken\) | -| `apiKey` | string | Yes | YouTube API Key | #### Output @@ -283,7 +276,6 @@ Get a list of video categories available on YouTube. Use this to discover valid | --------- | ---- | -------- | ----------- | | `regionCode` | string | No | ISO 3166-1 alpha-2 country code to get categories for \(e.g., "US", "GB", "JP"\). Defaults to US. | | `hl` | string | No | Language for category titles \(ISO 639-1 code, e.g., "en", "es", "fr"\). Defaults to English. | -| `apiKey` | string | Yes | YouTube API Key | #### Output @@ -308,7 +300,6 @@ Get detailed information about a specific YouTube video including statistics, co | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `videoId` | string | Yes | YouTube video ID \(11-character string, e.g., "dQw4w9WgXcQ"\) | -| `apiKey` | string | Yes | YouTube API Key | #### Output diff --git a/apps/docs/content/docs/integrations/zep.mdx b/apps/docs/content/docs/integrations/zep.mdx index 04aa27ff008..1e773e11029 100644 --- a/apps/docs/content/docs/integrations/zep.mdx +++ b/apps/docs/content/docs/integrations/zep.mdx @@ -2,7 +2,7 @@ title: Zep description: Long-term memory for AI agents integration: - type: "zep" + type: "zep_v2" color: "#E8E8E8" --- @@ -31,7 +31,6 @@ Start a new conversation thread in Zep | --------- | ---- | -------- | ----------- | | `threadId` | string | Yes | Unique identifier for the thread \(e.g., "thread_abc123"\) | | `userId` | string | Yes | User ID associated with the thread \(e.g., "user_123"\) | -| `apiKey` | string | Yes | Your Zep API key | #### Output @@ -59,7 +58,6 @@ List all conversation threads | `pageNumber` | number | No | Page number for pagination \(e.g., 1, 2, 3\) | | `orderBy` | string | No | Field to order results by \(created_at, updated_at, user_id, thread_id\) | | `asc` | boolean | No | Order direction: true for ascending, false for descending | -| `apiKey` | string | Yes | Your Zep API key | #### Output @@ -89,7 +87,6 @@ Delete a conversation thread from Zep | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `threadId` | string | Yes | Thread ID to delete \(e.g., "thread_abc123"\) | -| `apiKey` | string | Yes | Your Zep API key | #### Output @@ -112,7 +109,6 @@ Retrieve user context from a thread with summary or basic mode | `threadId` | string | Yes | Thread ID to get context from \(e.g., "thread_abc123"\) | | `mode` | string | No | Context mode: "summary" \(natural language\) or "basic" \(raw facts\) | | `minRating` | number | No | Minimum rating by which to filter relevant facts | -| `apiKey` | string | Yes | Your Zep API key | #### Output @@ -136,7 +132,6 @@ Retrieve messages from a thread | `limit` | number | No | Maximum number of messages to return \(e.g., 10, 50, 100\) | | `cursor` | string | No | Cursor for pagination | | `lastn` | number | No | Number of most recent messages to return \(overrides limit and cursor\) | -| `apiKey` | string | Yes | Your Zep API key | #### Output @@ -168,7 +163,6 @@ Add messages to an existing thread | --------- | ---- | -------- | ----------- | | `threadId` | string | Yes | Thread ID to add messages to \(e.g., "thread_abc123"\) | | `messages` | json | Yes | Array of message objects with role and content \(e.g., \[\{"role": "user", "content": "Hello"\}\]\) | -| `apiKey` | string | Yes | Your Zep API key | #### Output @@ -195,7 +189,6 @@ Create a new user in Zep | `firstName` | string | No | User first name | | `lastName` | string | No | User last name | | `metadata` | json | No | Additional metadata as JSON object \(e.g., \{"key": "value"\}\) | -| `apiKey` | string | Yes | Your Zep API key | #### Output @@ -222,7 +215,6 @@ Retrieve user information from Zep | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `userId` | string | Yes | User ID to retrieve \(e.g., "user_123"\) | -| `apiKey` | string | Yes | Your Zep API key | #### Output @@ -251,7 +243,6 @@ List all conversation threads for a specific user | --------- | ---- | -------- | ----------- | | `userId` | string | Yes | User ID to get threads for \(e.g., "user_123"\) | | `limit` | number | No | Maximum number of threads to return \(e.g., 10, 25, 50\) | -| `apiKey` | string | Yes | Your Zep API key | #### Output diff --git a/apps/docs/content/docs/mcp/authentication.mdx b/apps/docs/content/docs/mcp/authentication.mdx index 5208ed723f0..e0976b7e730 100644 --- a/apps/docs/content/docs/mcp/authentication.mdx +++ b/apps/docs/content/docs/mcp/authentication.mdx @@ -55,8 +55,10 @@ A personal key uses your current access in the workspaces you can reach. A works The Cursor process must receive `SIM_API_KEY` for its environment interpolation to work. For a self-hosted deployment, replace the Cloud URL in both examples with your server URL. +For an agent that only reads, create a **Read only** key: it reaches the operations `api:read` allows, and a tool call that changes something or runs a workflow fails with `403` and the code `READ_ONLY_API_KEY`. Search and search chat still use credits. See [key access](/api-reference/authentication#key-access). + - Keys created in Sim settings have no default expiration. Revoke a key when it is no longer needed. Prefer OAuth for an app that + Keys created in Sim settings never expire unless you choose an expiration. Revoke a key when it is no longer needed. Prefer OAuth for an app that supports browser sign-in, and store keys in your app's secret or environment settings rather than in a shared config file. diff --git a/apps/docs/content/docs/platform/self-hosting/environment-variables.mdx b/apps/docs/content/docs/platform/self-hosting/environment-variables.mdx index 4a06caf31d4..9261787a85d 100644 --- a/apps/docs/content/docs/platform/self-hosting/environment-variables.mdx +++ b/apps/docs/content/docs/platform/self-hosting/environment-variables.mdx @@ -158,7 +158,19 @@ Google, GitHub, and Microsoft sign-in, their callback URLs, and the `DISABLE_*_A ## Integration credentials -OAuth integrations require a provider application and its credentials on a self-hosted deployment. API-key integrations use the keys supplied in their blocks. The full table, redirect-URI format, and setup steps are in [Integrations and OAuth](/platform/self-hosting/integrations-oauth). +OAuth integrations require a provider application and its credentials on a self-hosted deployment. API-key integrations use the saved credentials users add under **Integrations**, and need no deployment configuration. The full table, redirect-URI format, and setup steps are in [Integrations and OAuth](/platform/self-hosting/integrations-oauth). + +### Keyless cloud credentials [#workload-identity] + +Saved AWS, Databricks, Snowflake, Microsoft app, and Google Cloud credentials can trust Sim through OpenID Connect instead of storing keys. Leave these unset to offer only key- and secret-based credentials. + +| Variable | Description | +| --- | --- | +| `WORKLOAD_IDENTITY_SIGNING_KEY` | PKCS#8 PEM RSA private key that signs Sim's workload-identity tokens: `openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048` | +| `WORKLOAD_IDENTITY_ISSUER` | Issuer URL clouds trust. Defaults to `/oidc/workload`; must be `https` on the default port, with no query or fragment, and reachable from the public internet, or keyless stays off. Never change it once trust policies reference it. | +| `WORKLOAD_IDENTITY_PREVIOUS_KEY` | A second published key (private or public PEM) that never signs: the incoming key before a rotation, or the retiring key after it | + +See [AWS credentials](/integrations/aws-credentials#self-hosted-deployments) for setup and the rotation steps. ## Access control @@ -243,6 +255,7 @@ Configure at least one. Every configured provider stays active and is tried in o | `EGRESS_ALLOWED_HOSTS` | unset | Comma-separated hostnames outbound requests may reach on a private network. Leading wildcard allowed, e.g. `host.docker.internal,*.svc.cluster.local`. Not honored for URLs harvested from content or a third-party response, nor for an HTTP block's `proxyUrl` | | `EGRESS_ALLOWED_IP_RANGES` | unset | Comma-separated CIDRs or IPs outbound requests may reach on a private network, e.g. `10.0.0.0/8`. Same exclusions | | `ALLOW_PRIVATE_DATABASE_HOSTS` | unset | **Deprecated.** Vouches for the entire private address space, for database, cache, and mail connector hosts only. Replace it with the two settings above naming specific destinations | +| `DOMAIN_VERIFICATION_NAMESERVERS` | `1.1.1.1,8.8.8.8` | Comma-separated nameserver IPs that SSO domain verification sends its TXT lookup to. Set it when the network only allows DNS to an internal resolver. Entries that are not IP addresses are ignored | A malformed entry in either allowlist stops the app at startup with a message naming the setting. See [the SSRF boundary](/platform/self-hosting/security#the-ssrf-boundary) for the accepted syntax and for what the allowlist does and does not reach. diff --git a/apps/docs/content/docs/platform/self-hosting/networking.mdx b/apps/docs/content/docs/platform/self-hosting/networking.mdx index 5912e1a4910..f929a7f665b 100644 --- a/apps/docs/content/docs/platform/self-hosting/networking.mdx +++ b/apps/docs/content/docs/platform/self-hosting/networking.mdx @@ -265,7 +265,7 @@ Most `/api` requests also pass through Next.js Proxy. The current app configurat The app makes outbound calls to model providers, integration APIs, your email provider, object storage, and your telemetry backend. Whether `HTTP_PROXY` / `HTTPS_PROXY` apply depends on which of those paths a call takes — there is no single answer, and no global setting that covers all of them. -The server runs on Bun, and Bun's native `fetch` honors `$HTTP_PROXY`, `$HTTPS_PROXY`, and `$NO_PROXY`. Sim installs no global dispatcher. Default `fetch` uses the configured proxy unless `NO_PROXY` excludes the destination. The rest either build their own HTTP agent or speak a non-HTTP protocol. +The server runs on Bun, and Bun's native `fetch` honors `$HTTP_PROXY`, `$HTTPS_PROXY`, and `$NO_PROXY`. Sim installs no global dispatcher. Default `fetch` uses the configured proxy unless `NO_PROXY` excludes the destination. The SSRF guard reads the same three variables for every request it sends, matching `NO_PROXY` with the rules in [Internal destinations and `NO_PROXY`](#internal-destinations-and-no_proxy). The rest either build their own HTTP agent or speak a non-HTTP protocol. This depends on the runtime. The published images run Bun. If you build the standalone output and run it under Node instead, Node ignores these variables unless started with `NODE_USE_ENV_PROXY=1` (Node 22.21+ / 24.5+), and the `fetch`-based "Yes" rows stop being proxied — model providers, Resend, Gmail sending, the desktop update feed, and the telemetry relay. The Azure Blob, GCS and Azure Communication Services rows still hold: those SDKs read the proxy variables through their own agents rather than through `fetch`. See [Bun fetch](https://bun.sh/docs/runtime/networking/fetch) and [Node proxy configuration](https://nodejs.org/learn/http/enterprise-network-configuration). @@ -275,20 +275,33 @@ This depends on the runtime. The published images run Bun. If you build the stan | Email via Resend, Azure Communication Services, and Gmail sending | Yes | | The desktop update feed's calls to GitHub | Yes | | Object storage — Azure Blob and GCS | Yes — their SDK pipelines read the proxy variables | -| Everything through the SSRF guard — the HTTP block, tools, connectors, outbound webhooks, content fetches, MCP servers | No | -| Azure OpenAI, Azure Anthropic, vLLM | Only when the endpoint comes from `AZURE_OPENAI_ENDPOINT`, `AZURE_ANTHROPIC_ENDPOINT`, or `VLLM_BASE_URL`. An endpoint typed into the block is validated and pinned to its resolved IP, which bypasses the proxy | -| Amazon Bedrock, and object storage on S3 | No — the AWS SDK uses its own request handler | -| Email via SMTP | No — Nodemailer opens a raw TCP connection | -| Email via Amazon SES | No — the AWS SDK transport, over HTTPS | -| OTLP export from the server SDK | No | +| Everything through the SSRF guard — SSO OIDC discovery, the HTTP block, tools, connectors, outbound webhooks, content fetches, MCP servers, and model endpoints typed into a block | Yes — see [Proxying guarded requests](#proxying-guarded-requests) | +| SSO sign-in — the token, JWKS, and userinfo calls made while a user signs in | Yes — they use the default `fetch` | +| SSO domain verification (TXT lookup) | No — DNS, sent to `1.1.1.1` and `8.8.8.8` unless `DOMAIN_VERIFICATION_NAMESERVERS` names your resolvers | +| Amazon Bedrock, object storage on S3, and every AWS integration (DynamoDB, SQS, Lambda, Textract, and the rest) | Yes — each AWS service client gets a proxy-aware agent while a proxy is set, and STS calls made for its credentials inherit it. Bedrock then uses HTTP/1.1 instead of HTTP/2. IAM Identity Center (SSO) credential lookups do not use the proxy | +| Email via SMTP, and the SMTP block | No — SMTP is not HTTP. Use a mail relay the app can reach directly, or Resend or SES | +| Email via Amazon SES | Yes — the AWS SDK client above | +| OTLP export from the server SDK | Yes — the exporters get the same proxy-aware agent | | The `/api/telemetry` relay that forwards browser events | Yes — it uses the default `fetch` | | Postgres and Redis | No — raw TCP | -The practical consequence: a mandatory-egress-proxy environment can route most LLM traffic, Resend mail, and Azure/GCS storage through the proxy, but guarded integration calls, S3, Bedrock, SMTP, telemetry, and datastore traffic still need a transparent proxy or NAT-based egress. +The practical consequence: a mandatory-egress-proxy environment can route LLM traffic, SSO, integration and workflow HTTP calls, Resend mail, Azure/GCS storage, AWS services, SES, and OTLP export through the proxy. SMTP and datastore traffic (Postgres, Redis, and the database blocks) still need a transparent proxy or NAT-based egress. + +### Proxying guarded requests + +A request through the SSRF guard keeps its checks when it is proxied. Before each request, and again for each redirect hop, the app resolves the destination's hostname with its own DNS and refuses it if any policy in [Security](/platform/self-hosting/security#the-ssrf-boundary) blocks any of the addresses it resolves to, since the proxy may connect to any of them. It then hands the hostname, not the address, to the proxy, so a proxy that allowlists by domain sees the name it expects. + +- **DNS must still resolve inside the container.** A destination that the app cannot resolve is refused before the proxy is asked. +- **The proxy resolves the name again.** The app cannot pin the address across a proxy, so the proxy's own egress policy is the last check. +- **`https` destinations are tunneled** with `CONNECT host:443`. **`http` destinations are forwarded** to the proxy as an absolute URL, as curl and Bun do. +- **Proxy credentials** go in the URL, e.g. `http://user:pass@proxy.internal:3128`. They are sent only to the proxy. An `https://` proxy URL also works; SOCKS proxies are not supported. +- **The HTTP block's `proxyUrl`** still takes precedence for that one request. ### Internal destinations and `NO_PROXY` -Set `NO_PROXY` for every destination that is not on the public internet, not just model endpoints. The app reaches the realtime server (`SOCKET_SERVER_URL`), the Presidio PII service (`PII_URL`), and itself (`INTERNAL_API_BASE_URL`) over the same default `fetch`, alongside self-hosted Ollama, LiteLLM, and vLLM — so a proxy that cannot reach your internal network breaks live updates and PII redaction, not only inference. +Set `NO_PROXY` for every destination that is not on the public internet, not just model endpoints. The app reaches the realtime server (`SOCKET_SERVER_URL`), the Presidio PII service (`PII_URL`), and itself (`INTERNAL_API_BASE_URL`) over the same default `fetch`, alongside self-hosted Ollama, LiteLLM, and vLLM — so a proxy that cannot reach your internal network breaks live updates and PII redaction, not only inference. AWS clients and OTLP export follow the same rule, so also add an internal S3-compatible store such as MinIO, AWS VPC endpoints, and an in-cluster OpenTelemetry collector. + +For the SSRF guard, AWS clients (including the boot-time Secrets Manager fetch), and OTLP export, Sim matches `NO_PROXY` entries with these rules: `example.com`, `.example.com`, and `*.example.com` each cover the domain and all its subdomains; `host:port` and `[::1]:port` cover one port; IPv6 entries work bare (`::1`) or bracketed; and CIDR ranges such as `10.0.0.0/8` cover IP-literal destinations only, not hostnames that resolve into the range. Bun's default `fetch`, which carries internal service URLs such as the realtime server and PII service, applies Bun's own `NO_PROXY` rules, which may not honor CIDR or bare IPv6 entries, so list those destinations by hostname. Set it in the application environment, not your shell — under `app.env` on Helm, or the service's `environment:` on Compose. On Helm the suffixes alone are not enough: the chart wires `SOCKET_SERVER_URL`, `PII_URL`, and `OLLAMA_URL` to bare Service names, which no domain suffix matches. Add those names too — `helm template` prints the rendered ones, and the prefix is the release name unless it already contains `sim`, in which case it is the release name alone: diff --git a/apps/docs/content/docs/search/gitlab.mdx b/apps/docs/content/docs/search/gitlab.mdx index 03a9eeb9634..7ed99eb6c4b 100644 --- a/apps/docs/content/docs/search/gitlab.mdx +++ b/apps/docs/content/docs/search/gitlab.mdx @@ -11,8 +11,8 @@ GitLab Search uses **Service account** mode, with one self-managed project per s | --- | --- | --- | | Token owner | Active instance administrator | Account that can read the project's content | | PAT scopes | `read_api`; `admin_mode` when Admin Mode is enabled | `read_api` | -| Reader authorization | Current GitLab identity, membership, roles, and supported restrictions | Admin-uploaded email mapping and project grants | -| Membership changes | Checked during the request | Admin updates the CSV files | +| Reader authorization | Current GitLab identity, membership, roles, and supported restrictions | Email mapping and project grants supplied by your organization | +| Membership changes | Checked during the request | After the next successful CSV update | | Confidential issues | Only when supported permissions allow the reader | Always excluded | The administrator path requires self-managed GitLab 17.4 or later and its administrator identity/settings APIs. A project Maintainer or group Owner is not an instance administrator. Non-admin mode does not require those administrator APIs. @@ -69,7 +69,9 @@ Use the full namespace path even if **Project** contains a numeric ID. Include i - Use UTF-8 CSV, at most 4 MiB and 100,000 data rows per file. The full request must fit 10 MiB. - Identical rows are deduplicated. Conflicting user-ID/email mappings and malformed rows are rejected. -These grants are admin-managed authorization. A granted reader can search allowed non-confidential content; Sim does not infer that person's actual GitLab role, restrictions, or membership from the non-admin token. Update both files when access changes. +In this example, Alice can search `engineering/platform` because the files link GitLab user `123` to both that project and `alice@example.com`. Sim checks that Alice is a current organization member and that her verified Sim email uniquely matches the grant. It checks again when she opens a result. GitLab receives the request under the source's read-only service account; Alice does not sign in to GitLab or authorize Sim individually. + +Saving the CSVs grants the access listed in them. Changing a membership in GitLab alone does not change these grants. Replace the CSVs to add or revoke access; the change applies to the next search or read without re-indexing. ## Access checks @@ -77,7 +79,7 @@ The source token retrieves content. Administrator mode proves the reader's curre Non-admin mode uses saved source-local CSV grants and revalidates the token's current host/project identity. Confidential issues are excluded, including issues that became confidential after search. -Reads recheck current settings, permissions, and metadata. A reference from another member, account, organization, project, or host cannot authorize a read. Live Search does not use a stored content or permission index. +Reads recheck current settings, permissions, and metadata. A reference from another member, account, organization, project, or host cannot authorize a read. Sim fetches content live from GitLab. In CSV mode, Sim stores the uploaded mappings and resulting project grants. ## Update permissions or rotate the token @@ -85,6 +87,41 @@ In project settings, replace either CSV file, retaining the other when appropria To rotate the PAT, supply its replacement in source settings. A blank token retains the saved token. Changing host, project, or permission strategy requires valid configuration for the new identity. +### Automate updates with GitLab CI + +After an admin configures a source in **Non-admin token** mode, a job can [read its revision](/api-reference/organizations/getSearchSourcePermissions) and [replace both CSVs](/api-reference/organizations/replaceSearchSourcePermissions). Copy `SIM_ORG_ID` and `SIM_SOURCE_ID` from the source settings URL: `/o//settings/integrations/sources/`. Set `SIM_BASE_URL` to your Sim origin (for hosted Sim, `https://www.sim.ai`). + +Store an organization admin's [personal Sim API key](/api-reference/authentication) with [**Full access**](/api-reference/authentication#key-access) as a [masked, protected GitLab CI variable](https://docs.gitlab.com/ci/variables/) named `SIM_API_KEY`. A personal key inherits its owner's access; a read-only key can read the revision but not replace the CSVs. This endpoint can only read the revision or replace both CSVs; it cannot change the GitLab PAT, host, or project. + +This example runs on a schedule against a protected branch. Replace `./ci/export-gitlab-permissions` with your existing export command. It must write a fresh, complete `users.csv` and `project-permissions.csv` and exit nonzero if it cannot finish. + +```yaml title=".gitlab-ci.yml" +sync_gitlab_permissions: + image: alpine:3.22 + resource_group: "sim-permissions-$SIM_SOURCE_ID" + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule" && $CI_COMMIT_REF_PROTECTED == "true"' + before_script: + - apk add --no-cache curl jq + script: + - | + set -eu + url="${SIM_BASE_URL%/}/api/v2/organizations/$SIM_ORG_ID/search/sources/$SIM_SOURCE_ID/permissions" + auth="X-API-Key: $SIM_API_KEY" + current=$(curl --fail --silent --show-error -H "$auth" "$url") + revision=$(printf '%s' "$current" | jq -er '.data.revision') + ./ci/export-gitlab-permissions + jq -n --argjson rev "$revision" \ + --rawfile users users.csv --rawfile grants project-permissions.csv \ + '{expectedRevision: $rev, permissions: {provider: "gitlab", + userMapping: {filename: "users.csv", content: $users}, + projectPermissions: {filename: "project-permissions.csv", content: $grants}}}' > update.json + curl --fail --silent --show-error --request PUT -H "$auth" \ + -H 'Content-Type: application/json' --data-binary @update.json "$url" +``` + +A successful update returns the new revision. A `409 Conflict` means someone updated the source after the job read its revision. Rerun the job from the beginning so it exports current grants. An invalid or header-only export leaves the previous grants in place. Alert on failed jobs so revocations aren't missed. If the project should have no readers, choose **Pause search** in the source settings. You can update the grants while the source is paused, then resume search when the grants are correct. + ## Troubleshooting | Problem | Check | diff --git a/apps/docs/content/docs/workflows/deployment/api.mdx b/apps/docs/content/docs/workflows/deployment/api.mdx index 412b1e76c63..481ac6a47af 100644 --- a/apps/docs/content/docs/workflows/deployment/api.mdx +++ b/apps/docs/content/docs/workflows/deployment/api.mdx @@ -14,6 +14,17 @@ You need workspace **Admin** access and the applicable deployment permission. Te Ordinary API calls use the active snapshot. Editing the draft does not change that version. Deploy an update before expecting a normal API call to use your changes. +### Run an earlier version [#run-a-version] + +To run a specific deployment version instead of the active one, name its number in `run.version`. The workflow must still be deployed, and the caller needs workspace **Write** access with an API key or OAuth access token; anonymous public-API callers cannot pin a version. An unknown version returns `404`. The run's status reports the version it ran in `version`. + +```bash +curl -X POST 'https://www.sim.ai/api/v2/workflows/{workflow-id}/execute' \ + -H "X-API-Key: $SIM_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"input":{"message":"Summarize this article"},"run":{"source":"deployment","version":3}}' +``` + ## Authentication Send a valid Sim API key in `X-API-Key`, or use a supported OAuth access token. Deployed workflow execution requires access to the workflow's workspace; OAuth callers also need the operation's `api:write` scope. See [API authentication](/api-reference/authentication). @@ -249,6 +260,8 @@ A completed response includes the result when requested: Stop polling on `completed`, `failed`, or `cancelled`. A paused Human run requires submitted input; a time pause can resume automatically. See [Human](/workflows/blocks/human-in-the-loop) and [Wait](/workflows/blocks/wait). Resuming an originally streaming run through the v2 resume endpoint returns an async receipt, not an SSE continuation. +A paused run lists every point it waits at in `paused.points`: the block, whether it is a `human` or `time` pause, its resume state, any parallel branch or loop iteration, its place in the resume queue, and the `inputFields` a Human pause asks for. With `includeOutput=true`, each point also carries the reviewer `display` content with resolved secrets redacted; organization permission groups that hide execution data withhold it, along with `inputFields`, whose labels and descriptions are resolved from run data. Resume a point by sending its `contextId` to `POST /api/v2/workflows/{workflow-id}/runs/{runId}/resume`. + ## Select outputs and handle large values [#oversized-outputs] `selectedOutputs` can project smaller fields. For a synchronous v2 call, selected values appear in `data.blockOutputs`, separately from `data.output`. diff --git a/apps/docs/content/integration-navigation.json b/apps/docs/content/integration-navigation.json index 9980d200365..17a8067e6a1 100644 --- a/apps/docs/content/integration-navigation.json +++ b/apps/docs/content/integration-navigation.json @@ -15,6 +15,13 @@ "integration": "attio", "title": "API keys" }, + "aws-credentials": { + "title": "AWS credentials" + }, + "bitbucket-service-account": { + "integration": "bitbucket", + "title": "Access tokens" + }, "box-service-account": { "integration": "box", "title": "Service accounts" @@ -27,6 +34,14 @@ "integration": "clickup", "title": "API tokens" }, + "databricks-credentials": { + "integration": "databricks", + "title": "Databricks connections" + }, + "docusign-service-account": { + "integration": "docusign", + "title": "JWT Grant" + }, "google-service-account": { "title": "Google service accounts" }, @@ -42,6 +57,9 @@ "integration": "linear", "title": "API keys" }, + "microsoft-app-credentials": { + "title": "Microsoft app credentials" + }, "monday-service-account": { "integration": "monday", "title": "API tokens" @@ -68,7 +86,7 @@ }, "snowflake-service-account": { "integration": "snowflake", - "title": "Programmatic access tokens" + "title": "Snowflake connections" }, "trello-service-account": { "integration": "trello", diff --git a/apps/docs/lib/redirects.ts b/apps/docs/lib/redirects.ts index a0ea0a5cb06..51b214455ca 100644 --- a/apps/docs/lib/redirects.ts +++ b/apps/docs/lib/redirects.ts @@ -93,8 +93,19 @@ const PATH_REDIRECTS: DocsRedirect[] = [ { source: '/api-reference', destination: '/api-reference/getting-started', permanent: true }, // sdks/* folded into api-reference/ { source: '/sdks', destination: '/api-reference/getting-started', permanent: true }, - { source: '/sdks/typescript', destination: '/api-reference/typescript', permanent: true }, - { source: '/sdks/python', destination: '/api-reference/python', permanent: true }, + { source: '/sdks/typescript', destination: '/api-reference/getting-started', permanent: true }, + { source: '/sdks/python', destination: '/api-reference/getting-started', permanent: true }, + // TypeScript and Python SDKs removed; their pages point at the HTTP API quickstart + { + source: '/api-reference/typescript', + destination: '/api-reference/getting-started', + permanent: true, + }, + { + source: '/api-reference/python', + destination: '/api-reference/getting-started', + permanent: true, + }, // building-agents/agents merged into the building-agents overview { source: '/building-agents/agents', destination: '/agents', permanent: true }, // form deployment removed diff --git a/apps/docs/openapi-v2-billing.json b/apps/docs/openapi-v2-billing.json index cd41bac2c0c..fabcf04f983 100644 --- a/apps/docs/openapi-v2-billing.json +++ b/apps/docs/openapi-v2-billing.json @@ -566,6 +566,7 @@ "PERMISSION_GROUP_CAPABILITY_BLOCKED", "INTEGRATION_NOT_ALLOWED", "INSUFFICIENT_SCOPE", + "READ_ONLY_API_KEY", "SCIM_MANAGED_MEMBERSHIP" ], "title": "Forbidden detail code", diff --git a/apps/docs/openapi-v2-files-audit.json b/apps/docs/openapi-v2-files-audit.json index 2fcff52693b..d28f2df00a7 100644 --- a/apps/docs/openapi-v2-files-audit.json +++ b/apps/docs/openapi-v2-files-audit.json @@ -4713,6 +4713,7 @@ "PERMISSION_GROUP_CAPABILITY_BLOCKED", "INTEGRATION_NOT_ALLOWED", "INSUFFICIENT_SCOPE", + "READ_ONLY_API_KEY", "SCIM_MANAGED_MEMBERSHIP" ], "title": "Forbidden detail code", diff --git a/apps/docs/openapi-v2-knowledge.json b/apps/docs/openapi-v2-knowledge.json index cadd389f3d1..75f473cb683 100644 --- a/apps/docs/openapi-v2-knowledge.json +++ b/apps/docs/openapi-v2-knowledge.json @@ -6018,6 +6018,7 @@ "PERMISSION_GROUP_CAPABILITY_BLOCKED", "INTEGRATION_NOT_ALLOWED", "INSUFFICIENT_SCOPE", + "READ_ONLY_API_KEY", "SCIM_MANAGED_MEMBERSHIP" ], "title": "Forbidden detail code", diff --git a/apps/docs/openapi-v2-logs.json b/apps/docs/openapi-v2-logs.json index 52c8878d3e4..58c1c1e947f 100644 --- a/apps/docs/openapi-v2-logs.json +++ b/apps/docs/openapi-v2-logs.json @@ -1065,6 +1065,7 @@ "PERMISSION_GROUP_CAPABILITY_BLOCKED", "INTEGRATION_NOT_ALLOWED", "INSUFFICIENT_SCOPE", + "READ_ONLY_API_KEY", "SCIM_MANAGED_MEMBERSHIP" ], "title": "Forbidden detail code", diff --git a/apps/docs/openapi-v2-resources.json b/apps/docs/openapi-v2-resources.json index 17f01a09649..e5a56c40eb0 100644 --- a/apps/docs/openapi-v2-resources.json +++ b/apps/docs/openapi-v2-resources.json @@ -421,6 +421,239 @@ } } }, + "/api/v2/workspaces/{workspaceId}/members/{userId}": { + "patch": { + "operationId": "updateWorkspaceMember", + "summary": "Update workspace member", + "description": "Change one existing member’s workspace role. Requires workspace Admin access. Only existing members can be changed; anyone else is Not found, and people are added with Create workspace invitations. You cannot lower your own role, and organization Admins, the workspace owner, and the workspace billing account always keep Admin. Members whose organization membership an identity provider manages cannot be changed here. Requesting the role a member already holds changes nothing and records no audit entry, except that an organization Admin is refused whatever role is requested. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.", + "x-sim-operation": "workspaces.permissions.update", + "x-oauth-scope": "api:write", + "tags": ["Workspaces"], + "security": [ + { + "apiKey": [] + }, + { + "oauthBearer": ["api:write"] + } + ], + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "description": "Workspace the member belongs to.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Workspace the member belongs to." + } + }, + { + "name": "userId", + "in": "path", + "required": true, + "description": "User identifier of the workspace member, as returned by List workspace members.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "User identifier of the workspace member, as returned by List workspace members." + } + } + ], + "requestBody": { + "required": true, + "description": "The new workspace role for this member.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkspaceMemberBody" + }, + "examples": { + "example1": { + "summary": "Request example 1", + "value": { + "role": "write" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The member with their updated workspace role.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkspaceMemberResponse" + }, + "examples": { + "example1": { + "summary": "Response example 1", + "value": { + "data": { + "userId": "user-123", + "email": "jane@example.com", + "name": "Jane Smith", + "image": null, + "role": "write", + "isExternal": false, + "joinedAt": "2026-01-15T10:30:00.000Z" + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + }, + "delete": { + "operationId": "removeWorkspaceMember", + "summary": "Remove workspace member", + "description": "Remove a member’s workspace access, or leave the workspace by naming yourself. Removing someone else requires workspace Admin access; any member may leave. The last Admin cannot leave, organization Admins hold access through their organization role and cannot be removed here, and the workspace billing account cannot be removed until billing is reassigned. Removing the workspace owner transfers ownership to the billing account. When this was the member’s last workspace in its organization, their organization membership and seat are removed too and all of their sessions end, since this endpoint is called with an API key or OAuth token rather than from a signed-in browser session. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.", + "x-sim-operation": "workspaces.members.remove", + "x-oauth-scope": "api:write", + "tags": ["Workspaces"], + "security": [ + { + "apiKey": [] + }, + { + "oauthBearer": ["api:write"] + } + ], + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "description": "Workspace the member belongs to.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Workspace the member belongs to." + } + }, + { + "name": "userId", + "in": "path", + "required": true, + "description": "User identifier of the workspace member, as returned by List workspace members.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "User identifier of the workspace member, as returned by List workspace members." + } + } + ], + "responses": { + "200": { + "description": "Acknowledgement that workspace access was removed.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveWorkspaceMemberResponse" + }, + "examples": { + "example1": { + "summary": "Response example 1", + "value": { + "data": { + "userId": "user-123", + "deleted": true, + "organizationMembershipRemoved": false + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, "/api/v2/mcp-servers": { "get": { "operationId": "listMcpServers", @@ -3591,7 +3824,7 @@ "post": { "operationId": "createServiceAccountCredential", "summary": "Create service-account credential", - "description": "Verify and store a service-account credential. Use the fields from List credential providers, encoded as a JSON object string in `credentials`. Requires workspace write access. A matching source returns the existing credential with `200`; creation returns `201`. Stored secrets are omitted from the response. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.", + "description": "Verify and store a service-account credential. Use the fields from List credential providers, encoded as a JSON object string in `credentials`. For a keyless AWS, Databricks, Snowflake, Microsoft app, or Google Cloud credential, call Create workload identity subject first, configure the cloud to trust it, then send its `keylessConfig` as `serviceAccountJson`. A refused keyless credential names its cause in `details.code`: `409` for `WORKLOAD_IDENTITY_SUBJECT_IN_USE` or `WORKLOAD_IDENTITY_SUBJECT_UNVERIFIABLE`, otherwise `400`, such as `WORKLOAD_IDENTITY_PROOF_EXPIRED` (request a new subject). Requires workspace write access. A matching source returns the existing credential with `200`; creation returns `201`. Stored secrets are omitted from the response. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.", "x-sim-operation": "credentials.service_accounts.create", "x-oauth-scope": "api:write", "tags": ["Credentials"], @@ -3621,6 +3854,16 @@ "displayName": "Zoom automation", "credentials": "{\"clientId\":\"YOUR_CLIENT_ID\",\"clientSecret\":\"YOUR_CLIENT_SECRET\",\"orgId\":\"YOUR_ACCOUNT_ID\"}" } + }, + "example2": { + "summary": "Request example 2", + "value": { + "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64", + "type": "service_account", + "providerId": "aws-service-account", + "displayName": "AWS production (keyless)", + "credentials": "{\"serviceAccountJson\":\"{\\\"authType\\\":\\\"oidc_role\\\",\\\"roleArn\\\":\\\"arn:aws:iam::123456789012:role/SimWorkflows\\\",\\\"subject\\\":\\\"sim:workspace::credential:\\\",\\\"subjectProof\\\":\\\"\\\"}\"}" + } } } } @@ -4019,6 +4262,115 @@ } } }, + "/api/v2/credentials/workload-identity/subjects": { + "post": { + "operationId": "createCredentialWorkloadIdentitySubject", + "summary": "Create workload identity subject", + "description": "Start a keyless credential. Returns the issuer, audience, and a fresh subject to configure in the cloud's trust policy, plus `keylessConfig`: a prefilled `serviceAccountJson` whose `subjectProof` is valid for 24 hours and only for the requesting user. Add the `requiredConfigFields`, then call Create service-account credential with the same `providerId`; Sim verifies the trust while saving. Google Cloud has no fixed audience: it is the credential's own pool provider. Without keyless support the deployment returns `400` with `details.code` `WORKLOAD_IDENTITY_NOT_CONFIGURED`. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.", + "x-sim-operation": "credentials.workload_identity.prepare", + "x-oauth-scope": "api:write", + "tags": ["Credentials"], + "security": [ + { + "apiKey": [] + }, + { + "oauthBearer": ["api:write"] + } + ], + "requestBody": { + "required": true, + "description": "Workspace that will own the keyless credential and the service-account provider it will be created under.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCredentialWorkloadIdentitySubjectRequest" + }, + "examples": { + "example1": { + "summary": "Request example 1", + "value": { + "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64", + "providerId": "aws-service-account" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "A subject and proof for one new keyless credential.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCredentialWorkloadIdentitySubjectResponse" + }, + "examples": { + "example1": { + "summary": "Response example 1", + "value": { + "data": { + "issuer": "https://www.sim.ai/oidc/workload", + "audience": "sts.amazonaws.com", + "subject": "sim:workspace::credential:", + "subjectProof": "", + "expiresAt": "2026-06-21T14:17:11.000Z", + "keylessConfig": { + "authType": "oidc_role", + "subject": "sim:workspace::credential:", + "subjectProof": "" + }, + "requiredConfigFields": ["roleArn"], + "optionalConfigFields": ["stsRegion", "sessionDurationSeconds"] + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, "/api/v2/credentials/{credentialId}": { "delete": { "operationId": "deleteCredential", @@ -4714,7 +5066,7 @@ "get": { "operationId": "getApiMeta", "summary": "Get API status", - "description": "Get the calling credential’s type and expiry. Requires a valid API key or OAuth access token. `v2Enabled` is true whenever this endpoint is served; the response does not enumerate resource permissions.\n\nOAuth scope: `api:read`.", + "description": "Get the calling credential’s type, expiry, and API scopes. Requires a valid API key or OAuth access token. `v2Enabled` is true whenever this endpoint is served; the response does not enumerate resource permissions.\n\nOAuth scope: `api:read`.", "x-sim-operation": "meta.capabilities.read", "x-oauth-scope": "api:read", "tags": ["Meta"], @@ -4752,7 +5104,8 @@ "data": { "v2Enabled": true, "keyType": "personal", - "expiresAt": null + "expiresAt": null, + "scopes": ["api:read", "api:write", "search:read"] } } } @@ -9559,12 +9912,251 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResendOrganizationInvitationBody" + "$ref": "#/components/schemas/ResendOrganizationInvitationBody" + }, + "examples": { + "example1": { + "summary": "Request example 1", + "value": {} + } + } + } + } + }, + "responses": { + "200": { + "description": "The pending invitation with its renewed expiry.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResendOrganizationInvitationResponse" + }, + "examples": { + "example1": { + "summary": "Response example 1", + "value": { + "data": { + "id": "invitation-123", + "organizationId": "org-123", + "email": "member@example.com", + "role": "member", + "kind": "organization", + "membershipIntent": "internal", + "status": "pending", + "createdAt": "2026-06-01T09:00:00.000Z", + "expiresAt": "2026-06-08T09:00:00.000Z" + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, + "/api/v2/organizations/{organizationId}/search/sources/{sourceId}/permissions": { + "get": { + "operationId": "getSearchSourcePermissions", + "summary": "Get Search source permission revision", + "description": "Read the permission revision for a Search source that accepts externally managed grants. Requires organization Admin access. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.", + "x-sim-operation": "knowledge.search.source_permissions.read", + "x-oauth-scope": "api:read", + "tags": ["Organizations"], + "security": [ + { + "apiKey": [] + }, + { + "oauthBearer": ["api:read"] + } + ], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "description": "Organization identifier.", + "schema": { + "type": "string", + "minLength": 1, + "description": "Organization identifier." + } + }, + { + "name": "sourceId", + "in": "path", + "required": true, + "description": "Identifier of the configured Search source.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Identifier of the configured Search source." + } + } + ], + "responses": { + "200": { + "description": "The current permission revision.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchSourcePermissionResponse" + }, + "examples": { + "example1": { + "summary": "Response example 1", + "value": { + "data": { + "revision": 3 + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + }, + "put": { + "operationId": "replaceSearchSourcePermissions", + "summary": "Replace Search source permissions", + "description": "Replace the complete permission grants for one Search source. Requires organization Admin access. Supply the current revision and the grants for the configured provider. A stale revision returns 409. GitLab CSV mode accepts both files together, up to 4 MiB and 100,000 data rows each. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.", + "x-sim-operation": "knowledge.search.source_permissions.replace", + "x-oauth-scope": "api:write", + "tags": ["Organizations"], + "security": [ + { + "apiKey": [] + }, + { + "oauthBearer": ["api:write"] + } + ], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "description": "Organization identifier.", + "schema": { + "type": "string", + "minLength": 1, + "description": "Organization identifier." + } + }, + { + "name": "sourceId", + "in": "path", + "required": true, + "description": "Identifier of the configured Search source.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Identifier of the configured Search source." + } + } + ], + "requestBody": { + "required": true, + "description": "The expected revision and complete replacement grants for the provider.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceSearchSourcePermissionsBody" }, "examples": { "example1": { "summary": "Request example 1", - "value": {} + "value": { + "expectedRevision": 2, + "permissions": { + "provider": "gitlab", + "userMapping": { + "filename": "users.csv", + "content": "user_id,email\n42,reader@example.com\n" + }, + "projectPermissions": { + "filename": "projects.csv", + "content": "project_path,user_id\nteam/project,42\n" + } + } + } } } } @@ -9572,7 +10164,7 @@ }, "responses": { "200": { - "description": "The pending invitation with its renewed expiry.", + "description": "The new permission revision.", "headers": { "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" @@ -9587,22 +10179,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResendOrganizationInvitationResponse" + "$ref": "#/components/schemas/SearchSourcePermissionResponse" }, "examples": { "example1": { "summary": "Response example 1", "value": { "data": { - "id": "invitation-123", - "organizationId": "org-123", - "email": "member@example.com", - "role": "member", - "kind": "organization", - "membershipIntent": "internal", - "status": "pending", - "createdAt": "2026-06-01T09:00:00.000Z", - "expiresAt": "2026-06-08T09:00:00.000Z" + "revision": 3 } } } @@ -14343,6 +14927,7 @@ "PERMISSION_GROUP_CAPABILITY_BLOCKED", "INTEGRATION_NOT_ALLOWED", "INSUFFICIENT_SCOPE", + "READ_ONLY_API_KEY", "SCIM_MANAGED_MEMBERSHIP" ], "title": "Forbidden detail code", @@ -14605,6 +15190,95 @@ } ] }, + "UpdateWorkspaceMemberResponse": { + "type": "object", + "properties": { + "data": { + "description": "Response data.", + "$ref": "#/components/schemas/V2WorkspaceMember" + } + }, + "required": ["data"], + "additionalProperties": false, + "title": "Update workspace member response", + "description": "The member with their updated workspace role.", + "examples": [ + { + "data": { + "userId": "user-123", + "email": "jane@example.com", + "name": "Jane Smith", + "image": null, + "role": "write", + "isExternal": false, + "joinedAt": "2026-01-15T10:30:00.000Z" + } + } + ] + }, + "UpdateWorkspaceMemberBody": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["admin", "write", "read"], + "description": "New workspace role. Inherited organization Admin access, the workspace owner, and the workspace billing account keep Admin and cannot be changed here." + } + }, + "required": ["role"], + "additionalProperties": false, + "title": "Update workspace member body", + "description": "The new workspace role for this member.", + "examples": [ + { + "role": "write" + } + ] + }, + "V2WorkspaceMemberDeletion": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "description": "User removed from the workspace." + }, + "deleted": { + "type": "boolean", + "const": true, + "description": "Whether workspace access was removed." + }, + "organizationMembershipRemoved": { + "type": "boolean", + "description": "Whether this was the member’s last workspace in the organization, so their organization membership and seat were removed too." + } + }, + "required": ["userId", "deleted", "organizationMembershipRemoved"], + "additionalProperties": false, + "title": "Workspace member removal", + "description": "Acknowledges removal of a workspace member." + }, + "RemoveWorkspaceMemberResponse": { + "type": "object", + "properties": { + "data": { + "description": "Response data.", + "$ref": "#/components/schemas/V2WorkspaceMemberDeletion" + } + }, + "required": ["data"], + "additionalProperties": false, + "title": "Remove workspace member response", + "description": "Acknowledgement that workspace access was removed.", + "examples": [ + { + "data": { + "userId": "user-123", + "deleted": true, + "organizationMembershipRemoved": false + } + } + ] + }, "V2McpServer": { "type": "object", "properties": { @@ -17257,6 +17931,32 @@ "additionalProperties": false }, "description": "Create-body fields accepted by this provider. Secret fields are write-only." + }, + "keyless": { + "description": "Present when this deployment can create a keyless credential for the provider. Start one with Create workload identity subject.", + "type": "object", + "properties": { + "authType": { + "type": "string", + "description": "The `authType` of the keyless `serviceAccountJson`." + }, + "requiredConfigFields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields you add to `keylessConfig`." + }, + "optionalConfigFields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields `keylessConfig` may also carry." + } + }, + "required": ["authType", "requiredConfigFields", "optionalConfigFields"], + "additionalProperties": false } }, "required": [ @@ -17717,6 +18417,142 @@ } ] }, + "V2CredentialWorkloadIdentitySubject": { + "type": "object", + "properties": { + "issuer": { + "type": "string", + "description": "OpenID Connect issuer URL to register as the identity provider the cloud trusts." + }, + "audience": { + "description": "The `aud` claim the cloud must accept. Absent for Google Cloud, whose audience is each credential's own workload identity pool provider.", + "type": "string" + }, + "subject": { + "type": "string", + "description": "The exact `sub` claim the trust configuration must allow; unique to one credential." + }, + "subjectProof": { + "type": "string", + "description": "Opaque proof submitted with `subject` when the credential is created. Bound to the requesting user, workspace, and provider." + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp after which `subjectProof` can no longer create a credential." + }, + "keylessConfig": { + "type": "object", + "properties": { + "authType": { + "type": "string", + "description": "The provider's keyless authentication type." + }, + "subject": { + "type": "string", + "description": "The exact `sub` claim the trust configuration must allow; unique to one credential." + }, + "subjectProof": { + "type": "string", + "description": "Opaque proof submitted with `subject` when the credential is created. Bound to the requesting user, workspace, and provider." + } + }, + "required": ["authType", "subject", "subjectProof"], + "additionalProperties": false, + "description": "Prefilled keyless `serviceAccountJson`: add `requiredConfigFields`, then send it to Create service-account credential." + }, + "requiredConfigFields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields from your cloud setup that `keylessConfig` still needs, such as roleArn or host." + }, + "optionalConfigFields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields `keylessConfig` may also carry." + } + }, + "required": [ + "issuer", + "subject", + "subjectProof", + "expiresAt", + "keylessConfig", + "requiredConfigFields", + "optionalConfigFields" + ], + "additionalProperties": false, + "title": "Credential workload identity subject", + "description": "The issuer, audience, and subject a cloud trust configuration pins, and the proof that attaches the subject to a new keyless credential." + }, + "CreateCredentialWorkloadIdentitySubjectResponse": { + "type": "object", + "properties": { + "data": { + "description": "Response data.", + "$ref": "#/components/schemas/V2CredentialWorkloadIdentitySubject" + } + }, + "required": ["data"], + "additionalProperties": false, + "title": "Create workload identity subject response", + "description": "Trust values to configure in the cloud, and the proof to submit when creating the credential.", + "examples": [ + { + "data": { + "issuer": "https://www.sim.ai/oidc/workload", + "audience": "sts.amazonaws.com", + "subject": "sim:workspace::credential:", + "subjectProof": "", + "expiresAt": "2026-06-21T14:17:11.000Z", + "keylessConfig": { + "authType": "oidc_role", + "subject": "sim:workspace::credential:", + "subjectProof": "" + }, + "requiredConfigFields": ["roleArn"], + "optionalConfigFields": ["stsRegion", "sessionDurationSeconds"] + } + } + ] + }, + "CreateCredentialWorkloadIdentitySubjectRequest": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Workspace that will own the keyless credential." + }, + "providerId": { + "type": "string", + "enum": [ + "aws-service-account", + "databricks-service-account", + "snowflake-service-account", + "microsoft-app-service-account", + "google-service-account" + ], + "description": "Service-account provider the keyless credential will be created under; pass the same value to Create service-account credential. The response and List credential providers name the fields its keyless configuration needs." + } + }, + "required": ["workspaceId", "providerId"], + "additionalProperties": false, + "title": "Create workload identity subject request", + "description": "Workspace that will own the keyless credential and the service-account provider it will be created under.", + "examples": [ + { + "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64", + "providerId": "aws-service-account" + } + ] + }, "V2CredentialDeleteData": { "type": "object", "properties": { @@ -18089,9 +18925,17 @@ } ], "description": "ISO 8601 timestamp when the calling credential expires, or null when it does not." + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "enum": ["api:read", "api:write", "search:read"] + }, + "description": "API scopes the calling credential can use. api:write implies api:read, and either implies search:read. A read-only API key holds api:read and search:read: it is refused every operation that changes state and every operation that declares no API scope, so api:read alone does not authorize those. A full-access key holds all three; an OAuth access token holds what it was granted and what that implies." } }, - "required": ["v2Enabled", "keyType", "expiresAt"], + "required": ["v2Enabled", "keyType", "expiresAt", "scopes"], "additionalProperties": false, "title": "API status", "description": "API availability and lifecycle facts about the calling credential." @@ -18107,13 +18951,14 @@ "required": ["data"], "additionalProperties": false, "title": "API status response", - "description": "API availability, credential type, and expiry for the caller.", + "description": "API availability, credential type, expiry, and scopes for the caller.", "examples": [ { "data": { "v2Enabled": true, "keyType": "personal", - "expiresAt": null + "expiresAt": null, + "scopes": ["api:read", "api:write", "search:read"] } } ] @@ -23298,6 +24143,129 @@ } ] }, + "SearchSourcePermissionRevision": { + "type": "object", + "properties": { + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Current permission revision." + } + }, + "required": ["revision"], + "additionalProperties": false, + "title": "Search source permission revision", + "description": "Revision of this source’s permission grants." + }, + "SearchSourcePermissionResponse": { + "type": "object", + "properties": { + "data": { + "description": "Response data.", + "$ref": "#/components/schemas/SearchSourcePermissionRevision" + } + }, + "required": ["data"], + "additionalProperties": false, + "title": "Search source permission response", + "description": "Current revision of the source permission grants.", + "examples": [ + { + "data": { + "revision": 3 + } + } + ] + }, + "ReplaceSearchSourcePermissionsBody": { + "type": "object", + "properties": { + "expectedRevision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Permission revision read before exporting the grants." + }, + "permissions": { + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "gitlab", + "description": "Provider of the configured Search source." + }, + "userMapping": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "CSV filename without path separators." + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 4194304, + "description": "UTF-8 CSV contents, up to 4 MiB. Both files travel in one request body limited to 10 MiB after JSON encoding." + } + }, + "required": ["filename", "content"], + "additionalProperties": false, + "description": "GitLab user IDs and their verified Sim email addresses." + }, + "projectPermissions": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "CSV filename without path separators." + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 4194304, + "description": "UTF-8 CSV contents, up to 4 MiB. Both files travel in one request body limited to 10 MiB after JSON encoding." + } + }, + "required": ["filename", "content"], + "additionalProperties": false, + "description": "GitLab project paths and the IDs of users granted access." + } + }, + "required": ["provider", "userMapping", "projectPermissions"], + "additionalProperties": false + } + ], + "description": "Complete permission grants in the source provider’s format." + } + }, + "required": ["expectedRevision", "permissions"], + "additionalProperties": false, + "title": "Search source permission grants", + "description": "The expected revision and complete replacement grants for the provider.", + "examples": [ + { + "expectedRevision": 2, + "permissions": { + "provider": "gitlab", + "userMapping": { + "filename": "users.csv", + "content": "user_id,email\n42,reader@example.com\n" + }, + "projectPermissions": { + "filename": "projects.csv", + "content": "project_path,user_id\nteam/project,42\n" + } + } + } + ] + }, "ListSsoProvidersResponse": { "type": "object", "properties": { diff --git a/apps/docs/openapi-v2-tables.json b/apps/docs/openapi-v2-tables.json index 1efe55b277d..509b1ef430d 100644 --- a/apps/docs/openapi-v2-tables.json +++ b/apps/docs/openapi-v2-tables.json @@ -5977,6 +5977,7 @@ "PERMISSION_GROUP_CAPABILITY_BLOCKED", "INTEGRATION_NOT_ALLOWED", "INSUFFICIENT_SCOPE", + "READ_ONLY_API_KEY", "SCIM_MANAGED_MEMBERSHIP" ], "title": "Forbidden detail code", diff --git a/apps/docs/openapi-v2-workflows.json b/apps/docs/openapi-v2-workflows.json index f36b9bc5f69..d18c6fee64d 100644 --- a/apps/docs/openapi-v2-workflows.json +++ b/apps/docs/openapi-v2-workflows.json @@ -2565,6 +2565,246 @@ } } }, + "/api/v2/workflows/{workflowId}/triggers": { + "get": { + "operationId": "listWorkflowTriggers", + "summary": "List workflow triggers", + "description": "List the schedules, polling triggers, internal-event triggers, and webhooks the live deployment serves, including disabled ones, with their failure counts. Requires workspace read access. Schedules and polling triggers disable themselves after 100 consecutive failures; a `webhook` trigger's failures happen at the sender, so its `failedCount` stays 0. Empty while the workflow is not deployed. Returns one page; `nextCursor` is always null.\n\nOAuth scope: `api:read`.", + "x-sim-operation": "workflows.triggers.read", + "x-oauth-scope": "api:read", + "tags": ["Workflows"], + "security": [ + { + "apiKey": [] + }, + { + "oauthBearer": ["api:read"] + } + ], + "parameters": [ + { + "name": "workflowId", + "in": "path", + "required": true, + "description": "Unique workflow identifier.", + "schema": { + "type": "string", + "minLength": 1, + "description": "Unique workflow identifier.", + "examples": ["3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36"] + } + } + ], + "responses": { + "200": { + "description": "The live deployment's triggers.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowTriggerListResponse" + }, + "examples": { + "example1": { + "summary": "Response example 1", + "value": { + "data": [ + { + "blockId": "blk_01J8ZK3QW4M6X2R9T7B5C0V4", + "kind": "schedule", + "provider": null, + "enabled": false, + "status": "disabled", + "failedCount": 100, + "lastFailedAt": "2026-08-09T18:04:11.000Z", + "nextRunAt": null, + "lastRanAt": "2026-08-09T18:04:00.000Z", + "cronExpression": "0 * * * *", + "timezone": "UTC", + "url": null + } + ], + "nextCursor": null + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, + "/api/v2/workflows/{workflowId}/triggers/{blockId}": { + "patch": { + "operationId": "updateWorkflowTrigger", + "summary": "Update workflow trigger", + "description": "Enable or disable one trigger of the live deployment without redeploying. Requires workspace write access. Enabling resets `failedCount`, schedules the next run, and clears any polling backoff; a schedule whose cron expression is invalid or has no future runs returns `400`. A permission group that blocks webhook triggers returns `403` when a member it governs enables a polling, internal-event, or webhook trigger; a workspace API key is not governed by permission groups, as with every other capability. Disabling a completed schedule marks it disabled. A block with several live rows switches them together. Requesting the current state changes nothing.\n\nDisabling only stops Sim from accepting the trigger; nothing is removed at the provider. A disable lasts until the next deploy or version activation, which re-enables every trigger, the same way it clears a trigger that disabled itself after repeated failures. Deliveries to a disabled webhook receive `404`, and some providers disable their subscription after repeated failures, which requires a redeploy to restore.\n\nOAuth scope: `api:write`.", + "x-sim-operation": "workflows.triggers.set_enabled", + "x-oauth-scope": "api:write", + "tags": ["Workflows"], + "security": [ + { + "apiKey": [] + }, + { + "oauthBearer": ["api:write"] + } + ], + "parameters": [ + { + "name": "workflowId", + "in": "path", + "required": true, + "description": "Unique workflow identifier.", + "schema": { + "type": "string", + "minLength": 1, + "description": "Unique workflow identifier.", + "examples": ["3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36"] + } + }, + { + "name": "blockId", + "in": "path", + "required": true, + "description": "Trigger block in the deployed workflow.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Trigger block in the deployed workflow." + } + } + ], + "requestBody": { + "required": true, + "description": "Enable or disable one trigger of the live deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkflowTriggerRequest" + }, + "examples": { + "example1": { + "summary": "Request example 1", + "value": { + "enabled": true + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The trigger after the update.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkflowTriggerResponse" + }, + "examples": { + "example1": { + "summary": "Response example 1", + "value": { + "data": { + "blockId": "blk_01J8ZK3QW4M6X2R9T7B5C0V4", + "kind": "schedule", + "provider": null, + "enabled": true, + "status": "active", + "failedCount": 0, + "lastFailedAt": "2026-08-09T18:04:11.000Z", + "nextRunAt": "2026-08-10T09:00:00.000Z", + "lastRanAt": "2026-08-09T18:04:00.000Z", + "cronExpression": "0 * * * *", + "timezone": "UTC", + "url": null + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "423": { + "$ref": "#/components/responses/Locked" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, "/api/v2/workflows/{workflowId}/deploy": { "post": { "operationId": "deployWorkflow", @@ -3718,7 +3958,7 @@ "post": { "operationId": "executeWorkflowV2", "summary": "Execute workflow", - "description": "Run the active deployment, or set `run.source: \"manual\"` to run the saved draft. Deployed execution requires workspace read access and OAuth `api:write` scope or an API key; public deployments also allow anonymous synchronous or streamed runs. Manual runs require personal-key or OAuth write access and cannot be async.\n\nUse `async: true` for a queue receipt, or `stream: true` for SSE. For a synchronous run with both flags false, send `Accept: application/x-ndjson` for heartbeat frames and a final frame containing `data`. With `waitTimeoutSeconds`, a synchronous run still going when the wait elapses continues in the background and answers with its run receipt instead: `202` on a JSON response, or a closing `detached` frame on an NDJSON stream, which keeps its `200`. Timeouts return `200` with `status: \"failed\"` and `error.code: \"TIMEOUT\"`; inspect run status. See [Streaming](/workflows/deployment/api#streaming) for event formats. Input descriptions specify compatible modes; invalid combinations return `400`.\n\nSupply `X-Run-Id` to prevent duplicate execution. Reuse returns `409`, without replaying results. A `503` with `error.details.code: \"ASYNC_ENQUEUE_AMBIGUOUS\"` omits `Retry-After` because the run may already have started. Reconcile using the returned run ID; do not start another run with a new or omitted ID.\n\nOAuth scope: `api:write`.", + "description": "Run the active deployment, set `run: { source: \"deployment\", version }` to run an earlier deployment version, or set `run.source: \"manual\"` to run the saved draft. Deployed execution requires workspace read access and OAuth `api:write` scope or a full-access API key; public deployments also allow anonymous synchronous or streamed runs. Running a chosen version requires workspace write access and a full-access API key or OAuth `api:write` scope; an unknown version returns `404`. Manual runs require a full-access personal API key or OAuth `api:write` scope, and workspace write access; workspace API keys are refused, and manual runs cannot be async.\n\nUse `async: true` for a queue receipt, or `stream: true` for SSE. For a synchronous run with both flags false, send `Accept: application/x-ndjson` for heartbeat frames and a final frame containing `data`. With `waitTimeoutSeconds`, a synchronous run still going when the wait elapses continues in the background and answers with its run receipt instead: `202` on a JSON response, or a closing `detached` frame on an NDJSON stream, which keeps its `200`. Timeouts return `200` with `status: \"failed\"` and `error.code: \"TIMEOUT\"`; inspect run status. See [Streaming](/workflows/deployment/api#streaming) for event formats. Input descriptions specify compatible modes; invalid combinations return `400`.\n\nSupply `X-Run-Id` to prevent duplicate execution. Reuse returns `409`, without replaying results. A `503` with `error.details.code: \"ASYNC_ENQUEUE_AMBIGUOUS\"` omits `Retry-After` because the run may already have started. Reconcile using the returned run ID; do not start another run with a new or omitted ID.\n\nOAuth scope: `api:write`.", "x-sim-operation": "workflows.execute", "x-oauth-scope": "api:write", "tags": ["Workflows"], @@ -4165,7 +4405,7 @@ "get": { "operationId": "getWorkflowRunV2", "summary": "Get workflow run", - "description": "Read current run state with workspace read access. Request final output and files with `includeOutput`, or block outputs with `selectedOutputs`. Organization permission groups can hide output, files, or cost.\n\n`includeFileBase64` inlines file bytes. A file or the total above 16 MiB returns `413` naming a download path; download large files individually. `HEAD` checks access with the same authorization as `GET` but skips side effects, returning an empty `200` without payload headers on success.\n\nOAuth scope: `api:read`.", + "description": "Read current run state with workspace read access. Request final output and files with `includeOutput`, or block outputs with `selectedOutputs`. Organization permission groups can hide output, files, or cost. A paused run lists every pause point in `paused.points`, with the fields a human pause asks for and its content with resolved secrets redacted; resume one with Resume workflow run.\n\n`includeFileBase64` inlines file bytes. A file or the total above 16 MiB returns `413` naming a download path; download large files individually. `HEAD` checks access with the same authorization as `GET` but skips side effects, returning an empty `200` without payload headers on success.\n\nOAuth scope: `api:read`.", "x-sim-operation": "workflows.runs.read", "x-oauth-scope": "api:read", "tags": ["Workflow Runs"], @@ -4277,6 +4517,7 @@ "startedAt": "2026-08-09T18:04:10.000Z", "endedAt": "2026-08-09T18:04:11.000Z", "durationMs": 1000, + "version": 3, "paused": null, "cost": { "total": 12 @@ -4451,7 +4692,7 @@ "post": { "operationId": "resumeWorkflowRunV2", "summary": "Resume workflow run", - "description": "Resume one human-in-the-loop pause with workspace write access. The resumed attempt receives a new run ID and returns a synchronous result or queue receipt. A run that originally streamed resumes asynchronously through v2; this endpoint returns no SSE stream.\n\nOAuth scope: `api:write`.", + "description": "Resume one human-in-the-loop pause with workspace write access, naming a `contextId` from the run's `paused.points`. The resumed attempt receives a new run ID and returns a synchronous result or queue receipt. A run that originally streamed resumes asynchronously through v2; this endpoint returns no SSE stream.\n\nOAuth scope: `api:write`.", "x-sim-operation": "workflows.runs.resume", "x-oauth-scope": "api:write", "tags": ["Workflow Runs"], @@ -7544,6 +7785,7 @@ "PERMISSION_GROUP_CAPABILITY_BLOCKED", "INTEGRATION_NOT_ALLOWED", "INSUFFICIENT_SCOPE", + "READ_ONLY_API_KEY", "SCIM_MANAGED_MEMBERSHIP" ], "title": "Forbidden detail code", @@ -12581,19 +12823,19 @@ } ] }, - "DeployResult": { + "WorkflowTrigger": { "type": "object", "properties": { - "id": { + "blockId": { "type": "string", - "description": "Unique workflow identifier.", - "examples": ["3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36"] + "description": "Trigger block in the deployed workflow." }, - "isDeployed": { - "type": "boolean", - "description": "Whether a workflow version is currently live and available for API execution." + "kind": { + "type": "string", + "enum": ["schedule", "polling", "event", "webhook"], + "description": "How the trigger fires: `schedule` on a cron, `polling` when Sim polls the source, `event` on an event raised inside Sim, `webhook` when an external sender delivers to its URL." }, - "deployedAt": { + "provider": { "anyOf": [ { "type": "string" @@ -12602,56 +12844,280 @@ "type": "null" } ], - "description": "ISO 8601 timestamp associated with the deployment, or null when unavailable.", - "format": "date-time", - "examples": ["2026-06-12T10:30:00.000Z"] + "description": "Trigger provider, e.g. `gmail` or `github`; null for schedules." }, - "warnings": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Non-fatal warnings about deployment side effects, such as notifications that are still queued or failed. Empty when there is nothing to report. Lint findings are never reported here." + "enabled": { + "type": "boolean", + "description": "Whether the trigger currently fires." }, - "activeDeployment": { + "status": { + "type": "string", + "enum": ["active", "disabled", "completed"], + "description": "`disabled` after a manual disable or an automatic one following repeated failures. `completed` applies only to schedules that have no runs left." + }, + "failedCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Consecutive failures since the last success; the trigger disables itself at 100. Always 0 for `webhook` and `event` triggers." + }, + "lastFailedAt": { "anyOf": [ { - "$ref": "#/components/schemas/ActiveDeploymentSummary" + "type": "string" }, { "type": "null" } ], - "description": "Currently live deployment version, or null while no version is active." + "description": "ISO 8601 timestamp of the most recent counted failure, or null.", + "format": "date-time" }, - "latestDeploymentAttempt": { + "nextRunAt": { "anyOf": [ { - "$ref": "#/components/schemas/DeploymentOperationSummary" + "type": "string" }, { "type": "null" } ], - "description": "Most recent deployment lifecycle attempt, or null when none is available." + "description": "ISO 8601 timestamp of the next scheduled run; null when disabled and for non-schedules.", + "format": "date-time" }, - "version": { - "description": "Deployment version created for this attempt, when available.", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 + "lastRanAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp of the last scheduled run; null for non-schedules.", + "format": "date-time" }, - "lint": { + "cronExpression": { "anyOf": [ { - "$ref": "#/components/schemas/WorkflowLintReport" + "type": "string" }, { "type": "null" } ], - "description": "Advisory lint for the admitted version, checked as the deploying member. Findings never block deployment. Null when lint exceeds its time budget or cannot complete; the deployment proceeds." - } + "description": "Cron expression of a schedule; null for non-schedules." + }, + "timezone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "IANA timezone the cron expression runs in; null for non-schedules." + }, + "url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ], + "description": "Delivery URL of a `webhook` trigger; null for other kinds and for providers on a shared endpoint." + } + }, + "required": [ + "blockId", + "kind", + "provider", + "enabled", + "status", + "failedCount", + "lastFailedAt", + "nextRunAt", + "lastRanAt", + "cronExpression", + "timezone", + "url" + ], + "additionalProperties": false, + "title": "Workflow trigger", + "description": "Health and enablement of one trigger the live deployment serves." + }, + "WorkflowTriggerListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowTrigger" + }, + "description": "Items in the current page." + }, + "nextCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Always `null`. This list has no `cursor` or `limit` parameters; see the operation for inventory limits." + } + }, + "required": ["data", "nextCursor"], + "additionalProperties": false, + "title": "Workflow trigger list response", + "description": "Every trigger the live deployment serves.", + "examples": [ + { + "data": [ + { + "blockId": "blk_01J8ZK3QW4M6X2R9T7B5C0V4", + "kind": "schedule", + "provider": null, + "enabled": false, + "status": "disabled", + "failedCount": 100, + "lastFailedAt": "2026-08-09T18:04:11.000Z", + "nextRunAt": null, + "lastRanAt": "2026-08-09T18:04:00.000Z", + "cronExpression": "0 * * * *", + "timezone": "UTC", + "url": null + } + ], + "nextCursor": null + } + ] + }, + "UpdateWorkflowTriggerResponse": { + "type": "object", + "properties": { + "data": { + "description": "Response data.", + "$ref": "#/components/schemas/WorkflowTrigger" + } + }, + "required": ["data"], + "additionalProperties": false, + "title": "Update workflow trigger response", + "description": "The trigger after the update.", + "examples": [ + { + "data": { + "blockId": "blk_01J8ZK3QW4M6X2R9T7B5C0V4", + "kind": "schedule", + "provider": null, + "enabled": true, + "status": "active", + "failedCount": 0, + "lastFailedAt": "2026-08-09T18:04:11.000Z", + "nextRunAt": "2026-08-10T09:00:00.000Z", + "lastRanAt": "2026-08-09T18:04:00.000Z", + "cronExpression": "0 * * * *", + "timezone": "UTC", + "url": null + } + } + ] + }, + "UpdateWorkflowTriggerRequest": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether the trigger should fire. A disable lasts until the next deploy or version activation, which re-enables every trigger." + } + }, + "required": ["enabled"], + "additionalProperties": false, + "title": "Update workflow trigger request", + "description": "Enable or disable one trigger of the live deployment.", + "examples": [ + { + "enabled": true + } + ] + }, + "DeployResult": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique workflow identifier.", + "examples": ["3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36"] + }, + "isDeployed": { + "type": "boolean", + "description": "Whether a workflow version is currently live and available for API execution." + }, + "deployedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp associated with the deployment, or null when unavailable.", + "format": "date-time", + "examples": ["2026-06-12T10:30:00.000Z"] + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Non-fatal warnings about deployment side effects, such as notifications that are still queued or failed. Empty when there is nothing to report. Lint findings are never reported here." + }, + "activeDeployment": { + "anyOf": [ + { + "$ref": "#/components/schemas/ActiveDeploymentSummary" + }, + { + "type": "null" + } + ], + "description": "Currently live deployment version, or null while no version is active." + }, + "latestDeploymentAttempt": { + "anyOf": [ + { + "$ref": "#/components/schemas/DeploymentOperationSummary" + }, + { + "type": "null" + } + ], + "description": "Most recent deployment lifecycle attempt, or null when none is available." + }, + "version": { + "description": "Deployment version created for this attempt, when available.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "lint": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkflowLintReport" + }, + { + "type": "null" + } + ], + "description": "Advisory lint for the admitted version, checked as the deploying member. Findings never block deployment. Null when lint exceeds its time budget or cannot complete; the deployment proceeds." + } }, "required": [ "id", @@ -14451,7 +14917,7 @@ } }, "run": { - "description": "Workflow state and entry point to execute. Omit for the active deployment. Manual execution requires OAuth or personal-key write access and supports synchronous or streamed runs only; `waitTimeoutSeconds` lets a synchronous one continue in the background.", + "description": "Workflow state and entry point to execute. Omit for the active deployment; `source: \"deployment\"` with `version` runs that deployment version. Manual execution requires OAuth or personal-key write access and supports synchronous or streamed runs only; `waitTimeoutSeconds` lets a synchronous one continue in the background.", "oneOf": [ { "type": "object", @@ -14459,7 +14925,13 @@ "source": { "type": "string", "const": "deployment", - "description": "Execute the active deployed workflow state." + "description": "Execute a deployed workflow version." + }, + "version": { + "description": "Deployment version to execute instead of the active one. Requires a full-access API key or an OAuth access token with `api:write`, plus workspace write access, and the workflow must be deployed. An unknown version returns `404`.", + "type": "integer", + "minimum": 1, + "maximum": 2147483647 } }, "required": ["source"], @@ -14534,7 +15006,7 @@ }, "async": { "default": false, - "description": "Queue the run and return a 202 receipt. Requires an API key or OAuth access token and a deployed run source. Cannot stream, select outputs, enable thinking/tool events, or include file-inlining options (`includeFileBase64` or `base64MaxBytes`).", + "description": "Queue the run and return a 202 receipt. Requires a full-access API key or an OAuth access token with `api:write`, and a deployed run source. Cannot stream, select outputs, enable thinking/tool events, or include file-inlining options (`includeFileBase64` or `base64MaxBytes`).", "type": "boolean" }, "executionTimeoutSeconds": { @@ -14544,7 +15016,7 @@ "maximum": 604800 }, "waitTimeoutSeconds": { - "description": "Wait at most this many seconds for a synchronous run; `0` waits until it settles. A run still going then continues in the background and the response carries its run receipt instead of its result: `202` on a JSON response, or a closing `detached` frame on an `application/x-ndjson` result stream, which keeps its `200`. The receipt is sent only once the run can be read on the runs resource, where it can also be cancelled. The run is admitted under asynchronous limits, and a dropped connection no longer cancels it. Requires an API key or OAuth access token. Cannot be combined with `async` or `stream`.", + "description": "Wait at most this many seconds for a synchronous run; `0` waits until it settles. A run still going then continues in the background and the response carries its run receipt instead of its result: `202` on a JSON response, or a closing `detached` frame on an `application/x-ndjson` result stream, which keeps its `200`. The receipt is sent only once the run can be read on the runs resource, where it can also be cancelled. The run is admitted under asynchronous limits, and a dropped connection no longer cancels it. Requires a full-access API key or an OAuth access token with `api:write`. Cannot be combined with `async` or `stream`.", "type": "integer", "minimum": 0, "maximum": 604800 @@ -14779,6 +15251,222 @@ } ] }, + "WorkflowRunPausePoint": { + "type": "object", + "properties": { + "contextId": { + "type": "string", + "description": "Pause context to name in `contextId` when resuming this point." + }, + "blockId": { + "type": "string", + "description": "Workflow block that paused." + }, + "blockName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Name of the block in the deployment version the run executed, or null for a manual run or when the version no longer exists." + }, + "kind": { + "type": "string", + "enum": ["human", "time"], + "description": "`human` waits for a resume request; `time` resumes automatically at `resumeAt`." + }, + "status": { + "type": "string", + "enum": ["paused", "queued", "resuming", "resumed", "failed"], + "description": "Resume state of this point: waiting, queued behind another resume, resuming, resumed, or a failed resume attempt." + }, + "registeredAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp when the run paused at this point, or null for a point recorded without one.", + "format": "date-time" + }, + "resumeAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 automatic-resume timestamp for a time pause, otherwise null.", + "format": "date-time" + }, + "parallel": { + "anyOf": [ + { + "type": "object", + "properties": { + "parallelId": { + "type": "string", + "description": "Parallel block containing the pause." + }, + "branchIndex": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Zero-based branch that paused." + }, + "branchTotal": { + "anyOf": [ + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ], + "description": "Number of branches, or null when unrecorded." + } + }, + "required": ["parallelId", "branchIndex", "branchTotal"], + "additionalProperties": false + }, + { + "type": "null" + } + ], + "description": "Parallel branch of the pause, or null outside a parallel." + }, + "loop": { + "anyOf": [ + { + "type": "object", + "properties": { + "loopId": { + "type": "string", + "description": "Loop block containing the pause." + }, + "iteration": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Zero-based iteration that paused." + } + }, + "required": ["loopId", "iteration"], + "additionalProperties": false + }, + { + "type": "null" + } + ], + "description": "Loop iteration of the pause, or null outside a loop." + }, + "inputFields": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowRunPauseInputField" + } + }, + { + "type": "null" + } + ], + "description": "Fields a human pause asks for, without default values. Labels and descriptions are resolved, so they are withheld when organization permission groups hide execution data. Null then, for a time pause, a pause with no declared form, fields over 64 KiB or not provably free of secrets, or one recorded before pause details were captured." + }, + "display": { + "anyOf": [ + { + "description": "Reviewer content value." + }, + { + "type": "null" + } + ], + "description": "Content the pause presents to its reviewer, with the run's resolved secrets redacted. Null unless `includeOutput=true`, when hidden by organization permission groups, for a time pause, for content over 64 KiB or not provably free of secrets, or for a pause recorded before pause details were captured." + }, + "queuePosition": { + "anyOf": [ + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ], + "description": "Position among pending resume requests for this run, or null when none waits." + } + }, + "required": [ + "contextId", + "blockId", + "blockName", + "kind", + "status", + "registeredAt", + "resumeAt", + "parallel", + "loop", + "inputFields", + "display", + "queuePosition" + ], + "additionalProperties": false, + "title": "Workflow run pause point", + "description": "One point at which a paused run waits." + }, + "WorkflowRunPauseInputField": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Key to supply this field under in the resume `input`." + }, + "label": { + "type": "string", + "description": "Display label for the field." + }, + "type": { + "type": "string", + "description": "Declared value type, such as `string`, `number`, or `boolean`." + }, + "description": { + "description": "Field help text, when configured.", + "type": "string" + }, + "placeholder": { + "description": "Placeholder text, when configured.", + "type": "string" + }, + "required": { + "type": "boolean", + "description": "Whether the field is marked required." + }, + "options": { + "description": "Selectable options, when the field offers a fixed set.", + "type": "array", + "items": { + "description": "One selectable option." + } + } + }, + "required": ["name", "label", "type", "required"], + "additionalProperties": false, + "title": "Pause input field", + "description": "One field a human pause asks the resumer to fill." + }, "V2RunFile": { "type": "object", "properties": { @@ -14896,6 +15584,19 @@ ], "description": "Run duration in milliseconds, or null while active." }, + "version": { + "anyOf": [ + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ], + "description": "Deployment version the run executed. Null for a manual run, a queued run not yet started, or a run whose version no longer exists." + }, "paused": { "anyOf": [ { @@ -14972,6 +15673,13 @@ "resumedCount": { "type": "number", "description": "Number of pause points that have resumed." + }, + "points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowRunPausePoint" + }, + "description": "Every point the run is paused at, earliest first, at most 100. Resume one with Resume workflow run, naming its `contextId` and supplying `input`." } }, "required": [ @@ -14982,7 +15690,8 @@ "blockedOnBlockId", "automaticResumeWaitingReason", "pausePointCount", - "resumedCount" + "resumedCount", + "points" ], "additionalProperties": false }, @@ -15073,6 +15782,7 @@ "startedAt", "endedAt", "durationMs", + "version", "paused", "cost", "error", @@ -15106,6 +15816,7 @@ "startedAt": "2026-08-09T18:04:10.000Z", "endedAt": "2026-08-09T18:04:11.000Z", "durationMs": 1000, + "version": 3, "paused": null, "cost": { "total": 12 @@ -19030,6 +19741,8 @@ "planetscale.deployRequests", "cloudwatch.logGroups", "cloudwatch.logStreams", + "cloudwatch.savedLogGroups", + "cloudwatch.savedLogStreams", "imap.mailboxes", "mcp.tools", "managedAgent.agents", @@ -19227,6 +19940,8 @@ "planetscale.deployRequests", "cloudwatch.logGroups", "cloudwatch.logStreams", + "cloudwatch.savedLogGroups", + "cloudwatch.savedLogStreams", "imap.mailboxes", "mcp.tools", "managedAgent.agents", diff --git a/apps/docs/package.json b/apps/docs/package.json index e1d7a1c29b2..013cdd66b7d 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -39,11 +39,11 @@ "react": "19.2.4", "react-dom": "19.2.4", "remark-gfm": "4.0.1", - "shiki": "4.5.0", + "shiki": "4.6.0", "framer-motion": "^12.5.0", "zod": "4.3.6", "@mdx-js/mdx": "3.1.1", - "@modelcontextprotocol/server": "2.3.1", + "@modelcontextprotocol/server": "2.4.0", "@radix-ui/react-tabs": "1.2.1", "@radix-ui/react-dialog": "1.3.1", "@radix-ui/react-dropdown-menu": "2.2.1", @@ -54,8 +54,8 @@ "remark-mdx": "3.1.1", "remark-parse": "11.0.0", "unified": "11.0.5", - "unist-util-visit": "5.1.0", - "@shikijs/rehype": "4.5.0", + "unist-util-visit": "5.2.0", + "@shikijs/rehype": "4.6.0", "server-only": "0.0.1", "postgres": "3.4.9", "remark-rehype": "11.1.2", @@ -77,8 +77,8 @@ "postcss": "^8.5.30", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", - "vitest": "^5.0.4", - "@modelcontextprotocol/client": "2.3.1", + "vitest": "^5.1.0", + "@modelcontextprotocol/client": "2.4.0", "@types/mdast": "4.0.4", "@types/mdx": "2.0.14", "htmlparser2": "10.1.0", diff --git a/apps/docs/scripts/fixtures/content-baseline.json b/apps/docs/scripts/fixtures/content-baseline.json index 80b2cab120d..6694ecde8c8 100644 --- a/apps/docs/scripts/fixtures/content-baseline.json +++ b/apps/docs/scripts/fixtures/content-baseline.json @@ -648,15 +648,11 @@ "/platform/costs", "#oauth-access-tokens", "/api-reference/getting-started", - "/api-reference/python", - "/api-reference/typescript", "/cli/authentication", "/platform/self-hosting/environment-variables", "/api-reference/getting-started", "/cli/authentication", - "/platform/permissions", - "/api-reference/python", - "/api-reference/typescript" + "/platform/permissions" ], "media": [] }, @@ -904,8 +900,6 @@ ], "links": [ "/openapi.json", - "/api-reference/typescript", - "/api-reference/python", "/cli", "/api-reference/authentication", "/api-reference/workflows/listWorkflows", @@ -916,585 +910,11 @@ "/workflows/deployment/api#streaming", "/api-reference/billing/getBillingStatus", "/api-reference/authentication", - "/api-reference/python", - "/api-reference/typescript", "/api-reference/workflows/executeWorkflowV2", "/api-reference/workflow-sync" ], "media": [] }, - { - "source": "api-reference/python.mdx", - "url": "/api-reference/python", - "sha256": "f6fdd02aa4a75ffe368e50de6a4d7c26302801af897a1a25263a26fc41e869ad", - "bytes": 20330, - "frontmatter": { - "title": "Python", - "description": "Execute deployed workflows from Python, poll runs, and handle SDK results." - }, - "headings": [ - { - "title": "Install and configure", - "url": "#installation", - "depth": 2 - }, - { - "title": "Server compatibility", - "url": "#server-compatibility", - "depth": 3 - }, - { - "title": "Run a workflow", - "url": "#quick-start", - "depth": 2 - }, - { - "title": "Start an asynchronous run", - "url": "#async-workflow-execution", - "depth": 2 - }, - { - "title": "Handle errors and rate limits", - "url": "#error-handling", - "depth": 2 - }, - { - "title": "Method reference", - "url": "#api-reference", - "depth": 2 - }, - { - "title": "Constructor", - "url": "#constructor", - "depth": 3 - }, - { - "title": "Execute", - "url": "#execute", - "depth": 3 - }, - { - "title": "Read workflow deployment and runs", - "url": "#read-workflow-deployment-and-runs", - "depth": 3 - }, - { - "title": "Read limits and update the client", - "url": "#read-limits-and-update-the-client", - "depth": 3 - }, - { - "title": "Return objects", - "url": "#data-classes", - "depth": 2 - }, - { - "title": "WorkflowExecutionResult", - "url": "#workflowexecutionresult", - "depth": 3 - }, - { - "title": "AsyncExecutionResult", - "url": "#asyncexecutionresult", - "depth": 3 - }, - { - "title": "WorkflowStatus", - "url": "#workflowstatus", - "depth": 3 - }, - { - "title": "Run dictionary", - "url": "#run-dictionary", - "depth": 3 - }, - { - "title": "RateLimitInfo", - "url": "#ratelimitinfo", - "depth": 3 - }, - { - "title": "UsageLimits", - "url": "#usagelimits", - "depth": 3 - }, - { - "title": "File inputs", - "url": "#file-inputs", - "depth": 2 - }, - { - "title": "Execute several workflows", - "url": "#batch-workflow-execution", - "depth": 2 - }, - { - "title": "Stream through HTTP", - "url": "#streaming-workflow-execution", - "depth": 2 - }, - { - "title": "Upgrade from 0.1.x", - "url": "#upgrade-from-01x", - "depth": 2 - }, - { - "title": "Related docs", - "url": "#related-docs", - "depth": 2 - } - ], - "explicitIds": [], - "imports": [ - { - "source": "fumadocs-ui/components/callout", - "names": [ - { - "local": "Callout", - "imported": "Callout" - } - ] - } - ], - "components": [ - { - "name": "Callout", - "attributes": { - "type": "warn" - }, - "line": 137 - } - ], - "expressions": [], - "codeFences": [ - { - "lang": "bash", - "meta": null, - "sha256": "4b7a6f45be2afc4518f3a3339c7baee59d88b0a655b96e3da44bfbd8ac95be3e", - "bytes": 25, - "line": 10 - }, - { - "lang": "python", - "meta": null, - "sha256": "eb1a4a2d1885ba1013feb4097beae8f667870d1d2222ff2e391d7c03a798ce65", - "bytes": 776, - "line": 26 - }, - { - "lang": "python", - "meta": null, - "sha256": "c1677ae09cc712a7f53ad7abb04b64d2c905deca70ab5d2571ee195635b01c28", - "bytes": 1119, - "line": 60 - }, - { - "lang": "python", - "meta": null, - "sha256": "1202876d901ed0abbf20cc69d0ac3ee05ab900395fc5a491fdc3f69e17f65492", - "bytes": 385, - "line": 118 - }, - { - "lang": "text", - "meta": null, - "sha256": "46d4f38bdd673c87afe3e8b4832faa98226e07d23344b9385ba04a7658047a5d", - "bytes": 67, - "line": 145 - }, - { - "lang": "text", - "meta": null, - "sha256": "340adca5c4921786619fd14319bf0498b6e25961eba5b82e91a5595f83f3e6a3", - "bytes": 645, - "line": 153 - }, - { - "lang": "text", - "meta": null, - "sha256": "6be2019327342bcddbae35924c8e7f00236552ca4a3b8bf44880ae47c5e157b0", - "bytes": 262, - "line": 187 - }, - { - "lang": "python", - "meta": null, - "sha256": "7d50e2341d850611f67296e8ed7fb2a63f3b79a02627c8cfec1f29ec02be2d10", - "bytes": 103, - "line": 197 - }, - { - "lang": "text", - "meta": null, - "sha256": "93613572aa1c2d5e83ff53b54dbd4f00d389c7e9163f9bb7d5c7b22fa085e55e", - "bytes": 165, - "line": 208 - }, - { - "lang": "python", - "meta": null, - "sha256": "a0173b58cee71b947bc2acb4437c6a7a6236b27af78436b7d2d9bd731ac4742c", - "bytes": 181, - "line": 276 - }, - { - "lang": "text", - "meta": null, - "sha256": "9afcfcab96aabedc5778d0e629ec1836a52741196772f7a36575bbe6b4368ecb", - "bytes": 252, - "line": 291 - }, - { - "lang": "python", - "meta": null, - "sha256": "c54b0954c8c12ba9e3d97566ee379c55e1620206faac796ad9fc16db6c8d1d34", - "bytes": 221, - "line": 308 - }, - { - "lang": "python", - "meta": null, - "sha256": "27f4dad755ed9478fa1cebc32bf3a98df7a560d5eafc3b8aa5ad801aa6539504", - "bytes": 366, - "line": 321 - }, - { - "lang": "python", - "meta": null, - "sha256": "6be28d212d424441cfca53571a5470674b9d235ae19d5e3618edc902aa2f2b82", - "bytes": 381, - "line": 340 - }, - { - "lang": "python", - "meta": null, - "sha256": "c98622c3e36c7d24f58009561ed68a8851792efeb85fe826e7b11ed18d8438c0", - "bytes": 570, - "line": 358 - } - ], - "links": [ - "/api-reference/authentication", - "/api-reference/workflow-runs/getWorkflowRunV2", - "/api-reference/workflow-runs/resumeWorkflowRunV2", - "/api-reference/getting-started#error-handling", - "/api-reference/getting-started#sync-vs-async-execution", - "/workflows/deployment/api#oversized-outputs", - "/api-reference/workflow-runs/getWorkflowRunV2", - "/api-reference/billing/getBillingStatus", - "/workflows/deployment/api#streaming", - "/workflows/deployment/agent-events#reference-client", - "/api-reference/authentication", - "/api-reference/typescript", - "/api-reference/getting-started", - "/api-reference/workflow-runs/getWorkflowRunV2", - "https://github.com/simstudioai/sim/tree/main/packages/python-sdk" - ], - "media": [] - }, - { - "source": "api-reference/typescript.mdx", - "url": "/api-reference/typescript", - "sha256": "48c4610c3e883157facf2464d234977fdcc3400d910681c8dd13534bd20f806d", - "bytes": 22085, - "frontmatter": { - "title": "TypeScript", - "description": "Execute deployed workflows from a server, poll runs, and handle SDK results." - }, - "headings": [ - { - "title": "Install and configure", - "url": "#installation", - "depth": 2 - }, - { - "title": "Server compatibility", - "url": "#server-compatibility", - "depth": 3 - }, - { - "title": "Run a workflow", - "url": "#quick-start", - "depth": 2 - }, - { - "title": "Start an asynchronous run", - "url": "#async-workflow-execution", - "depth": 2 - }, - { - "title": "Handle errors and retries", - "url": "#error-handling", - "depth": 2 - }, - { - "title": "Method reference", - "url": "#api-reference", - "depth": 2 - }, - { - "title": "Constructor", - "url": "#constructor", - "depth": 3 - }, - { - "title": "Execute", - "url": "#execute", - "depth": 3 - }, - { - "title": "Read deployment and runs", - "url": "#read-deployment-and-runs", - "depth": 3 - }, - { - "title": "Read limits and update the client", - "url": "#read-limits-and-update-the-client", - "depth": 3 - }, - { - "title": "Return types", - "url": "#types", - "depth": 2 - }, - { - "title": "WorkflowExecutionResult", - "url": "#workflowexecutionresult", - "depth": 3 - }, - { - "title": "AsyncExecutionResult", - "url": "#asyncexecutionresult", - "depth": 3 - }, - { - "title": "WorkflowStatus", - "url": "#workflowstatus", - "depth": 3 - }, - { - "title": "WorkflowRunStatus", - "url": "#workflowrunstatus", - "depth": 3 - }, - { - "title": "JobStatusResult", - "url": "#jobstatusresult", - "depth": 3 - }, - { - "title": "LargeValueRef", - "url": "#largevalueref", - "depth": 3 - }, - { - "title": "RateLimitInfo", - "url": "#ratelimitinfo", - "depth": 3 - }, - { - "title": "UsageLimits", - "url": "#usagelimits", - "depth": 3 - }, - { - "title": "File inputs", - "url": "#file-upload", - "depth": 2 - }, - { - "title": "Use from a web backend", - "url": "#browser-usage", - "depth": 2 - }, - { - "title": "Express integration", - "url": "#nodejs-express-integration", - "depth": 3 - }, - { - "title": "Next.js API route", - "url": "#nextjs-api-route", - "depth": 3 - }, - { - "title": "Stream through HTTP", - "url": "#streaming-workflow-execution", - "depth": 2 - }, - { - "title": "Upgrade from 0.1.x", - "url": "#upgrade-from-01x", - "depth": 2 - }, - { - "title": "Related docs", - "url": "#related-docs", - "depth": 2 - } - ], - "explicitIds": [], - "imports": [ - { - "source": "fumadocs-ui/components/callout", - "names": [ - { - "local": "Callout", - "imported": "Callout" - } - ] - } - ], - "components": [ - { - "name": "Callout", - "attributes": { - "type": "warn" - }, - "line": 127 - } - ], - "expressions": [], - "codeFences": [ - { - "lang": "bash", - "meta": null, - "sha256": "a3cd775cbb4aa2b9f93b0b048abc680f916c675c3090fa6d20a479a526f9e456", - "bytes": 24, - "line": 10 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "76a9e38658aa530d71622cd2bcfdca46679fcd77e11e5daf5e4d22fa16477c90", - "bytes": 925, - "line": 24 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "494e2b1fff12dc593c20c12f14acfbb862ce4f3793806d5e227fcea74f3d5cbb", - "bytes": 1153, - "line": 61 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "2439dcdf61021131759b26013a22208cad2986d48137bb2270f74a82b15c2b2a", - "bytes": 243, - "line": 113 - }, - { - "lang": "text", - "meta": null, - "sha256": "c2167de31fcd5b335ea7afa84f45e3be314bee52b29de78239ff3bda981c104d", - "bytes": 44, - "line": 135 - }, - { - "lang": "text", - "meta": null, - "sha256": "f8a8c3d343140543b5b2330c2e8721c846e05a2ebba473ef2db1fc77986f64ca", - "bytes": 428, - "line": 143 - }, - { - "lang": "text", - "meta": null, - "sha256": "e73d36e02d17bb95dce1e7a60f7a2c8b1109bf96f72ad249959a33f2734f4d21", - "bytes": 293, - "line": 168 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "811d8583f3d1f4655b4c70914b229fce499c7002c17e1856dc12567d29ff4439", - "bytes": 113, - "line": 178 - }, - { - "lang": "text", - "meta": null, - "sha256": "cb34f55a09b67eb5cf3faf9d34cd8b63f1438b19106f616d308ca33a8854e090", - "bytes": 153, - "line": 189 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "ef2e937be62740a50aeb99c136cb8afa24298ea28810baf04a84874b7e66248c", - "bytes": 153, - "line": 258 - }, - { - "lang": "text", - "meta": null, - "sha256": "2b44123a52d64b62c138532058bc04ce2ecb964f9a75ec4f12298af49fb35794", - "bytes": 378, - "line": 269 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "6fca0e1fe5c65eff8153bfa027319556df2ccbcfd4b0a1eff06c72ea6fd02f43", - "bytes": 265, - "line": 291 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "f59b121b324660695e548c3d13bd0e572b623f4b79e24d6f1317c5b125119c96", - "bytes": 141, - "line": 307 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "3f33d121a0e6bcec039e6c20490cb1641bf21b8196731825ef9625e2de749424", - "bytes": 129, - "line": 318 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "a5c27589fcc685ba165109d7cd983d306049dcd3753d95a747decd6401e763c1", - "bytes": 616, - "line": 331 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "e5e40b323691e88e6b2e4632e58e4e515b7e8869cb75976d63c274a96f05352c", - "bytes": 171, - "line": 356 - }, - { - "lang": "typescript", - "meta": null, - "sha256": "66957c241bc7f84b68bfd41dc3b1270c1e3ecd2d8b5acd27fde2f25c3b3763fb", - "bytes": 991, - "line": 374 - } - ], - "links": [ - "/api-reference/authentication", - "/api-reference/workflow-runs/getWorkflowRunV2", - "/api-reference/workflow-runs/resumeWorkflowRunV2", - "/api-reference/getting-started#error-handling", - "/api-reference/getting-started#sync-vs-async-execution", - "/api-reference/workflow-runs/getWorkflowRunV2", - "/workflows/deployment/api#oversized-outputs", - "/api-reference/billing/getBillingStatus", - "/workflows/deployment/api#streaming", - "/workflows/deployment/agent-events#reference-client", - "/api-reference/authentication", - "/api-reference/python", - "/api-reference/getting-started", - "/api-reference/workflow-runs/getWorkflowRunV2", - "https://github.com/simstudioai/sim/tree/main/packages/ts-sdk" - ], - "media": [] - }, { "source": "api-reference/workflow-sync.mdx", "url": "/api-reference/workflow-sync", @@ -3577,70 +2997,91 @@ "meta": null, "sha256": "e407b3388ac13c30e985d64e41a9fb8f68c52e6e7a020bc3db79ce9473944724", "bytes": 40, - "line": 16 + "line": 20 }, { "lang": "bash", "meta": null, "sha256": "cb3d67be4a05a8cc95870cadb5ac682c16e80889db6606e388dd48651bfc4af4", "bytes": 30, - "line": 34 + "line": 38 }, { "lang": "bash", "meta": null, "sha256": "3ce249d6b52442bdf5df53436bae6b9718064994b4b26ecf25372ce53606db08", "bytes": 45, - "line": 59 + "line": 63 + }, + { + "lang": "bash", + "meta": null, + "sha256": "48d4019bc1905094aec776873af1d5a4e94ec2f52d043c51bbbb559b6a876944", + "bytes": 276, + "line": 102 }, { "lang": "bash", "meta": null, "sha256": "e0ac83da55217195ac0ca3bd1a41314be433d0a62428f36eb9666df607e8322c", "bytes": 46, - "line": 92 + "line": 114 }, { "lang": "bash", "meta": null, "sha256": "664dfa9d010be73d8c8d4c1b079e946a646cc4677891d5fd34df9e5817cabfed", "bytes": 40, - "line": 122 + "line": 144 }, { "lang": "bash", "meta": null, "sha256": "f7e2cb3ed34992b91cdcbd2fa65eadb48134c31e0d295519421644ad0668f20e", "bytes": 47, - "line": 142 + "line": 164 }, { "lang": "bash", "meta": null, "sha256": "4051f5bb25072a25196ba8e847ab22b9d42aeb0cbb36186b5e7cc051d77c00e0", "bytes": 53, - "line": 196 + "line": 219 }, { "lang": "bash", "meta": null, "sha256": "a74b652635bc27ca58af7b080b78d983469558a2668f0b3f033a1b5b2f4cef10", "bytes": 55, - "line": 228 + "line": 251 }, { "lang": "bash", "meta": null, "sha256": "2b6d0db0f06b8f51733c7da29a0c183aab73a6da83b787db76e7c3064fc9c78c", "bytes": 64, - "line": 261 + "line": 284 }, { "lang": "bash", "meta": null, "sha256": "ef5100ffa39072ae2ba9bb021d546036b95ded8da431fad855f809550c8913fb", "bytes": 47, - "line": 296 + "line": 319 + }, + { + "lang": "bash", + "meta": null, + "sha256": "57a7270a0eaa11ca8fe83eedaf7846ab9193ecbc2e2a1d2189f5a350d7881fdc", + "bytes": 44, + "line": 351 + }, + { + "lang": "bash", + "meta": null, + "sha256": "48d4019bc1905094aec776873af1d5a4e94ec2f52d043c51bbbb559b6a876944", + "bytes": 276, + "line": 375 } ], "links": [ @@ -4739,8 +4180,6 @@ "/cli/scripting", "/cli/workflow-sync", "/cli/troubleshooting", - "/api-reference/typescript", - "/api-reference/python", "/api-reference/getting-started" ], "media": [] @@ -6746,6 +6185,20 @@ "bytes": 51, "line": 201 }, + { + "lang": "bash", + "meta": null, + "sha256": "229448ff3c47ffdfdd720fb88b38edcf58f51d063f7b7836d4f4fdc478f99b5c", + "bytes": 69, + "line": 238 + }, + { + "lang": "bash", + "meta": null, + "sha256": "7863a0cbd4e2cb3715358cca55047765a210f9a42a3dd171e42f574185fe0ab9", + "bytes": 73, + "line": 270 + }, { "lang": "bash", "meta": null, @@ -11529,2275 +10982,2338 @@ "meta": null, "sha256": "e89f5163bfcc02bcec8be44abb82d7807ccaeecc397ea8b3d8575e189f05a46a", "bytes": 19, - "line": 36 + "line": 40 }, { "lang": "bash", "meta": null, "sha256": "3ce59639490b19f90613bf0b002f0d5f747ae6bf89110a31aaad132f4a5d56df", "bytes": 20, - "line": 58 + "line": 62 }, { "lang": "bash", "meta": null, "sha256": "7be8dbf1bad323118735b5a0b4cdf49c653cc20508516225fe058776f0421e2e", "bytes": 20, - "line": 76 + "line": 80 }, { "lang": "bash", "meta": null, "sha256": "5b13d0a219062e58827d50697e4cec707e48ebef466a5058abb6e524376cc18e", "bytes": 23, - "line": 96 + "line": 100 }, { "lang": "bash", "meta": null, "sha256": "fac9741c4ccc5f0596cba54e6e2ecc4997e489032315eb4c80dd872c941dd0ae", "bytes": 20, - "line": 117 + "line": 121 }, { "lang": "bash", "meta": null, "sha256": "cc2a546bb69dcb90170c414517e67c86b5a20c7918c28999cb06d15f19ad4774", "bytes": 28, - "line": 135 + "line": 139 }, { "lang": "bash", "meta": null, "sha256": "f22525d91b0a915d24281bdac40a70c066449d9310582f844446e4fb2d9b347f", "bytes": 181, - "line": 163 + "line": 167 }, { "lang": "bash", "meta": null, "sha256": "21d922f40e072101cbcfdfcd4ee3c6e9ca22fc35bc4828a37f79f57dafec8d02", "bytes": 17, - "line": 181 + "line": 185 }, { "lang": "bash", "meta": null, "sha256": "56b302d091c4b589dde86d24739634d8f7d560873ca281d9962939aa6ff46ac1", "bytes": 33, - "line": 189 + "line": 193 }, { "lang": "bash", "meta": null, "sha256": "ce318a355b5dabeedbde09c7ddeb4f9132a4a57501b15fd3c2d23b3b8c75fd7f", "bytes": 20, - "line": 223 + "line": 227 }, { "lang": "bash", "meta": null, "sha256": "c74ad02c1945ec9c5cb8316f5ffeb64cc0b9dde01530570343e2dd2ee5c2ec23", "bytes": 20, - "line": 233 + "line": 237 }, { "lang": "bash", "meta": null, "sha256": "abe19c862f574d48aa91f6a506496b6f6c414d03fab399a6c86e1c12b2f8302a", "bytes": 21, - "line": 241 + "line": 245 }, { "lang": "bash", "meta": null, "sha256": "5c4299cf8aa6117a3a1a3c728d72c2a86f9627a5766d47be8ae1f03088642346", "bytes": 25, - "line": 253 + "line": 257 }, { "lang": "bash", "meta": null, "sha256": "cf03b326f03d9bcb4260e2be1ca5bcb89551a8f7f2bb1ba85744f50a9a84b434", "bytes": 73, - "line": 271 + "line": 275 }, { "lang": "bash", "meta": null, "sha256": "1350c2e26838e2b4a68726a51de8a06e2a5d32cc9714e0171c0dafd867db1d90", "bytes": 41, - "line": 288 + "line": 292 }, { "lang": "bash", "meta": null, "sha256": "8b747ef16c178bb05ca7171ec48e801f20a250e774a44d2d3b9753bd69aa3ec2", "bytes": 29, - "line": 318 + "line": 322 }, { "lang": "bash", "meta": null, "sha256": "88526e37fa1577427e6126306dc69d3afa94894d3dd313ae31286bff5eadafa4", "bytes": 28, - "line": 353 + "line": 357 }, { "lang": "bash", "meta": null, "sha256": "437c1deab5031ec9ce664b5429f7b62c062fd592469411b7f61cc16a9e21aa15", "bytes": 26, - "line": 373 + "line": 377 }, { "lang": "bash", "meta": null, "sha256": "575f3eca46c258a161e39e0bb8154f4e8ea02ed85bc48ba09e313bad08b2bdd7", "bytes": 24, - "line": 405 + "line": 409 }, { "lang": "bash", "meta": null, "sha256": "ecc11df2b60a4b0dc7d6b4e95f6fbee1f43554a8e32ee7232d50543a94e22273", "bytes": 25, - "line": 425 + "line": 429 }, { "lang": "bash", "meta": null, "sha256": "451b8c0648b69af0d37673c7b6c4bd98eb8ce4baca0c902038fc1460938adf88", "bytes": 35, - "line": 457 + "line": 461 }, { "lang": "bash", "meta": null, "sha256": "5037ed0778643f28636735f01bb24cd5f938bdc923f02abfacf6e37840a822f7", "bytes": 34, - "line": 488 + "line": 492 }, { "lang": "bash", "meta": null, "sha256": "ef5100ffa39072ae2ba9bb021d546036b95ded8da431fad855f809550c8913fb", "bytes": 47, - "line": 521 + "line": 525 }, { "lang": "bash", "meta": null, "sha256": "4051f5bb25072a25196ba8e847ab22b9d42aeb0cbb36186b5e7cc051d77c00e0", "bytes": 53, - "line": 551 + "line": 555 }, { "lang": "bash", "meta": null, "sha256": "2b6d0db0f06b8f51733c7da29a0c183aab73a6da83b787db76e7c3064fc9c78c", "bytes": 64, - "line": 585 + "line": 589 }, { "lang": "bash", "meta": null, "sha256": "a74b652635bc27ca58af7b080b78d983469558a2668f0b3f033a1b5b2f4cef10", "bytes": 55, - "line": 616 + "line": 620 }, { "lang": "bash", "meta": null, "sha256": "e407b3388ac13c30e985d64e41a9fb8f68c52e6e7a020bc3db79ce9473944724", "bytes": 40, - "line": 647 + "line": 651 }, { "lang": "bash", "meta": null, "sha256": "cb3d67be4a05a8cc95870cadb5ac682c16e80889db6606e388dd48651bfc4af4", "bytes": 30, - "line": 667 + "line": 671 }, { "lang": "bash", "meta": null, "sha256": "f7e2cb3ed34992b91cdcbd2fa65eadb48134c31e0d295519421644ad0668f20e", "bytes": 47, - "line": 692 + "line": 696 }, { "lang": "bash", "meta": null, "sha256": "3ce249d6b52442bdf5df53436bae6b9718064994b4b26ecf25372ce53606db08", "bytes": 45, - "line": 744 + "line": 749 + }, + { + "lang": "bash", + "meta": null, + "sha256": "48d4019bc1905094aec776873af1d5a4e94ec2f52d043c51bbbb559b6a876944", + "bytes": 276, + "line": 788 + }, + { + "lang": "bash", + "meta": null, + "sha256": "57a7270a0eaa11ca8fe83eedaf7846ab9193ecbc2e2a1d2189f5a350d7881fdc", + "bytes": 44, + "line": 800 + }, + { + "lang": "bash", + "meta": null, + "sha256": "48d4019bc1905094aec776873af1d5a4e94ec2f52d043c51bbbb559b6a876944", + "bytes": 276, + "line": 824 }, { "lang": "bash", "meta": null, "sha256": "e0ac83da55217195ac0ca3bd1a41314be433d0a62428f36eb9666df607e8322c", "bytes": 46, - "line": 777 + "line": 836 }, { "lang": "bash", "meta": null, "sha256": "664dfa9d010be73d8c8d4c1b079e946a646cc4677891d5fd34df9e5817cabfed", "bytes": 40, - "line": 807 + "line": 866 }, { "lang": "bash", "meta": null, "sha256": "55d8bb7d28001fafb0a76ccaf87863cb67f0150ffef73889e5c363ce3775131b", "bytes": 33, - "line": 833 + "line": 892 }, { "lang": "bash", "meta": null, "sha256": "02ff8ad73ece2f21edb90fc1a69904f322032da6d90a23d28d2d490beb55ab53", "bytes": 48, - "line": 857 + "line": 916 }, { "lang": "bash", "meta": null, "sha256": "06a7f6e71ab68db991126dcf2d660866aec7b89d27f751d082d2f90030a19b00", "bytes": 35, - "line": 887 + "line": 946 }, { "lang": "bash", "meta": null, "sha256": "3c9e74b86271246976e186b32c311a50f367fcd701f0506a56c8c8150c01b400", "bytes": 31, - "line": 907 + "line": 966 }, { "lang": "bash", "meta": null, "sha256": "c0bbfff139a69379e188cfe4bbd3393285fce565dd83f7098eb2d387356da9c4", "bytes": 48, - "line": 930 + "line": 989 }, { "lang": "bash", "meta": null, "sha256": "c569c5e38502a1c1e8f6078954bc90b559dd80adb577bb4cf7b63ec7bbdb3aa7", "bytes": 46, - "line": 970 + "line": 1029 }, { "lang": "bash", "meta": null, "sha256": "939a624f32ea05ac4cddd6037a27cdc7829018f0ac8096e4afd85aa9fab93ea2", "bytes": 24, - "line": 991 + "line": 1060 }, { "lang": "bash", "meta": null, "sha256": "428eca668e88f6323ab89a2e488c5fd403b8b04064b26b55a3b8ca7ddb966be5", "bytes": 26, - "line": 1010 + "line": 1079 }, { "lang": "bash", "meta": null, "sha256": "66b86826e7fbeb5cc1b35c54dcbf9727264c2b6d50528e8c21accaff3a708806", "bytes": 31, - "line": 1034 + "line": 1103 }, { "lang": "bash", "meta": null, "sha256": "a461b9c9fef3090d3367148a2a183410e81624e42e340c21a4138c30c6b4409c", "bytes": 41, - "line": 1056 + "line": 1125 }, { "lang": "bash", "meta": null, "sha256": "8c86de167a3b73c5430d2b15f558b7c0b2a82cd347b4e85907862196cf8e16bc", "bytes": 32, - "line": 1087 + "line": 1156 }, { "lang": "bash", "meta": null, "sha256": "8a9eb6688329292f21fecba059890d65dab0a136e86f26d38987c27d68013ad8", "bytes": 43, - "line": 1115 + "line": 1184 }, { "lang": "bash", "meta": null, "sha256": "be3c635d1b158a14e8e00c6e88d84e4126e29d89f0464ebf0b296f19b2bdf59b", "bytes": 32, - "line": 1138 + "line": 1207 }, { "lang": "bash", "meta": null, "sha256": "993262e619d4470867077b7a4e0c528f715be468511bf9e072e8a9429ef250a1", "bytes": 35, - "line": 1160 + "line": 1229 }, { "lang": "bash", "meta": null, "sha256": "adc4973b83b313bcaf5f9058297e030a9c272cb1b5612fe8465b02edf2ad1b73", "bytes": 54, - "line": 1192 + "line": 1261 }, { "lang": "bash", "meta": null, "sha256": "48fbd97b704f5e02a593cbf0020bc3c6600ff1b4d349ae80b3cba39225a4b9fc", "bytes": 46, - "line": 1223 + "line": 1292 }, { "lang": "bash", "meta": null, "sha256": "49e712965d1a8af321563bc382bb448e0b68067c3c68861f84896b5f97e70314", "bytes": 42, - "line": 1244 + "line": 1313 }, { "lang": "bash", "meta": null, "sha256": "e463cbe0c80554eaa7fe151c51523307240c2226a54924a3d984cdc0a84f529b", "bytes": 52, - "line": 1277 + "line": 1346 }, { "lang": "bash", "meta": null, "sha256": "23bede1f0d68221f4da857a62e0594d031e40e5a794bd96c79eba2de2924072a", "bytes": 54, - "line": 1310 + "line": 1379 }, { "lang": "bash", "meta": null, "sha256": "ad1df0d9a424569351d851507b674bb104654e1251d7bee11ebc6955e0f2be6c", "bytes": 56, - "line": 1342 + "line": 1411 }, { "lang": "bash", "meta": null, "sha256": "37cdd4ceab29424161734300286176b9971cd7d6c134022308d5b21e4fa9e714", "bytes": 33, - "line": 1374 + "line": 1443 }, { "lang": "bash", "meta": null, "sha256": "0bc8335422d07dfbc74c55102518d6d2965a5aa27be6994b8b4317478e13ef3d", "bytes": 37, - "line": 1405 + "line": 1474 }, { "lang": "bash", "meta": null, "sha256": "863b992687ec7121ef44d5680b290502052bfe51e7e43aa24376ddba51fc3841", "bytes": 28, - "line": 1435 + "line": 1504 }, { "lang": "bash", "meta": null, "sha256": "9d7e15ac24392a0771cf7857c3f110be1ede32e18427995923edc5978fdcaa31", "bytes": 38, - "line": 1455 + "line": 1524 }, { "lang": "bash", "meta": null, "sha256": "15849bbb5c9f4f1c0754c0c99d3048b910f6fdcbb02ab84a4e34d7ed1930cd15", "bytes": 24, - "line": 1488 + "line": 1557 }, { "lang": "bash", "meta": null, "sha256": "31cbe1979c2c645d78646d966ac83fd45e6b556d56ab0d8fc47a7abd281e524d", "bytes": 24, - "line": 1515 + "line": 1584 }, { "lang": "bash", "meta": null, "sha256": "1a728e81af7425e00df25a70a5819d272cbddcab487426cebc94de7834509999", "bytes": 33, - "line": 1538 + "line": 1607 }, { "lang": "bash", "meta": null, "sha256": "f7a8e9f75a2ea3d88d6d12b097f20a94e2b915741cc10f805b070c6ad4261a7e", "bytes": 35, - "line": 1570 + "line": 1639 }, { "lang": "bash", "meta": null, "sha256": "94537050876dfc16340c4db7965fcc2a098f09fcb9911d5e154c11721d212e6a", "bytes": 26, - "line": 1600 + "line": 1669 }, { "lang": "bash", "meta": null, "sha256": "cdee40f5dc3195f9db4462ddd0e5b7bc03b44f600a82f357d6b651ecee770840", "bytes": 26, - "line": 1620 + "line": 1689 }, { "lang": "bash", "meta": null, "sha256": "bda181f24ee73df10ced9c179ddb81b115d62b4da46eb64b63dd23e7f4c51a96", "bytes": 34, - "line": 1647 + "line": 1716 }, { "lang": "bash", "meta": null, "sha256": "14e217553dedf133ef014fd989058ee281844a55293e05a9dddf044947c058fe", "bytes": 40, - "line": 1677 + "line": 1746 }, { "lang": "bash", "meta": null, "sha256": "30f91370cbb0ddf76900ab363e83bb26c0a95cb81518de875885f66d4c9df4a3", "bytes": 33, - "line": 1709 + "line": 1778 }, { "lang": "bash", "meta": null, "sha256": "1c143b6c0cd2e76ce8c74504ecb5ee2560b147eb299d8f0d3d956854b706958c", "bytes": 32, - "line": 1740 + "line": 1809 }, { "lang": "bash", "meta": null, "sha256": "b36a5102aee98e664a1ca4cc19d91310af00b2a4afbb04c78589bb4c3a2936a9", "bytes": 29, - "line": 1771 + "line": 1840 }, { "lang": "bash", "meta": null, "sha256": "62a17f5231fba3f732cba025c16c8b6706411b9267e6abd58e4e51aff8777043", "bytes": 22, - "line": 1802 + "line": 1871 }, { "lang": "bash", "meta": null, "sha256": "760a88e6d91ed147f40d39808ae39d178b6c38b61a5c69dd37755079cff376c2", "bytes": 69, - "line": 1828 + "line": 1897 }, { "lang": "bash", "meta": null, "sha256": "e0173107cc10779c1b96d55c66a3bd628e9bc21b39234101e2f9d7519087780e", "bytes": 51, - "line": 1858 + "line": 1927 }, { "lang": "bash", "meta": null, "sha256": "71bb24f623f1958775fe0f9df499ad3f8241c3132ea8f8a40d5f26bd2041b65e", "bytes": 53, - "line": 1888 + "line": 1957 }, { "lang": "bash", "meta": null, "sha256": "51d1aa93ed9262fa8f275fbd24e805ad9a3a075ed2f5c3888d57ecf909426c3e", "bytes": 61, - "line": 1922 + "line": 1991 }, { "lang": "bash", "meta": null, "sha256": "4782c15ea248b0cb3a7cffa4242b54f9bea866ecf26f63ada4ed921455699cb3", "bytes": 54, - "line": 1955 + "line": 2024 }, { "lang": "bash", "meta": null, "sha256": "c8f14f40bbc787d13778c342211d73c9f0b118a8d37d3c82a87c72b42b9d5d93", "bytes": 56, - "line": 1987 + "line": 2056 }, { "lang": "bash", "meta": null, "sha256": "913db46a26314244a1e4c903adb6772a8e405da8cba64f7218ff3683d7daa72a", "bytes": 41, - "line": 2017 + "line": 2086 }, { "lang": "bash", "meta": null, "sha256": "7cda9f00d098a51943b10891142d7e6a8d4cb46d752e1cdae5825ab6c56b8aa9", "bytes": 42, - "line": 2037 + "line": 2106 }, { "lang": "bash", "meta": null, "sha256": "e09f666b66b4d7acdcd6aa26831c7f258ff49baa0fd762ea920b90fd39846582", "bytes": 61, - "line": 2057 + "line": 2126 }, { "lang": "bash", "meta": null, "sha256": "8b204243cf243c64557adb522d2bfd72be53400afab3c03f4c314117ed37655d", "bytes": 74, - "line": 2091 + "line": 2160 }, { "lang": "bash", "meta": null, "sha256": "cf87bc61bba385818ebc622f9b9f481213e1d86f65df7ab1a193749ab929a651", "bytes": 68, - "line": 2124 + "line": 2193 }, { "lang": "bash", "meta": null, "sha256": "2d57fee1358f9ce5a3f166aed1400940cbe737b04d8f487a16eb35bc3bb5a6d4", "bytes": 78, - "line": 2159 + "line": 2228 }, { "lang": "bash", "meta": null, "sha256": "7f02636a02ece5cd252aad171e251b18e1c29207d62dfcc962485052b799fdc4", "bytes": 65, - "line": 2191 + "line": 2260 }, { "lang": "bash", "meta": null, "sha256": "e6a2660057e8859cebd226a4420672f12beed4e274bdc57a4b70c96a0142b1b6", "bytes": 66, - "line": 2213 + "line": 2282 }, { "lang": "bash", "meta": null, "sha256": "54b00a2d3830092a8fe552bba29419710da6647ac3cc96d6a0a9ffbef596c440", "bytes": 78, - "line": 2249 + "line": 2318 }, { "lang": "bash", "meta": null, "sha256": "a0a28be794c70e6366719b49f3ad627faa087181960d287d6a17bc44d7a8bd9d", "bytes": 64, - "line": 2283 + "line": 2352 }, { "lang": "bash", "meta": null, "sha256": "2a48509e548ba0f0d2bb97bfc37ac1807f3b34cc5c44cbd1c5ca57735614fc24", "bytes": 71, - "line": 2318 + "line": 2387 }, { "lang": "bash", "meta": null, "sha256": "ea154f4a00845d5c60d54aff14d7173c8de383ec43e13454a8c326459ed7695f", "bytes": 58, - "line": 2349 + "line": 2418 }, { "lang": "bash", "meta": null, "sha256": "f14623530e7aeb20de131c59208269f9703e85ab6a89cd4368fb3c2ff7c36863", "bytes": 56, - "line": 2370 + "line": 2439 }, { "lang": "bash", "meta": null, "sha256": "cdf3ade3d9fa9f1f3a191265a2b0de8092eaaa49bd961d94eccb09355dfddb26", "bytes": 71, - "line": 2406 + "line": 2475 }, { "lang": "bash", "meta": null, "sha256": "2f509b586cc62ff6bb0b5c1ee7e5062689b1cfd987acc2c18dee6b10bc3a194f", "bytes": 65, - "line": 2460 + "line": 2529 }, { "lang": "bash", "meta": null, "sha256": "6546c4a95a217203c622f0e62f945cbe2863d42d02a5229621f43f12b68bc063", "bytes": 39, - "line": 2494 + "line": 2563 }, { "lang": "bash", "meta": null, "sha256": "e9f1685d2a50e4bd554c7daf19621a7704aa5b1aea3540af28884066ae413c69", "bytes": 36, - "line": 2514 + "line": 2583 }, { "lang": "bash", "meta": null, "sha256": "ad0c1107caee2427422e0a52a6da2df0a037010f93467fd957fb0136f938d675", "bytes": 30, - "line": 2534 + "line": 2603 }, { "lang": "bash", "meta": null, "sha256": "fae160a2cd7e296a9f9b00fa789ba95a2f1e507713a469902de21ed945d9eb25", "bytes": 59, - "line": 2557 + "line": 2626 }, { "lang": "bash", "meta": null, "sha256": "abdae7f49c77877550cfc2d7195132c12fb0117a0471faf1c525d44ad2de1321", "bytes": 73, - "line": 2593 + "line": 2662 }, { "lang": "bash", "meta": null, "sha256": "d11c4c59feac4345ba7dfb02edc2780adae89cf702e570f32e1ccc8555bdb169", "bytes": 60, - "line": 2626 + "line": 2695 }, { "lang": "bash", "meta": null, "sha256": "af533689c0b530ff902ae36c2cac7198f43ff6968d168a94da153d81fe8020fd", "bytes": 81, - "line": 2647 + "line": 2716 }, { "lang": "bash", "meta": null, "sha256": "f907eb4bee803d3f45d4abc9baae106aaec0249143dfd9cf56aa17285776a430", "bytes": 83, - "line": 2681 + "line": 2750 }, { "lang": "bash", "meta": null, "sha256": "28c4e709c778f27d924fbe49edb4bf198b7c73e9b65619e6fad43d1fb9e2cfb7", "bytes": 57, - "line": 2713 + "line": 2782 }, { "lang": "bash", "meta": null, "sha256": "0a7262cac83ce7c16b17a1364a123764e5fa8b645dd60fd5b394b3f74dd0e1fa", "bytes": 71, - "line": 2745 + "line": 2814 }, { "lang": "bash", "meta": null, "sha256": "a25c869b7add79bfe6f194ec72190bb6b4957bb303fe6b4c31c9455f4a2e11a9", "bytes": 73, - "line": 2777 + "line": 2846 }, { "lang": "bash", "meta": null, "sha256": "b3f9e190362420216749c9924e83a35c5a5c2414ab03ff89601a6b9501494bf9", "bytes": 35, - "line": 2810 + "line": 2879 }, { "lang": "bash", "meta": null, "sha256": "ae5dcbc80aeb125607ebd763669a1f335d6ddf087a6baf1853453867675ce865", "bytes": 45, - "line": 2832 + "line": 2901 }, { "lang": "bash", "meta": null, "sha256": "c91b2cb83c28571b965071d6643d955366931a6ca3dd340e54d8536ea90cefde", "bytes": 36, - "line": 2863 + "line": 2932 }, { "lang": "bash", "meta": null, "sha256": "de622393fa33cc9b0002c7d670d9f45c2bcd9fbfe3f0c99be71ae96969a005dd", "bytes": 47, - "line": 2888 + "line": 2957 }, { "lang": "bash", "meta": null, "sha256": "356cb12341ce90e24b765d02a997421c0469a3b26509c90e42a17af4dd3f9e95", "bytes": 48, - "line": 2913 + "line": 2982 }, { "lang": "bash", "meta": null, "sha256": "a611d4f425ac65781756225557a7e8f8559faccaa4c6466ef23fa78cd2a2226c", "bytes": 35, - "line": 2943 + "line": 3012 }, { "lang": "bash", "meta": null, "sha256": "8c7bab52ede04cca1dd71b466bd76c7f5b67c6b7f0fed08e14ee7656d6b2b160", "bytes": 28, - "line": 2963 + "line": 3032 }, { "lang": "bash", "meta": null, "sha256": "063e71c7485fe7d3657e692841a2f0629e6a31b48e8501282b0456202eefaa5d", "bytes": 39, - "line": 2988 + "line": 3057 }, { "lang": "bash", "meta": null, "sha256": "4f9e7b46b98628ddded377f3a73cb2e9b4751023b7b1fc90c1cefc90555c80c4", "bytes": 30, - "line": 3008 + "line": 3077 }, { "lang": "bash", "meta": null, "sha256": "efaaf65fd8057cd592d386d0e0c800eef4e71d649a1f011fd2f20495d8d942ab", "bytes": 48, - "line": 3036 + "line": 3105 }, { "lang": "bash", "meta": null, "sha256": "5303a1a6194097b3acc7546ccafb0faced44b084951b7e14ff1029cf0fdff957", "bytes": 43, - "line": 3069 + "line": 3138 }, { "lang": "bash", "meta": null, "sha256": "a9ba0ea93105e0f224ed0c3e661adf5fe706a875fb9741348134814d65c6d882", "bytes": 48, - "line": 3090 + "line": 3159 }, { "lang": "bash", "meta": null, "sha256": "a721f46390cb321c013b700b13f46f2e9833616be44a3ee8e0971c3e5c0f945b", "bytes": 37, - "line": 3122 + "line": 3191 }, { "lang": "bash", "meta": null, "sha256": "9f9b6ea8992e8fcd2bdb540857d42288ba0ecedb959ff5c1c56de708e65db79e", "bytes": 33, - "line": 3153 + "line": 3222 }, { "lang": "bash", "meta": null, "sha256": "1478c9884eb8c9bc2dd64170d3fb101030957399aab5da911d5f77d46b662c73", "bytes": 26, - "line": 3184 + "line": 3253 }, { "lang": "bash", "meta": null, "sha256": "51b81826c6f4b7933c1c802c82baca186c67efe8e59e69a6a7754360fe8675da", "bytes": 30, - "line": 3210 + "line": 3279 }, { "lang": "bash", "meta": null, "sha256": "8aeb5fc735c8037c480f3c8220149fb6f811de62c946584523d50d1cd1811a53", "bytes": 24, - "line": 3242 + "line": 3311 }, { "lang": "bash", "meta": null, "sha256": "a1bf4221604cc962c677313543850fe6cd140476ef52de3a9e29b305865ae8f0", "bytes": 23, - "line": 3271 + "line": 3340 }, { "lang": "bash", "meta": null, "sha256": "5215bc6f0c098350b8067dd283226f3444cf73599ab1989c5dfae5c563069a04", "bytes": 25, - "line": 3315 + "line": 3384 }, { "lang": "bash", "meta": null, "sha256": "98f84efeb01c77bc8c1aadb62fbd989c5ae41de97d07fd9b59e8b2b53cafe06f", "bytes": 145, - "line": 3339 + "line": 3408 }, { "lang": "bash", "meta": null, "sha256": "baad97733d922f4ef4c068577ef9f0f855c44f3b54e9b80a61733a4fecfd350d", "bytes": 32, - "line": 3359 + "line": 3428 }, { "lang": "bash", "meta": null, "sha256": "fdb563355cb89d0b6f534e825e70973c85326879a7d4f0adaee447ed78db5260", "bytes": 46, - "line": 3392 + "line": 3461 }, { "lang": "bash", "meta": null, "sha256": "8409df5d28808a04cbea45c4f0162a7a283a0d4eee0fc3391d82a8663121a495", "bytes": 33, - "line": 3422 + "line": 3491 }, { "lang": "bash", "meta": null, "sha256": "c3189f21b856e8c158f2a39d3260d7c0ac93ba2c0c73494c7e1374d45a51346f", "bytes": 30, - "line": 3442 + "line": 3511 }, { "lang": "bash", "meta": null, "sha256": "066839a4599d90ccd88f845122e61c608a8ea120e9f1476857f0a2eb8c723de7", "bytes": 50, - "line": 3465 + "line": 3534 }, { "lang": "bash", "meta": null, "sha256": "209da9ab7f502fbcaec3297113212d496eca57b124e273dc8080efa6c4e0de71", "bytes": 46, - "line": 3496 + "line": 3565 }, { "lang": "bash", "meta": null, "sha256": "c01ee6bd733ccd61abdb244baa88a17758166a1eacaa4456d3a8bbe56e6ff00f", "bytes": 15, - "line": 3541 + "line": 3610 }, { "lang": "bash", "meta": null, "sha256": "e2c2a5ffcc518a49f6cc038c691b068769101b4bf94905eb0a472299ab563620", "bytes": 39, - "line": 3555 + "line": 3624 }, { "lang": "bash", "meta": null, "sha256": "ccaaa978abfa31e70784eda567c50bec64a5cf485a205fa545e71f46c1821f3a", "bytes": 40, - "line": 3576 + "line": 3645 }, { "lang": "bash", "meta": null, "sha256": "7e6eac72f3011e3714d4eeb9914601dbe344ff899504c1324ff55c2b6eed60d5", "bytes": 53, - "line": 3601 + "line": 3670 }, { "lang": "bash", "meta": null, "sha256": "d557d9bb45aa7b4ee10d2d502d92710aafe9fcbc7b12d0a219c3eff49863e1f7", "bytes": 53, - "line": 3632 + "line": 3701 }, { "lang": "bash", "meta": null, "sha256": "6f4dc8213285da7c740b7f183f519a14d88e853125347cf4b3585c153f3d6c95", "bytes": 62, - "line": 3662 + "line": 3731 }, { "lang": "bash", "meta": null, "sha256": "c7934b9e19cf7a0ff636ecdb465b1a6727ad23d36b695eb0b08783efa08698d3", "bytes": 50, - "line": 3692 + "line": 3761 }, { "lang": "bash", "meta": null, "sha256": "c6c30823efb18475aef576f5eac37ab1a319e1de7b2462fccb62b249396e3d9f", "bytes": 52, - "line": 3714 + "line": 3783 }, { "lang": "bash", "meta": null, "sha256": "8567533e04160a832142ff673958ff2456ea163f86df91685ba5da691604a882", "bytes": 56, - "line": 3741 + "line": 3810 }, { "lang": "bash", "meta": null, "sha256": "f8809dfed9678563c959c376cf67dcc022eeee8e5e4384e8aa46ff6470838960", "bytes": 59, - "line": 3761 + "line": 3830 }, { "lang": "bash", "meta": null, "sha256": "c4202b9b75440588bb6562fce248037a4425448373f72f9ee3ad9ade135fab34", "bytes": 48, - "line": 3782 + "line": 3851 }, { "lang": "bash", "meta": null, "sha256": "711c4598e2329b7c5079f9cc2569196c4af7ebfa267e38ab904ff60e579e5169", "bytes": 48, - "line": 3807 + "line": 3876 }, { "lang": "bash", "meta": null, "sha256": "ef9a879b046809272052e1804f9ebe98b60cd0cdf476251b599ca44bb3ae1b11", "bytes": 63, - "line": 3833 + "line": 3902 }, { "lang": "bash", "meta": null, "sha256": "b07d1aa721f8a38cfc8e79bc68497e76240430fcc1fec0818294974f67755a1d", "bytes": 63, - "line": 3863 + "line": 3932 }, { "lang": "bash", "meta": null, "sha256": "6231d67b8fca5d05ef85468209b86d04c0c016014f46f2f444da5321bd655941", "bytes": 46, - "line": 3897 + "line": 3966 }, { "lang": "bash", "meta": null, "sha256": "30a1944929f1b7cdc8d8866735022851ef133f4160fc018f736201f1531e7907", "bytes": 58, - "line": 3919 + "line": 3988 }, { "lang": "bash", "meta": null, "sha256": "6ce2fc39c89d2789f343b2590c28ff069d4bcf685241cdfd645d579b886625a5", "bytes": 44, - "line": 3949 + "line": 4018 }, { "lang": "bash", "meta": null, "sha256": "edbc6c008ae5039761ca84f5af6c63c15aab125fe0bc7b890362159598318cf6", "bytes": 65, - "line": 3975 + "line": 4044 }, { "lang": "bash", "meta": null, "sha256": "cdeb9be48212517be6994c625844b9761842bd3822c74e7d86970a022811a07d", "bytes": 61, - "line": 4010 + "line": 4079 }, { "lang": "bash", "meta": null, "sha256": "7f3233f2465f84ad271fbe67411e1651f83d9dd39e15128c477276a103443baa", "bytes": 61, - "line": 4042 + "line": 4111 }, { "lang": "bash", "meta": null, "sha256": "af80c56179adf35f94c4c1c3eb3676cf2ca68b61af3c1b43a9ac020c4205f4a2", "bytes": 61, - "line": 4075 + "line": 4144 }, { "lang": "bash", "meta": null, "sha256": "48fced6ea852f10aa3620044ca010c0bf50acfb3b743012f836a98be0d7cdf08", "bytes": 58, - "line": 4106 + "line": 4175 }, { "lang": "bash", "meta": null, "sha256": "6d9a078ab511579f7f7234fa28d72ae92f7df84c9f9957aff14951a2fe1b08f3", "bytes": 46, - "line": 4136 + "line": 4205 }, { "lang": "bash", "meta": null, "sha256": "88b2622d7dc41ea41543f26e4dd1b513c271cb0e0452b937033088f7ae80484f", "bytes": 46, - "line": 4161 + "line": 4230 }, { "lang": "bash", "meta": null, "sha256": "757036af4cd38ae573a0589ddcd08b179542a30bfa802f88ff3debb198baae47", "bytes": 62, - "line": 4209 + "line": 4278 }, { "lang": "bash", "meta": null, "sha256": "7311b5a978eda8140f8e314d709e565a535c76064199595eb139c015f16312d5", "bytes": 42, - "line": 4239 + "line": 4308 }, { "lang": "bash", "meta": null, "sha256": "c3410310e5eccb2e0a7440b8d3cbfc2508270890a08ec5a85c5b8cc5c5a3b8b8", "bytes": 45, - "line": 4259 + "line": 4328 }, { "lang": "bash", "meta": null, "sha256": "08f1a585205b784ac0527c4ffb9aae95a4d8bd1702abddb87d1aa25415de45d2", "bytes": 38, - "line": 4280 + "line": 4349 }, { "lang": "bash", "meta": null, "sha256": "e4502c5ec3c9643358cf434f3271f7f39ee39f899661c58e7af68b98f42cbcb1", "bytes": 60, - "line": 4300 + "line": 4369 }, { "lang": "bash", "meta": null, "sha256": "59e8699f41a3f06be75f1eb4addb041b762d100d6e15f61788e729a8a277f55f", "bytes": 63, - "line": 4330 + "line": 4399 }, { "lang": "bash", "meta": null, "sha256": "9a525f2d35624ea70a558a4081c20c98418fb371cce3ac2d70cb01462271f1c6", "bytes": 40, - "line": 4361 + "line": 4430 }, { "lang": "bash", "meta": null, "sha256": "5db8d4c149bb0b8378ac584947f1a8d037aa2375312e098338ee1566030988e4", "bytes": 51, - "line": 4387 + "line": 4456 }, { "lang": "bash", "meta": null, "sha256": "cba02b44f48f44f069fb55f48ec4e680a49b71ab99d8e7013ff4113236cd6e19", "bytes": 51, - "line": 4418 + "line": 4487 }, { "lang": "bash", "meta": null, "sha256": "1825feb4954c2f39280e98a8ab8f8784e097c9823d04cbccf616c28f24eeee4b", "bytes": 43, - "line": 4449 + "line": 4518 }, { "lang": "bash", "meta": null, "sha256": "0bd4be17187ea4870dfddcf74c03492e4ff3e4eb734d8e38fc710a0afd70e771", "bytes": 41, - "line": 4475 + "line": 4544 }, { "lang": "bash", "meta": null, "sha256": "e26b81a721ffd90b20b61be8168187fc20333274f6c3e537eb6cc44f87397f8b", "bytes": 40, - "line": 4499 + "line": 4568 + }, + { + "lang": "bash", + "meta": null, + "sha256": "229448ff3c47ffdfdd720fb88b38edcf58f51d063f7b7836d4f4fdc478f99b5c", + "bytes": 69, + "line": 4597 + }, + { + "lang": "bash", + "meta": null, + "sha256": "7863a0cbd4e2cb3715358cca55047765a210f9a42a3dd171e42f574185fe0ab9", + "bytes": 73, + "line": 4629 }, { "lang": "bash", "meta": null, "sha256": "a85bb64841a5626286099eb6175af5f8d0b0c8f19d71275c41ba144d5ce6d75b", "bytes": 32, - "line": 4528 + "line": 4662 }, { "lang": "bash", "meta": null, "sha256": "c8500badf466ac4cddb64017931e69aedba037eb5c44d0be411f3e2368254f87", "bytes": 38, - "line": 4551 + "line": 4685 }, { "lang": "bash", "meta": null, "sha256": "c5da7d1cca60bf076fb11e3a830b1c639ae02b8b2413e4956b12b54d4af7869d", "bytes": 43, - "line": 4583 + "line": 4717 }, { "lang": "bash", "meta": null, "sha256": "ee42a8df21f3cb29767908892e907b58edc31fccac8414e646690b238373feac", "bytes": 49, - "line": 4605 + "line": 4739 }, { "lang": "bash", "meta": null, "sha256": "3aa75d103d8d87811ca0f13f238ef5bfc84debf02826b8883eb0a09b372b7230", "bytes": 44, - "line": 4628 + "line": 4762 }, { "lang": "bash", "meta": null, "sha256": "c97bf15cbec26edcc86e8e3a908361b4c12a7d4b5578d5ea685d4fc68993c3f9", "bytes": 55, - "line": 4654 + "line": 4788 }, { "lang": "bash", "meta": null, "sha256": "971979fe121168021fc32881c5e8807f271cbaee608d70cf22b709662e0aae21", "bytes": 38, - "line": 4686 + "line": 4820 }, { "lang": "bash", "meta": null, "sha256": "76f320882518b53d2a6055f48c06b0deba00efa8baa1c1932ec6f1c598868a82", "bytes": 48, - "line": 4714 + "line": 4848 }, { "lang": "bash", "meta": null, "sha256": "1c902a61277e58e20d330fd4f7510a922639122f3e26126aeff5a404579d210d", "bytes": 45, - "line": 4745 + "line": 4879 }, { "lang": "bash", "meta": null, "sha256": "3e09d80993bd2231d73817167d0a3e7d022d9bb5fadbf1563a854531a3c22ced", "bytes": 36, - "line": 4775 + "line": 4909 }, { "lang": "bash", "meta": null, "sha256": "39050b42d7a1e9da84fac3fa817aa0635c47c28f32bceabcaf8811283ad7d7ce", "bytes": 48, - "line": 4799 + "line": 4933 }, { "lang": "bash", "meta": null, "sha256": "a4d3e32ee5d44b474b2fe040b4be5d32c0ad725d1b3c3e4342dc2ca08fa0a157", "bytes": 30, - "line": 4841 + "line": 4975 }, { "lang": "bash", "meta": null, "sha256": "37f58f218a36b8141bd5c32fb2774d731631774d332ce2fe9f7a3af158f122c1", "bytes": 42, - "line": 4867 + "line": 5001 }, { "lang": "bash", "meta": null, "sha256": "992f83d520658780ee956610f7965ecaad8d3c782b452d78d619edccd3afa26f", "bytes": 29, - "line": 4897 + "line": 5031 }, { "lang": "bash", "meta": null, "sha256": "594f0358c607649151209b234a12f44d7a125fa9150668fbf46ad475c36e52c9", "bytes": 28, - "line": 4917 + "line": 5051 }, { "lang": "bash", "meta": null, "sha256": "1f2babf217eecbcd3edee20e939434b8b177c454ae3edad1ed0a79b24bf0929f", "bytes": 42, - "line": 4940 + "line": 5074 }, { "lang": "bash", "meta": null, "sha256": "aa98bb7f2995f97bb02887ae3591ff0609686e634f9ad60068fbe12f38a4a026", "bytes": 35, - "line": 4984 + "line": 5118 }, { "lang": "bash", "meta": null, "sha256": "e421d326ab72354b8ec1d5711570d31868223c50da978b51961f9896acae5f35", "bytes": 26, - "line": 5015 + "line": 5149 }, { "lang": "bash", "meta": null, "sha256": "65c4b3a60ee39d43b253afd6818ed08dd93a5297f82b4648e4a3d5b0002731e4", "bytes": 32, - "line": 5039 + "line": 5173 }, { "lang": "bash", "meta": null, "sha256": "e2f60ffbb1bae7635a1e9ba79c228b19464a582144e1be50bfaba8d2c2db30c8", "bytes": 27, - "line": 5081 + "line": 5215 }, { "lang": "bash", "meta": null, "sha256": "afa0b971e60daad0dddc74b93f44fac1f51be91d47626ed4942b465fc6eba770", "bytes": 28, - "line": 5103 + "line": 5237 }, { "lang": "bash", "meta": null, "sha256": "4a0236d60519b55978f69d4eaa3ac6b8de2b354a7e63a14255abb64862d0456e", "bytes": 27, - "line": 5133 + "line": 5267 }, { "lang": "bash", "meta": null, "sha256": "ebd45772f099ce26c6315895068a1703d8e757f43daf35f6d3618c408319b37b", "bytes": 37, - "line": 5157 + "line": 5291 }, { "lang": "bash", "meta": null, "sha256": "e6e8ba88a5ede0c440346b124c101ccd53241c492f6dc28c3453fd23b8917cb3", "bytes": 24, - "line": 5187 + "line": 5321 }, { "lang": "bash", "meta": null, "sha256": "c19501e59583b680d6ce36b1133f3850ef1c96d05e7e5985b6cb8b1c5e19ff46", "bytes": 45, - "line": 5207 + "line": 5341 }, { "lang": "bash", "meta": null, "sha256": "4b84b742cd46fa81f9dc42d64c66c4b801b9fc6c381c1fda6690647d082639e3", "bytes": 43, - "line": 5237 + "line": 5371 }, { "lang": "bash", "meta": null, "sha256": "b9eefbf636684f9388fb43e708ad6dd2326b629f85a95e039f69e13651993c41", "bytes": 45, - "line": 5271 + "line": 5405 }, { "lang": "bash", "meta": null, "sha256": "bf2cc4f904e1d46c6ad5fb9c85a1a77b75062b61997add02c5c8d5b4cdcda262", "bytes": 25, - "line": 5302 + "line": 5436 }, { "lang": "bash", "meta": null, "sha256": "eef07048b814eb3b00f997c8b0907d1e21817abad072f7e8502087fdbe9a0188", "bytes": 37, - "line": 5325 + "line": 5459 }, { "lang": "bash", "meta": null, "sha256": "f35ce94c4d5abe5378ba2b6095a786d9989540caefe3b2248b43ec375b69d212", "bytes": 45, - "line": 5363 + "line": 5497 }, { "lang": "bash", "meta": null, "sha256": "f8a9f16a31241f7bed8f14a44cbc6ba4c13707b0bbc71f95abaded420f98cae8", "bytes": 45, - "line": 5395 + "line": 5529 }, { "lang": "bash", "meta": null, "sha256": "6eee9d069d9053a29e2bd61c5a85821fead20cc3af306f55e3597f9931588d32", "bytes": 45, - "line": 5426 + "line": 5560 }, { "lang": "bash", "meta": null, "sha256": "b0485a54f218265aaf8ef27b26af37e80603744b4cd13af804b9460ded6c769c", "bytes": 44, - "line": 5457 + "line": 5591 }, { "lang": "bash", "meta": null, "sha256": "eff43eafc0b5c9829de1e71e899e82b6db9bf5a3ba4da138fe2608ecf2ad96ea", "bytes": 44, - "line": 5492 + "line": 5626 }, { "lang": "bash", "meta": null, "sha256": "598ec0411dcfb12246c2ad819aeb9083cfd0dcf6a894aec2a0e908d82226e85f", "bytes": 32, - "line": 5523 + "line": 5657 }, { "lang": "bash", "meta": null, "sha256": "2869d4bb0b7552d346eca2e8bfe110dccdb3a13073c0e5a531ed34e8d111459f", "bytes": 44, - "line": 5543 + "line": 5677 }, { "lang": "bash", "meta": null, "sha256": "faa95847578604b50b3c020d50d94589351f7b1ec36e28cac7fe2140de6c4549", "bytes": 33, - "line": 5586 + "line": 5720 }, { "lang": "bash", "meta": null, "sha256": "25e24485a99181672176131b6ac2152347841a1a567430fbb98a553a929770f3", "bytes": 47, - "line": 5608 + "line": 5742 }, { "lang": "bash", "meta": null, "sha256": "9720d5ef881383f1c6da7a8a3a008f3afd615af99678d1bb81118511ff84dfcc", "bytes": 42, - "line": 5638 + "line": 5772 }, { "lang": "bash", "meta": null, "sha256": "3b9ac116c754c7e2bcf7f077f6061399dbfff4238408c4ef0d048fb442c3baf8", "bytes": 50, - "line": 5671 + "line": 5805 }, { "lang": "bash", "meta": null, "sha256": "3487041dfbdb03a03ebed0e308da4117442785d83c8158248fbad5df46f35fb2", "bytes": 48, - "line": 5704 + "line": 5838 }, { "lang": "bash", "meta": null, "sha256": "25f7e3715a24bd9657f90a5d9bc466cfe7b121a2660596c62c08a6ae0c4834f2", "bytes": 47, - "line": 5737 + "line": 5871 }, { "lang": "bash", "meta": null, "sha256": "8a310ebdd720a3d0e2fffb7db415873a53befe47ea4d638fca4ecd59ac27595e", "bytes": 40, - "line": 5769 + "line": 5903 }, { "lang": "bash", "meta": null, "sha256": "d3dba384fd5ce87e831b8454186e60d5e1cd6ae404b6324009adf4a961d85cfe", "bytes": 41, - "line": 5802 + "line": 5936 }, { "lang": "bash", "meta": null, "sha256": "4bec76dae83cf450410fd42df2a56ef2b20e69143f9631539a58f363631cb986", "bytes": 41, - "line": 5837 + "line": 5971 }, { "lang": "bash", "meta": null, "sha256": "ad373ebb46a54ef7fead24880468d70c4a415003db879817f5d78e717994573c", "bytes": 50, - "line": 5867 + "line": 6001 }, { "lang": "bash", "meta": null, "sha256": "9f85f14e7c0f00e23c82b9a097ab414df558c764e4171f8bdd6771386b02677c", "bytes": 42, - "line": 5889 + "line": 6023 }, { "lang": "bash", "meta": null, "sha256": "856a70bda3a296ed9e7d3de557191906106fd51da9fab3d2372bd4a3d0115ae1", "bytes": 48, - "line": 5923 + "line": 6057 }, { "lang": "bash", "meta": null, "sha256": "12b44782510d874df9900581914d66e8e8a0708ea6e1f8cab799c1187d4778f2", "bytes": 50, - "line": 5956 + "line": 6090 }, { "lang": "bash", "meta": null, "sha256": "908dcdcee6aa38f9ed48e63e5a70bba22dc7aaa603dfb8c62f0999259a1b9eb1", "bytes": 61, - "line": 5989 + "line": 6123 }, { "lang": "bash", "meta": null, "sha256": "55ea841db41ce49eb504636a54c29187e16b403b686059375abfa8aa51d01f7d", "bytes": 48, - "line": 6020 + "line": 6154 }, { "lang": "bash", "meta": null, "sha256": "c414854798bccb75c4b223fbed069ed445872158dbfb2b23093e140827d1e838", "bytes": 48, - "line": 6055 + "line": 6189 }, { "lang": "bash", "meta": null, "sha256": "9f45f6e6567f09f49ec40484721420a71a15d42f7ab92d3a3cfa1124ab8419fd", "bytes": 36, - "line": 6076 + "line": 6210 }, { "lang": "bash", "meta": null, "sha256": "862b4e873ebd70c303716275ebf30b3822c9c9e7f261a78e27b18f9585d798b0", "bytes": 46, - "line": 6096 + "line": 6230 }, { "lang": "bash", "meta": null, "sha256": "bb65d63f55930b4ea7b86e0475159b98a53e0525015508285c90425f28d53a55", "bytes": 45, - "line": 6117 + "line": 6251 }, { "lang": "bash", "meta": null, "sha256": "18d5a60f518896e595f00666ed45fdd624c789287809e18798e0d9f5610d48c8", "bytes": 43, - "line": 6147 + "line": 6281 }, { "lang": "bash", "meta": null, "sha256": "e44d003a6ba11690de0cc469320be892fae3b09033939a46724fccfb0aac4ab0", "bytes": 48, - "line": 6168 + "line": 6302 }, { "lang": "bash", "meta": null, "sha256": "bee16895751b8f386d2188e18ae402e521187d2bb7da9af709c2432ba9634209", "bytes": 46, - "line": 6191 + "line": 6325 }, { "lang": "bash", "meta": null, "sha256": "d3dede19db079b39e165a036fac3dd3376044ada7849f3ec70220ae188ac28e5", "bytes": 33, - "line": 6221 + "line": 6355 }, { "lang": "bash", "meta": null, "sha256": "0ac8381a83158432224d7b6bead7492d25f36e68eadc15c18e9bf84eddef77d0", "bytes": 42, - "line": 6243 + "line": 6377 }, { "lang": "bash", "meta": null, "sha256": "57308ec7b05d6b120ff6841edb51d8159d84444d6eaf36b3bccb9d1b2bb385e6", "bytes": 27, - "line": 6277 + "line": 6411 }, { "lang": "bash", "meta": null, "sha256": "90b5b274482044f6d036867177353233bf58d00f5dd345f76c0866a23be10343", "bytes": 32, - "line": 6300 + "line": 6434 }, { "lang": "bash", "meta": null, "sha256": "0636f27b84a87525a97146f65475905163efcae563920102110bdaef39d0cbe2", "bytes": 42, - "line": 6322 + "line": 6456 }, { "lang": "bash", "meta": null, "sha256": "36c08c95e7a531c73736416e90aea14eb199ad1746f90a4ecc78db9c037fd03e", "bytes": 33, - "line": 6353 + "line": 6487 }, { "lang": "bash", "meta": null, "sha256": "ab978cbdc23f7d851799251cfe01863187b2cd628b05c3c750ad1db2d26fb5fc", "bytes": 44, - "line": 6378 + "line": 6512 }, { "lang": "bash", "meta": null, "sha256": "2333fb674a47979dc56cf4e10c852516a7d179ddc234cc184130c6e82ae5811b", "bytes": 33, - "line": 6401 + "line": 6535 }, { "lang": "bash", "meta": null, "sha256": "d301cb24422359056dad68402e9f54a8ed15e21c0f183fb4c8eb937738f155dc", "bytes": 43, - "line": 6421 + "line": 6555 }, { "lang": "bash", "meta": null, "sha256": "bd3c423a63ecf70ee28e0c4aa2fe7b9e6078179ec2577ba80004cdfaf527843a", "bytes": 52, - "line": 6454 + "line": 6588 }, { "lang": "bash", "meta": null, "sha256": "a064ea603a971a2a3582988297d7293e6e0c6cabedfad714f2af97c0ff1884e7", "bytes": 39, - "line": 6485 + "line": 6619 }, { "lang": "bash", "meta": null, "sha256": "20d848044f1a1ac09fe6621d3e3f7fe7e2a6271c08e23f5eaea2c4c4b3f21c9d", "bytes": 31, - "line": 6506 + "line": 6640 }, { "lang": "bash", "meta": null, "sha256": "b19fe44255306f492be72ced4183894fe6ab47c5e87ab02cf909c5a6e89ab964", "bytes": 52, - "line": 6526 + "line": 6660 }, { "lang": "bash", "meta": null, "sha256": "0a8251333833585605881b98f9d121fa0e5669e58ba11cb583f69a333cea2ad6", "bytes": 37, - "line": 6563 + "line": 6697 }, { "lang": "bash", "meta": null, "sha256": "3f1de42768c4151cd364eb2534408179e93ca92dda918a493c9d05d4c8f03209", "bytes": 53, - "line": 6593 + "line": 6727 }, { "lang": "bash", "meta": null, "sha256": "60b57e472e506c0d6430f325c0e05c6cdfe9a62b4051a9f371929799ec7cfdef", "bytes": 24, - "line": 6615 + "line": 6749 }, { "lang": "bash", "meta": null, "sha256": "aeea8ffe933fcbb749950596d5c83192d1c42e2dac8f9532625c97afd7fe18a2", "bytes": 25, - "line": 6635 + "line": 6769 }, { "lang": "bash", "meta": null, "sha256": "3010a87af250eea55a7de4dcdda1fdcef515e73b7d2f5977d955f9d66ca4e130", "bytes": 25, - "line": 6660 + "line": 6794 }, { "lang": "bash", "meta": null, "sha256": "4cff190ec4b1d449d089493b45b00f986e3d17e563bca1816769ee3a1cfea2f2", "bytes": 28, - "line": 6682 + "line": 6816 }, { "lang": "bash", "meta": null, "sha256": "b7a3a8b8a114216dbdb506005496e5510feb227b0b6e39ba2b5145743e852577", "bytes": 37, - "line": 6702 + "line": 6836 }, { "lang": "bash", "meta": null, "sha256": "36e0677e842c003940707713b5334c6f2cabdfe6548790bc0c1b8546b0f820b6", "bytes": 32, - "line": 6734 + "line": 6868 }, { "lang": "bash", "meta": null, "sha256": "a9a07815e06f5bab4b2d1fc80a606227a8a617c105c56680c4bef68a520970c1", "bytes": 37, - "line": 6755 + "line": 6889 }, { "lang": "bash", "meta": null, "sha256": "79420742f040a499740d4b465a8e8fdbc628c275fbd2880182637c5917b67b9e", "bytes": 34, - "line": 6792 + "line": 6926 }, { "lang": "bash", "meta": null, "sha256": "a6b4fb5e2728894616f9d11df544879c4786b80458d5905a67ffc2224022ec1e", "bytes": 30, - "line": 6831 + "line": 6965 }, { "lang": "bash", "meta": null, "sha256": "1aa431dc5b8d1fdc3d54a77c56f15a1b9ac5896e343818d3d0ad2edbabc541db", "bytes": 23, - "line": 6862 + "line": 6996 }, { "lang": "bash", "meta": null, "sha256": "262692daffbb55d5cbbf1717fb2c8ff08bc009514bae8ff83ee756d4caba08d1", "bytes": 36, - "line": 6888 + "line": 7022 }, { "lang": "bash", "meta": null, "sha256": "1e9b19a7ea2fdbf0902ed74b48cecc710d7fa2272d4dbed9172af1ae0258310d", "bytes": 22, - "line": 6920 + "line": 7054 }, { "lang": "bash", "meta": null, "sha256": "734b96827127375d652bc125b1c9a243946cf5f8c251b43a7097656fcc2ce544", "bytes": 24, - "line": 6940 + "line": 7074 }, { "lang": "bash", "meta": null, "sha256": "c6544463a5be88fa1bb81e80633a6e4bf70e99d424d408c75930046e74c1dc5f", "bytes": 41, - "line": 6971 + "line": 7105 }, { "lang": "bash", "meta": null, "sha256": "2ba0e68b06d7be939be3058e281cd495a16a41611ae4dadb91c97dea4b95fe29", "bytes": 52, - "line": 6997 + "line": 7131 }, { "lang": "bash", "meta": null, "sha256": "f0f241b97b09236a5fcdc9527cff2f76cf382270caf4315aa8ae11f8ca919932", "bytes": 58, - "line": 7027 + "line": 7161 }, { "lang": "bash", "meta": null, "sha256": "fde5893586a0c2c7cbd2ab3976d7d9ef35e861b56eab07191919891e09528bd4", "bytes": 46, - "line": 7060 + "line": 7194 }, { "lang": "bash", "meta": null, "sha256": "3d28258fd09b2c94b7cb24cb37035ce71d3777b58b2270998781761f8957081e", "bytes": 71, - "line": 7082 + "line": 7216 }, { "lang": "bash", "meta": null, "sha256": "45cdb94cf834d1950ab049a6a65c398b4917312897fdcaa3f25fa0081e707912", "bytes": 39, - "line": 7113 + "line": 7247 }, { "lang": "bash", "meta": null, "sha256": "ac7f120b7e8c39cb3de7b0936dfd2f6d8f38ece480b0523518883cef7b5b643c", "bytes": 39, - "line": 7133 + "line": 7267 }, { "lang": "bash", "meta": null, "sha256": "7994ad60b43332fb5bc583bf1ea5599fa2f659cd8ad811105837cf9e87fa28ed", "bytes": 52, - "line": 7155 + "line": 7289 }, { "lang": "bash", "meta": null, "sha256": "5ed096b4dcf0efad18c54d322d528b7bb0e80336d061ee7eaa5d39d3bd505a4d", "bytes": 62, - "line": 7196 + "line": 7330 }, { "lang": "bash", "meta": null, "sha256": "daf3ccbc5a308bc4c62d1a761e01fac3d40ff67cc256c4c9624d0cd00a0072c4", "bytes": 53, - "line": 7229 + "line": 7363 }, { "lang": "bash", "meta": null, "sha256": "48ecf1013a285c0de3e23936d53a850d85af0564a7e089de2eba1ac7f8881a44", "bytes": 53, - "line": 7268 + "line": 7402 }, { "lang": "bash", "meta": null, "sha256": "653a412a8eeaffac8afbbbb2533375f02df67fe7ef882ca53fc7ca2755d0bcec", "bytes": 43, - "line": 7301 + "line": 7435 }, { "lang": "bash", "meta": null, "sha256": "bfb90a10455a59ef26068d176221816b9c374c4001d386ffdbb0140c4cfe40ca", "bytes": 40, - "line": 7331 + "line": 7465 }, { "lang": "bash", "meta": null, "sha256": "d683e8c6e5a1d063ea9f6c74fd9020d48aede0d5af7031f6f48bd5a173ad2d3d", "bytes": 33, - "line": 7366 + "line": 7500 }, { "lang": "bash", "meta": null, "sha256": "96abc89ee23cb4a7e2fc3caca2afbd2dd57d2734ca9e73816c412a71c5c5eb9d", "bytes": 43, - "line": 7393 + "line": 7527 }, { "lang": "bash", "meta": null, "sha256": "d63bf37625acc32ec1ee6e59a56f57469b9cb4dab03e23067a54d2f7d8abd3a3", "bytes": 41, - "line": 7425 + "line": 7559 }, { "lang": "bash", "meta": null, "sha256": "eb79c561c688b305a39f834bb39d74f65095db957e337d8e6081bbc00545bb08", "bytes": 53, - "line": 7458 + "line": 7592 }, { "lang": "bash", "meta": null, "sha256": "f30977b7b952e6a369b72b828256fac3f9ede78ce6e7c45560a8d09989bd4db5", "bytes": 49, - "line": 7489 + "line": 7623 }, { "lang": "bash", "meta": null, "sha256": "7d00d2a238c8794c5479e28ef14e6e104bcaff1aaaef70a094a2a51c55b6420c", "bytes": 50, - "line": 7510 + "line": 7644 }, { "lang": "bash", "meta": null, "sha256": "f34921d79bc4b608cb3e3c3f882729808afe1d8cf95ca45c63cbdf00ab494a3e", "bytes": 62, - "line": 7541 + "line": 7675 }, { "lang": "bash", "meta": null, "sha256": "ebfb60766394d803a01326b040ac37c5f122134555f1b968aeb6a79b10d74fc7", "bytes": 30, - "line": 7573 + "line": 7707 }, { "lang": "bash", "meta": null, "sha256": "432348582be57741628ca2b6a762c385ed57bbbfd4e4794b914409b7a89db582", "bytes": 35, - "line": 7595 + "line": 7729 }, { "lang": "bash", "meta": null, "sha256": "25d75743717b8e1c96f6b78e334e3e3cbc6dac5e395068308a4e358e06a0bac6", "bytes": 45, - "line": 7617 + "line": 7751 }, { "lang": "bash", "meta": null, "sha256": "2fc4fd97c2a62f635b42d646e481b18b0402ec0d051e7532988c026d838f0534", "bytes": 36, - "line": 7648 + "line": 7782 }, { "lang": "bash", "meta": null, "sha256": "3da31453cf86b97920f8d013f00122dee830af847c064e30ed3e158bd8f642cb", "bytes": 47, - "line": 7673 + "line": 7807 }, { "lang": "bash", "meta": null, "sha256": "d4de05e9142f532574133568e3fe2965c7fd82338e5b54ba0d8ae5713639eaca", "bytes": 43, - "line": 7698 + "line": 7832 }, { "lang": "bash", "meta": null, "sha256": "e63db5a4bab57e3229127bb086d0c5429dfe20de83eaf5097e8943f90520b9b3", "bytes": 51, - "line": 7730 + "line": 7864 }, { "lang": "bash", "meta": null, "sha256": "b130d4ed01c735074275b1505e7959e6dd3ae89c6b7ac159d17ccd495bec8f38", "bytes": 38, - "line": 7760 + "line": 7894 }, { "lang": "bash", "meta": null, "sha256": "32709306f0caabb4378fce9efba6b9c60a3985bb3500402d9a7f9e89fe35e00e", "bytes": 49, - "line": 7784 + "line": 7918 }, { "lang": "bash", "meta": null, "sha256": "a09f283134986b066a49a41c5ef250ff1502ae2adbc3316baacf92712112cc2d", "bytes": 43, - "line": 7826 + "line": 7960 }, { "lang": "bash", "meta": null, "sha256": "d941e4f91a379820ba5d640e7ceffd42c6fdd70b5b19da9c508c259b40ce14f7", "bytes": 53, - "line": 7857 + "line": 7991 }, { "lang": "bash", "meta": null, "sha256": "fb10d8958f9f1b03ab76713459abf552755dae3334103a451c387c9d1c7b3605", "bytes": 40, - "line": 7888 + "line": 8022 }, { "lang": "bash", "meta": null, "sha256": "b5dc1e771889fb1bf5f4d81d340cfd35d3a2263c87a725341d1d5c70405eb78d", "bytes": 43, - "line": 7937 + "line": 8071 }, { "lang": "bash", "meta": null, "sha256": "736889b5b5a59bc4fe7965922999b5cbec295acad8313c1e486d5668c7da961e", "bytes": 30, - "line": 7969 + "line": 8103 }, { "lang": "bash", "meta": null, "sha256": "4d6c0bc0130fd4a83c7b2fcac6f5b4b7bc2f275c05fa6101551ac61ce1405aca", "bytes": 44, - "line": 7989 + "line": 8123 }, { "lang": "bash", "meta": null, "sha256": "62d28b9be74c32c4ba00fefe878c492a6a5c671cc4a500373d7f2bdba9f7ef79", "bytes": 54, - "line": 8009 + "line": 8143 }, { "lang": "bash", "meta": null, "sha256": "7115696baaba8ca1ecd8f7cb22f215b7a53bf13d5757bd6932267644a0377807", "bytes": 36, - "line": 8039 + "line": 8173 }, { "lang": "bash", "meta": null, "sha256": "10330e610f59c2fb169fcff11cb8147563efbcc109f875222ecbfaaa48c05f31", "bytes": 50, - "line": 8061 + "line": 8195 }, { "lang": "bash", "meta": null, "sha256": "e88ab81833a4d529271c80e2cdf0e7ff5037a60066a918a9702a28ceca73a846", "bytes": 30, - "line": 8098 + "line": 8232 }, { "lang": "bash", "meta": null, "sha256": "f094df6732656b52792e5e766a69079c3e8bd929326bda7284e0bcb760bedd79", "bytes": 28, - "line": 8128 + "line": 8262 + }, + { + "lang": "bash", + "meta": null, + "sha256": "7579bd6ac197afa0afa5f62d9b412a7e265725bd507c1c63b438a60fd72ce1d0", + "bytes": 40, + "line": 8289 + }, + { + "lang": "bash", + "meta": null, + "sha256": "63aac11606dc0eeb6cc452c20c405806840960425372cf2cf7c8b4b90a7de878", + "bytes": 62, + "line": 8309 }, { "lang": "bash", "meta": null, "sha256": "52be4758f4289e9f75875d8401cf57fda54283b2df00e413921d07303e728638", "bytes": 28, - "line": 8155 + "line": 8340 }, { "lang": "bash", "meta": null, "sha256": "4c51312bac2ca9ea2dba0e7cf27f06d5e99502bab35ab569064057a85514cc38", "bytes": 38, - "line": 8176 + "line": 8361 }, { "lang": "bash", "meta": null, "sha256": "2b26aa6f404ab19bda2d846871eb04b732f6397d1899fc28d89a29706fc0a670", "bytes": 34, - "line": 8202 + "line": 8387 }, { "lang": "bash", "meta": null, "sha256": "47af2a240e0fa4bca92d786d1fea2d4b6cd7ee8810c4540b619271457739f829", "bytes": 60, - "line": 8224 + "line": 8409 }, { "lang": "bash", "meta": null, "sha256": "f4a5d9b54707e03cc845a3ffc99493f9990d7c0a0aa96a48a492b303ef1959b4", "bytes": 45, - "line": 8257 + "line": 8442 }, { "lang": "bash", "meta": null, "sha256": "4a4ff033e75c32373422638e4f8079487d9cc37fa2b250cc2907584e690e2699", "bytes": 45, - "line": 8290 + "line": 8475 }, { "lang": "bash", "meta": null, "sha256": "66d1f7b4fc772fcb1ac84bcba2c1aa0e1a26105dda64b564c40dec35fc5fdaf2", "bytes": 43, - "line": 8320 + "line": 8505 }, { "lang": "bash", "meta": null, "sha256": "5fc8d60c19e8e3fc1c640ea87e091d0b95642a210a7e1a7d9d19e527b6f11207", "bytes": 38, - "line": 8352 + "line": 8537 }, { "lang": "bash", "meta": null, "sha256": "a7acb92552eae9b29dd7b973751bb7f3c2f7eb1dc00f36e84c594cb88dd9fc1c", "bytes": 33, - "line": 8373 + "line": 8558 }, { "lang": "bash", "meta": null, "sha256": "bb84857c237d2bfae0c6f63653597fe55263147428a5e8d36ceacfcda6bd6246", "bytes": 26, - "line": 8404 + "line": 8589 }, { "lang": "bash", "meta": null, "sha256": "3e634421662d08fe73882dd6a0d428545c33597790333f83b4c5086c8d494a1e", "bytes": 49, - "line": 8430 + "line": 8615 }, { "lang": "bash", "meta": null, "sha256": "c48dfec9df0cebb16aa976f859770a8b5beb67e2cf01dc5472dd8b3727ea164d", "bytes": 47, - "line": 8450 + "line": 8635 }, { "lang": "bash", "meta": null, "sha256": "8fd3a890425cf05e630bcd969b2aff57b3993d6632d905cab5b23cfce60e425c", "bytes": 49, - "line": 8471 + "line": 8656 }, { "lang": "bash", "meta": null, "sha256": "bd32d826b077176044eabbf175d64a747ba1a826bb36c95efb46925525f75ba9", "bytes": 45, - "line": 8497 + "line": 8682 }, { "lang": "bash", "meta": null, "sha256": "ee94be8e7fcb3c1bdb0abd6991992563371ba113b80b433cce0c71ac499f1d48", "bytes": 43, - "line": 8521 + "line": 8706 }, { "lang": "bash", "meta": null, "sha256": "bbaecaea783d60d4e8cbaad1c6951d2aa5fde5772f050166a24415162d9de3bc", "bytes": 29, - "line": 8543 + "line": 8728 }, { "lang": "bash", "meta": null, "sha256": "627ab83bbee7d7a608e8771c98556a00cb9df200a40fe9b68d79f93c322de59d", "bytes": 18, - "line": 8568 + "line": 8753 }, { "lang": "bash", "meta": null, "sha256": "9396c5d2aba2c0ff0d484a0a778dc38368154eb19edca9372cf576f8abedac90", "bytes": 32, - "line": 8578 + "line": 8763 }, { "lang": "bash", "meta": null, "sha256": "456a3638df3f1208cc5620fe2f7744e60215930e90cdada8c6d3f3041844b81c", "bytes": 22, - "line": 8588 + "line": 8773 }, { "lang": "bash", "meta": null, "sha256": "80c6f38f04b6c653c8de67e79ba7b6ca1a5ec208e8765a036048fd0adc4b4522", "bytes": 37, - "line": 8598 + "line": 8783 }, { "lang": "bash", "meta": null, "sha256": "3be0a5bf2f996779b39f57c35c08a046709ad91ffbb1ecd700c2d0f8a1c9b554", "bytes": 40, - "line": 8623 + "line": 8808 }, { "lang": "bash", "meta": null, "sha256": "94cdaecf523029bf20f25cf278f76142041e4ad9ce6980a0de225f6dd0b93a8f", "bytes": 43, - "line": 8645 + "line": 8830 }, { "lang": "bash", "meta": null, "sha256": "1731734821194e9c9ab5d0e866031dff102eab3c67b81c83780dce706461c802", "bytes": 40, - "line": 8665 + "line": 8850 }, { "lang": "bash", "meta": null, "sha256": "466187fc1a5788ab09d7700cf7cd19c9c541a571d0bc86d70fe52a1eb831b851", "bytes": 54, - "line": 8687 + "line": 8872 }, { "lang": "bash", "meta": null, "sha256": "e10bc7e084ad3b9231262208e0ac216aee47dc0ec7b1507214690ea9c098d5c9", "bytes": 32, - "line": 8719 + "line": 8904 }, { "lang": "bash", "meta": null, "sha256": "6bb0cf09f6bd8ba02e1f24a5eed89b35c0226ec7eefce07502015a455520fd0f", "bytes": 33, - "line": 8729 + "line": 8914 }, { "lang": "bash", "meta": null, "sha256": "23acce8016e243ab651bcc860cd8f1b1d236e6a5b7951abc76282cd674b2a25e", "bytes": 39, - "line": 8752 + "line": 8937 }, { "lang": "bash", "meta": null, - "sha256": "ea1cc85c4e5a1b39b5b26bca357d22583d993fbd321ba62dc529b811a553b050", - "bytes": 32, - "line": 8776 + "sha256": "210e8ab3d8af173ce648501ca50c828325ac1f4452006ad4485cb7a24d760ae4", + "bytes": 37, + "line": 8961 + }, + { + "lang": "bash", + "meta": null, + "sha256": "cdff2d4272d5bc3a18729e63e5ece9053d33ac0f8faf35cfaa3a289baa2dccc1", + "bytes": 48, + "line": 8983 + }, + { + "lang": "bash", + "meta": null, + "sha256": "c307203dd609208b90cbc0372e3401a75e943421bdf77e4e10779ed2c89ca4ff", + "bytes": 48, + "line": 9013 }, { "lang": "bash", "meta": null, "sha256": "f6340c3d5dc4eedb98435df593ff277d0e44fbcb4c61e71a05123267b5352e78", "bytes": 29, - "line": 8796 + "line": 9043 }, { "lang": "bash", "meta": null, "sha256": "ca6e405dc70365db67ec66e6c2499fedf89ae3d7a90f4b5d044a2aab13bb8f37", "bytes": 37, - "line": 8818 + "line": 9065 }, { "lang": "bash", "meta": null, "sha256": "52918df5b4d4003d22322d5785d8a0202a2b66d39a3c71171f92a65545e28788", "bytes": 37, - "line": 8839 + "line": 9086 }, { "lang": "bash", "meta": null, "sha256": "28e77f2a35930390cc7de2fc5abdce7809311c33b10eb749941d5c44b9de0cfa", "bytes": 37, - "line": 8864 + "line": 9111 }, { "lang": "bash", "meta": null, "sha256": "088f9018d5639eac73c710bf1ef4fd134b0cd4c290587a161af1322b415a013f", "bytes": 29, - "line": 8891 + "line": 9138 }, { "lang": "bash", "meta": null, "sha256": "67384a12d7642ea8a82e807a58e42f921ea37cb496e2315ac87368ae0e297e4d", "bytes": 29, - "line": 8923 + "line": 9170 }, { "lang": "bash", "meta": null, "sha256": "4c9e46c0b1c48cb348bfb37e4c705e4dfe1cd5f221d667ebd02dd7325fce7e29", "bytes": 38, - "line": 8955 + "line": 9202 }, { "lang": "bash", "meta": null, "sha256": "20fbb381c0b4ef786f755f485789ecb9e76538ec6394956bb6153ee4a79547ca", "bytes": 31, - "line": 8978 + "line": 9225 }, { "lang": "bash", "meta": null, "sha256": "54ffdfd98a1fc3aad38e602c1d02336a81fe97d48e6732402a7980d0f55cb20d", "bytes": 40, - "line": 8999 + "line": 9246 } ], "links": [ @@ -17713,6 +17229,20 @@ "bytes": 54, "line": 404 }, + { + "lang": "bash", + "meta": null, + "sha256": "7579bd6ac197afa0afa5f62d9b412a7e265725bd507c1c63b438a60fd72ce1d0", + "bytes": 40, + "line": 436 + }, + { + "lang": "bash", + "meta": null, + "sha256": "63aac11606dc0eeb6cc452c20c405806840960425372cf2cf7c8b4b90a7de878", + "bytes": 62, + "line": 454 + }, { "lang": "bash", "meta": null, @@ -18286,10 +17816,24 @@ { "lang": "bash", "meta": null, - "sha256": "ea1cc85c4e5a1b39b5b26bca357d22583d993fbd321ba62dc529b811a553b050", - "bytes": 32, + "sha256": "210e8ab3d8af173ce648501ca50c828325ac1f4452006ad4485cb7a24d760ae4", + "bytes": 37, "line": 42 }, + { + "lang": "bash", + "meta": null, + "sha256": "c307203dd609208b90cbc0372e3401a75e943421bdf77e4e10779ed2c89ca4ff", + "bytes": 48, + "line": 62 + }, + { + "lang": "bash", + "meta": null, + "sha256": "cdff2d4272d5bc3a18729e63e5ece9053d33ac0f8faf35cfaa3a289baa2dccc1", + "bytes": 48, + "line": 94 + }, { "lang": "bash", "meta": null, @@ -67519,7 +67063,15 @@ "line": 122 } ], - "codeFences": [], + "codeFences": [ + { + "lang": "bash", + "meta": null, + "sha256": "b867716c5b178cc345e9ec091ffd5db588c778b02f244e3e51513c9266fbaf8a", + "bytes": 733, + "line": 199 + } + ], "links": [ "https://console.cloud.google.com/", "#scopes-reference", @@ -141986,12 +141538,19 @@ ], "expressions": [], "codeFences": [ + { + "lang": "sql", + "meta": null, + "sha256": "96716701cef0ce8a3205217105b932f669acc115ec2faed939bf81704eb83ed2", + "bytes": 169, + "line": 29 + }, { "lang": "sql", "meta": null, "sha256": "0fbd73d76ccb6a73e3f2f817a592bff15a0d1ffb45b4425fbfc91cc7ac0a48da", "bytes": 134, - "line": 46 + "line": 94 } ], "links": [ @@ -177603,6 +177162,13 @@ "sha256": "5c2ebfcc044ebc01c17a4af2664a4fbc667ae4916ef21555d90cfd9f245394bd", "bytes": 70, "line": 56 + }, + { + "lang": "yaml", + "meta": "title=\".gitlab-ci.yml\"", + "sha256": "bf3d6e7bec6b87f3966580713633eb8baed778c9dced4569a65cc62d790dee42", + "bytes": 1084, + "line": 98 } ], "links": [ @@ -182218,6 +181784,13 @@ ], "expressions": [], "codeFences": [ + { + "lang": "bash", + "meta": null, + "sha256": "32c20ca5dbc0941290361d3ddba31d3585a80493e65c486960e2660fae4c03d0", + "bytes": 243, + "line": 21 + }, { "lang": "bash", "meta": null, @@ -233534,12 +233107,12 @@ }, { "source": "/sdks/typescript", - "destination": "/api-reference/typescript", + "destination": "/api-reference/getting-started", "permanent": true }, { "source": "/sdks/python", - "destination": "/api-reference/python", + "destination": "/api-reference/getting-started", "permanent": true }, { @@ -234154,12 +233727,12 @@ }, { "source": "/:lang(en|es|fr|de|ja|zh)/sdks/typescript", - "destination": "/api-reference/typescript", + "destination": "/api-reference/getting-started", "permanent": true }, { "source": "/:lang(en|es|fr|de|ja|zh)/sdks/python", - "destination": "/api-reference/python", + "destination": "/api-reference/getting-started", "permanent": true }, { diff --git a/apps/realtime/package.json b/apps/realtime/package.json index e1ab2cd0645..373ce5c95ca 100644 --- a/apps/realtime/package.json +++ b/apps/realtime/package.json @@ -47,6 +47,6 @@ "@types/node": "24.19.2", "socket.io-client": "4.8.4", "typescript": "^7.0.2", - "vitest": "^5.0.4" + "vitest": "^5.1.0" } } diff --git a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts index a5ef78efab1..00be285e60a 100644 --- a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts +++ b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts @@ -128,14 +128,14 @@ export const RESOURCES_MENU: NavMenu = { items: [ { title: 'Docs', - description: 'Guides, SDKs, and API reference', + description: 'Guides, CLI, and API reference', href: 'https://docs.sim.ai', external: true, preview: { kind: 'docs', eyebrow: 'Documentation', title: 'Build with Sim', - details: ['Quickstart', 'SDKs', 'API reference'], + details: ['Quickstart', 'CLI', 'API reference'], }, }, { diff --git a/apps/sim/app/account/settings/layout.test.tsx b/apps/sim/app/account/settings/layout.test.tsx new file mode 100644 index 00000000000..a3214f53af2 --- /dev/null +++ b/apps/sim/app/account/settings/layout.test.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from 'react' +import { authMockFns } from '@sim/testing' +import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock' +import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock' +import { renderToStaticMarkup } from 'react-dom/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('next/navigation', () => nextNavigationMock) +vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/permissions/super-user', () => ({ isPlatformAdmin: vi.fn(async () => false) })) +vi.mock('@/components/settings/standalone-settings-shell', () => ({ + StandaloneSettingsShell: ({ children }: { children: ReactNode }) => children, +})) + +import AccountSettingsLayout from '@/app/account/settings/layout' +import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider' + +/** + * Renders the account layout around a section that reads the read-only key flag the way the + * personal API keys page's create modal does on every render, and returns what it read. + */ +async function readFlagInAccountSettings(): Promise { + let observed: boolean | undefined + function ReadOnlyKeysSection() { + observed = useFeatureFlag('api-key-read-only') + return null + } + renderToStaticMarkup(await AccountSettingsLayout({ children: })) + if (observed === undefined) throw new Error('The account section never rendered') + return observed +} + +describe('AccountSettingsLayout', () => { + beforeEach(() => { + authMockFns.mockGetSession.mockResolvedValue({ + user: { id: 'viewer-1' }, + session: { id: 'session-1' }, + }) + }) + + it.each([true, false])('gives account sections the read-only key flag (%s)', async (enabled) => { + featureFlagsMockFns.mockIsFeatureEnabled.mockImplementation( + async (flag) => flag === 'api-key-read-only' && enabled + ) + expect(await readFlagInAccountSettings()).toBe(enabled) + }) +}) diff --git a/apps/sim/app/account/settings/layout.tsx b/apps/sim/app/account/settings/layout.tsx index e18e8bb84f1..2cc46a3d1f8 100644 --- a/apps/sim/app/account/settings/layout.tsx +++ b/apps/sim/app/account/settings/layout.tsx @@ -1,21 +1,38 @@ import { redirect } from 'next/navigation' import { StandaloneSettingsShell } from '@/components/settings/standalone-settings-shell' +import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag' import { getSession } from '@/lib/auth' import { resolveDeploymentShape } from '@/lib/core/config/deployment-shape' import { isPlatformAdmin } from '@/lib/permissions/super-user' +import { FeatureFlagsProvider } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider' export default async function AccountSettingsLayout({ children }: { children: React.ReactNode }) { const session = await getSession() if (!session?.user) redirect('/login') - const isSuperUser = await isPlatformAdmin(session.user.id) + const [isSuperUser, readOnlyApiKeysEnabled] = await Promise.all([ + isPlatformAdmin(session.user.id), + isReadOnlyApiKeyCreationEnabled(), + ]) + /** Account settings host no workspace surface, so only the flags its sections read are on. */ return ( - - {children} - + + {children} + + ) } diff --git a/apps/sim/app/api/auth/oauth/token/route.test.ts b/apps/sim/app/api/auth/oauth/token/route.test.ts index f0784fcd428..9152d3bfa1d 100644 --- a/apps/sim/app/api/auth/oauth/token/route.test.ts +++ b/apps/sim/app/api/auth/oauth/token/route.test.ts @@ -7,6 +7,7 @@ import { createMockRequest, hybridAuthMockFns, } from '@sim/testing' +import { createRouteContext } from '@sim/testing/helpers/http' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -36,11 +37,13 @@ vi.mock('@/lib/credentials/application/resolve-managed-oauth-token', () => ({ resolveManagedOAuthCredentialToken: { execute: mockResolveManagedOAuthCredentialToken }, })) +import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { createQuickBooksAccountId } from '@/lib/oauth/quickbooks' import { GET, POST } from '@/app/api/auth/oauth/token/route' import { getToolMetadata } from '@/tools/metadata' const mockGetToolMetadata = vi.mocked(getToolMetadata) +const mockCheckSessionOrInternalAuth = vi.mocked(checkSessionOrInternalAuth) describe('OAuth Token API Routes', () => { beforeEach(() => { @@ -290,6 +293,116 @@ describe('OAuth Token API Routes', () => { }) }) + describe('saved service-account credentials', () => { + const authorized = { + ok: true, + requesterUserId: 'test-user-id', + credentialOwnerUserId: 'test-user-id', + workspaceId: 'workspace-id', + } + + beforeEach(() => { + authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValueOnce({ + accountId: '', + credentialId: 'saved-credential-id', + credentialType: 'service_account', + providerId: 'vanta-service-account', + workspaceId: 'workspace-id', + usedCredentialTable: true, + }) + authOAuthUtilsMockFns.mockResolveServiceAccountToken.mockResolvedValue({ + accessToken: 'saved-secret', + }) + mockGetToolMetadata.mockReturnValue({ + id: 'vanta_list_tests', + params: {}, + oauth: { required: true, provider: 'vanta', credentialKind: 'service-account' }, + }) + }) + + it('refuses to release the secret to a session caller', async () => { + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ + success: true, + authType: AuthType.SESSION, + userId: 'test-user-id', + }) + mockAuthorizeCredentialUse.mockResolvedValueOnce(authorized) + + const response = await POST( + createMockRequest('POST', { + credentialId: 'saved-credential-id', + toolId: 'vanta_list_tests', + }), + createRouteContext({}) + ) + + expect(response.status).toBe(403) + const body = await response.json() + expect(body).toMatchObject({ code: 'SERVICE_ACCOUNT_SESSION_FORBIDDEN' }) + expect(JSON.stringify(body)).not.toContain('saved-secret') + expect(authOAuthUtilsMockFns.mockResolveServiceAccountToken).not.toHaveBeenCalled() + }) + + it('resolves the secret for an internal-JWT execution caller', async () => { + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ + success: true, + authType: AuthType.INTERNAL_JWT, + userId: 'test-user-id', + }) + mockAuthorizeCredentialUse.mockResolvedValueOnce(authorized) + + const response = await POST( + createMockRequest('POST', { + credentialId: 'saved-credential-id', + toolId: 'vanta_list_tests', + workflowId: 'workflow-id', + }), + createRouteContext({}) + ) + + expect(response.status).toBe(200) + await expect(response.json()).resolves.toMatchObject({ + accessToken: 'saved-secret', + credentialType: 'service_account', + }) + }) + }) + + it('still returns an OAuth token to a session caller', async () => { + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ + success: true, + authType: AuthType.SESSION, + userId: 'test-user-id', + }) + mockAuthorizeCredentialUse.mockResolvedValueOnce({ + ok: true, + requesterUserId: 'test-user-id', + credentialOwnerUserId: 'test-user-id', + }) + authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({ + id: 'credential-id', + accessToken: 'test-token', + refreshToken: 'refresh-token', + accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), + providerId: 'google', + }) + authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({ + accessToken: 'session-oauth-token', + refreshed: false, + }) + + const response = await POST( + createMockRequest('POST', { credentialId: 'credential-id' }), + createRouteContext({}) + ) + + expect(response.status).toBe(200) + await expect(response.json()).resolves.toMatchObject({ + accessToken: 'session-oauth-token', + credentialType: 'oauth', + }) + }) + describe('credentialAccountUserId + providerId path', () => { it('should reject internal JWT authentication', async () => { hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ diff --git a/apps/sim/app/api/cli/auth/poll/route.ts b/apps/sim/app/api/cli/auth/poll/route.ts index 19d9646bbb3..50517528091 100644 --- a/apps/sim/app/api/cli/auth/poll/route.ts +++ b/apps/sim/app/api/cli/auth/poll/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { pollCliAuthContract } from '@/lib/api/contracts' import { parseRequest } from '@/lib/api/server' import { + apiKeyCreationErrorStatus, performCreatePersonalApiKey, performCreateWorkspaceApiKey, } from '@/lib/api-key/orchestration' @@ -83,7 +84,7 @@ async function mintForGrant( if (!result.success || !result.key) { return { ok: false, - status: result.errorCode === 'conflict' ? 409 : 500, + status: apiKeyCreationErrorStatus(result.errorCode), message: result.error ?? 'Failed to generate API key', } } diff --git a/apps/sim/app/api/credentials/workload-identity/route.ts b/apps/sim/app/api/credentials/workload-identity/route.ts new file mode 100644 index 00000000000..e9d22dd8a7a --- /dev/null +++ b/apps/sim/app/api/credentials/workload-identity/route.ts @@ -0,0 +1,20 @@ +import { prepareWorkloadIdentityContract } from '@/lib/api/contracts/credentials' +import { + defineInternalJsonRoute, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { internalCredentialErrorPolicy } from '@/lib/credentials/api/route-policies' +import { credentialOperations } from '@/lib/credentials/application/operations' +import { prepareWorkloadIdentity } from '@/lib/credentials/application/prepare-workload-identity' + +export const POST = defineInternalJsonRoute({ + contract: prepareWorkloadIdentityContract, + auth: internalSessionAuth, + operation: credentialOperations.prepareWorkloadIdentity, + rateLimit: internalRateLimits.user({ bucketName: 'credentials.workload_identity.prepare' }), + errorPolicy: internalCredentialErrorPolicy, + mapInput: ({ body }) => body, + useCase: prepareWorkloadIdentity, + present: (result) => result, +}) diff --git a/apps/sim/app/api/desktop/update/latest-mac.yml/route.test.ts b/apps/sim/app/api/desktop/update/latest-mac.yml/route.test.ts index 780c4ad7c17..9ac20452df8 100644 --- a/apps/sim/app/api/desktop/update/latest-mac.yml/route.test.ts +++ b/apps/sim/app/api/desktop/update/latest-mac.yml/route.test.ts @@ -173,7 +173,7 @@ describe('desktop update manifest route', () => { it('walks past a page of unrelated releases to reach the newest desktop build', async () => { const filler = Array.from({ length: DESKTOP_RELEASES_PAGE_SIZE }, (_, index) => ({ - tag_name: `python-sdk-v0.${index}.0`, + tag_name: `helm-chart-1.${index}.0`, draft: false, prerelease: false, assets: [], @@ -200,7 +200,7 @@ describe('desktop update manifest route', () => { it('fails the feed instead of serving an older release when a page cannot be read', async () => { const filler = Array.from({ length: DESKTOP_RELEASES_PAGE_SIZE }, (_, index) => ({ - tag_name: `python-sdk-v0.${index}.0`, + tag_name: `helm-chart-1.${index}.0`, draft: false, prerelease: false, assets: [], diff --git a/apps/sim/app/api/mcp/discover/route.ts b/apps/sim/app/api/mcp/discover/route.ts index 381fee62ab3..dbbe810cd17 100644 --- a/apps/sim/app/api/mcp/discover/route.ts +++ b/apps/sim/app/api/mcp/discover/route.ts @@ -3,6 +3,7 @@ import { workflowMcpServer, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' +import { API_KEY_EXPIRED } from '@/lib/api-key/policy-messages' import { checkHybridAuth } from '@/lib/auth/hybrid' import { getBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -21,7 +22,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => { if (!auth.success || !auth.userId) { return NextResponse.json( - { success: false, error: 'Authentication required. Provide X-API-Key header.' }, + { + success: false, + error: + auth.error === API_KEY_EXPIRED + ? API_KEY_EXPIRED + : 'Authentication required. Provide X-API-Key header.', + }, { status: 401 } ) } diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts index 7a1d7bd850d..e450390e946 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts @@ -80,6 +80,7 @@ const PERSONAL_API_KEY_PRINCIPAL = { kind: 'personal_api_key', userId: 'user-1', keyId: 'personal-key-1', + access: 'write', } as const const OAUTH_WRITE_PRINCIPAL = { diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.ts index aa1464b421d..7bfefdb42dd 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.ts @@ -17,7 +17,11 @@ import { SUPPORTED_PROTOCOL_VERSIONS, type Tool, } from '@modelcontextprotocol/sdk/types.js' -import { isUserCredentialPrincipal, type WorkflowExecutionPrincipal } from '@sim/auth/principal' +import { + isReadOnlyApiKeyPrincipal, + isUserCredentialPrincipal, + type WorkflowExecutionPrincipal, +} from '@sim/auth/principal' import { db } from '@sim/db' import { workflow, @@ -37,7 +41,11 @@ import { mcpServeRouteParamsSchema, mcpToolCallParamsSchema, } from '@/lib/api/contracts/mcp' -import { PERSONAL_KEY_DENIED } from '@/lib/api-key/policy-messages' +import { + API_KEY_EXPIRED, + PERSONAL_KEY_DENIED, + READ_ONLY_API_KEY_DENIED, +} from '@/lib/api-key/policy-messages' import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid' import { InvalidOAuthAccessTokenError, @@ -483,10 +491,14 @@ async function resolveWorkflowMcpBillingAttribution( * A 401 that points OAuth clients at this server's protected-resource metadata, * so they can discover Sim's authorization server and start the flow. */ -function unauthorizedResponse(serverId: string, invalidToken = false): NextResponse { +function unauthorizedResponse( + serverId: string, + invalidToken = false, + error?: string +): NextResponse { return withWorkflowMcpAuthChallenge( NextResponse.json( - { error: invalidToken ? 'Invalid access token' : 'Unauthorized' }, + { error: invalidToken ? 'Invalid access token' : (error ?? 'Unauthorized') }, { status: 401, ...(invalidToken && { headers: { 'WWW-Authenticate': 'Bearer error="invalid_token"' } }), @@ -561,7 +573,10 @@ async function authorizeMcpServeRequest( } } if (!auth.success || !auth.userId) { - return { response: unauthorizedResponse(server.id) } + const expired = auth.error === API_KEY_EXPIRED + return { + response: unauthorizedResponse(server.id, false, expired ? API_KEY_EXPIRED : undefined), + } } if (!auth.principal) { throw new Error('Authenticated MCP request is missing its principal') @@ -604,13 +619,24 @@ async function authorizeMcpServeRequest( } } -/** Calling a tool runs a workflow, so an OAuth token needs `api:write`. */ +/** + * Calling a tool runs a workflow, so an OAuth token needs `api:write` and a + * read-only API key is refused outright: it has no scope to step up to. + */ function insufficientToolCallScopeResponse( id: RequestId, serverId: string, executeAuthContext: ExecuteAuthContext | null ): NextResponse | null { const principal = executeAuthContext?.principal + if (principal && isReadOnlyApiKeyPrincipal(principal)) { + return NextResponse.json( + createError(id, ErrorCode.InvalidRequest, READ_ONLY_API_KEY_DENIED, { + code: 'READ_ONLY_API_KEY', + }), + { status: 403 } + ) + } if (principal?.kind !== 'oauth_access_token') return null if (oauthScopeSatisfies(principal.scopes, OAUTH_API_WRITE_SCOPE)) return null return insufficientScopeResponse( diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts index 56955c216d2..54f3a68e6e1 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts @@ -185,6 +185,7 @@ describe('POST /api/resume/[workflowId]/[executionId]/[contextId]', () => { success: true, keyId: WORKSPACE_KEY_PRINCIPAL.keyId, keyType: 'workspace', + access: 'write', workspaceId: WORKSPACE_ID, }) mockResolvePermission.mockResolvedValue('write') @@ -250,6 +251,7 @@ describe('POST /api/resume/[workflowId]/[executionId]/[contextId]', () => { success: true, keyId: 'personal-key', keyType: 'personal', + access: 'write', userId: 'read-only-member', }), }, diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts index 26ac84b84d8..afbafe19338 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts @@ -160,7 +160,7 @@ export const GET = withRouteHandler( } ) => { const { workflowId: requestedWorkflowId } = await context.params - const access = await validateWorkflowAccess(request, requestedWorkflowId, false) + const access = await validateWorkflowAccess(request, requestedWorkflowId) if (access.error) { return NextResponse.json({ error: access.error.message }, { status: access.error.status }) } diff --git a/apps/sim/app/api/schedules/[id]/route.test.ts b/apps/sim/app/api/schedules/[id]/route.test.ts deleted file mode 100644 index 82b401a1f2c..00000000000 --- a/apps/sim/app/api/schedules/[id]/route.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Tests for schedule reactivate PUT API route - */ -import { - auditMock, - authMockFns, - databaseMock, - workflowAuthzMockFns, - workflowsUtilsMock, -} from '@sim/testing' -import { NextRequest } from 'next/server' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) - -vi.mock('@sim/audit', () => auditMock) - -import { PUT } from './route' - -function createRequest(body: Record): NextRequest { - return new NextRequest(new URL('http://test/api/schedules/sched-1'), { - method: 'PUT', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }) -} - -function createParams(id: string): { params: Promise<{ id: string }> } { - return { params: Promise.resolve({ id }) } -} - -const mockDbSelect = databaseMock.db.select as ReturnType -const mockDbUpdate = databaseMock.db.update as ReturnType - -function mockDbChain(selectResults: unknown[][]) { - let selectCallIndex = 0 - mockDbSelect.mockImplementation(() => ({ - from: () => ({ - where: () => ({ - limit: () => selectResults[selectCallIndex++] || [], - }), - }), - })) - - mockDbUpdate.mockImplementation(() => ({ - set: () => ({ - where: vi.fn().mockResolvedValue({}), - }), - })) -} - -describe('Schedule PUT API (Reactivate)', () => { - beforeEach(() => { - authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } }) - workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ - allowed: true, - status: 200, - workflow: { id: 'wf-1', workspaceId: 'ws-1' }, - workspacePermission: 'write', - }) - }) - - describe('Authorization', () => { - it('returns 403 when user is not workflow owner', async () => { - workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ - allowed: false, - status: 403, - workflow: { id: 'wf-1', workspaceId: null }, - workspacePermission: null, - message: - 'This workflow is not attached to a workspace. Personal workflows are deprecated and cannot be accessed.', - }) - mockDbChain([ - [{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }], - [{ userId: 'other-user', workspaceId: null }], - ]) - - const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1')) - - expect(res.status).toBe(403) - const data = await res.json() - expect(data.error).toContain('Personal workflows are deprecated') - }) - - it('returns 403 for workspace member with only read permission', async () => { - workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ - allowed: false, - status: 403, - workflow: { id: 'wf-1', workspaceId: 'ws-1' }, - workspacePermission: 'read', - message: 'Unauthorized: Access denied to write this workflow', - }) - mockDbChain([ - [{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }], - [{ userId: 'other-user', workspaceId: 'ws-1' }], - ]) - - const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1')) - - expect(res.status).toBe(403) - }) - }) - - describe('Schedule State Handling', () => { - it('returns 400 when schedule has invalid cron expression', async () => { - mockDbChain([ - [ - { - id: 'sched-1', - workflowId: 'wf-1', - status: 'disabled', - cronExpression: 'invalid-cron', - timezone: 'UTC', - }, - ], - [{ userId: 'user-1', workspaceId: null }], - ]) - - const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1')) - - expect(res.status).toBe(400) - const data = await res.json() - expect(data.error).toBe('Schedule has invalid cron expression') - }) - }) -}) diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts index 2aa958fe13d..6fc6bca6206 100644 --- a/apps/sim/app/api/schedules/[id]/route.ts +++ b/apps/sim/app/api/schedules/[id]/route.ts @@ -2,19 +2,26 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflowSchedule } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { - assertWorkflowMutable, - authorizeWorkflowByWorkspacePermission, - WorkflowLockedError, -} from '@sim/platform-authz/workflow' +import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getScheduleByIdContract, updateScheduleContract } from '@/lib/api/contracts/schedules' import { parseRequest } from '@/lib/api/server' +import { + createInternalResourceConcealmentPolicy, + defineInternalJsonRoute, + extendInternalErrorPolicy, + internalErrorResponse, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' import { getSession } from '@/lib/auth' +import { InsufficientWorkspacePermissionsError } from '@/lib/core/application' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { validateCronExpression } from '@/lib/workflows/schedules/utils' +import { workflowOperations } from '@/lib/workflows/application/operations' +import { setWorkflowTriggerEnabled } from '@/lib/workflows/application/triggers' const logger = createLogger('ScheduleAPI') @@ -97,124 +104,57 @@ export const GET = withRouteHandler( } ) -export const PUT = withRouteHandler( - async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { - const requestId = generateRequestId() - - try { - const session = await getSession() - if (!session?.user?.id) { - logger.warn('Unauthorized schedule update attempt') - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const parsed = await parseRequest(updateScheduleContract, request, context, { - validationErrorResponse: () => - NextResponse.json({ error: 'Invalid request body' }, { status: 400 }), - }) - if (!parsed.success) return parsed.response - - const { id: scheduleId } = parsed.data.params - const validatedBody = parsed.data.body - - const result = await fetchAndAuthorize(requestId, scheduleId, session.user.id, 'write') - if (result instanceof NextResponse) return result - const { schedule, workspaceId } = result - if (schedule.workflowId) { - await assertWorkflowMutable(schedule.workflowId) +/** + * Disables or reactivates a workflow schedule through the same use case as + * `PATCH /api/v2/workflows/{workflowId}/triggers/{blockId}`. The id must name + * the live deployment's row: a schedule of an older version answers 404 + * instead of being revived. + */ +export const PUT = defineInternalJsonRoute({ + contract: updateScheduleContract, + auth: internalSessionAuth, + operation: workflowOperations.setTriggerEnabled, + rateLimit: internalRateLimits.none({ + reason: 'Schedule enablement retains its existing session admission policy.', + }), + errorPolicy: createInternalResourceConcealmentPolicy({ + notFoundMessage: 'Schedule not found', + base: extendInternalErrorPolicy( + { + project: internalOrchestrationErrorPolicy.project, + unhandled: () => internalErrorResponse(500, { error: 'Failed to update schedule' }), + }, + (error) => + error instanceof InsufficientWorkspacePermissionsError + ? internalErrorResponse(403, { + error: 'Unauthorized: Access denied to write this workflow', + }) + : null + ), + }), + parseOptions: { + validationErrorResponse: () => + NextResponse.json({ error: 'Invalid request body' }, { status: 400 }), + }, + mapInput: ({ params, body }) => ({ + target: { kind: 'row' as const, table: 'schedule' as const, id: params.id }, + enabled: body.action === 'reactivate', + }), + useCase: setWorkflowTriggerEnabled, + present: ({ trigger, changes }, { input }) => { + const changed = changes.length > 0 + if (!input.enabled) { + return { + message: changed ? 'Schedule disabled successfully' : 'Schedule is already disabled', } - - const { action } = validatedBody - - if (action === 'disable') { - if (schedule.status === 'disabled') { - return NextResponse.json({ message: 'Schedule is already disabled' }) - } - - await db - .update(workflowSchedule) - .set({ status: 'disabled', nextRunAt: null, lastQueuedAt: null, updatedAt: new Date() }) - .where(and(eq(workflowSchedule.id, scheduleId), isNull(workflowSchedule.archivedAt))) - - logger.info(`Disabled schedule: ${scheduleId}`) - - recordAudit({ - workspaceId, - actorId: session.user.id, - actorName: session.user.name, - actorEmail: session.user.email, - action: AuditAction.SCHEDULE_UPDATED, - resourceType: AuditResourceType.SCHEDULE, - resourceId: scheduleId, - resourceName: schedule.jobTitle ?? undefined, - description: `Disabled schedule "${schedule.jobTitle ?? scheduleId}"`, - metadata: { - operation: 'disable', - sourceType: schedule.sourceType, - previousStatus: schedule.status, - }, - request, - }) - - return NextResponse.json({ message: 'Schedule disabled successfully' }) - } - - // reactivate - if (schedule.status === 'active') { - return NextResponse.json({ message: 'Schedule is already active' }) - } - - if (!schedule.cronExpression) { - logger.error(`Schedule has no cron expression: ${scheduleId}`) - return NextResponse.json({ error: 'Schedule has no cron expression' }, { status: 400 }) - } - - const cronResult = validateCronExpression(schedule.cronExpression, schedule.timezone || 'UTC') - if (!cronResult.isValid || !cronResult.nextRun) { - logger.error(`Invalid cron expression for schedule: ${scheduleId}`) - return NextResponse.json({ error: 'Schedule has invalid cron expression' }, { status: 400 }) - } - - const now = new Date() - const nextRunAt = cronResult.nextRun - - await db - .update(workflowSchedule) - .set({ status: 'active', failedCount: 0, infraRetryCount: 0, updatedAt: now, nextRunAt }) - .where(and(eq(workflowSchedule.id, scheduleId), isNull(workflowSchedule.archivedAt))) - - logger.info(`Reactivated schedule: ${scheduleId}`) - - recordAudit({ - workspaceId, - actorId: session.user.id, - actorName: session.user.name, - actorEmail: session.user.email, - action: AuditAction.SCHEDULE_UPDATED, - resourceType: AuditResourceType.SCHEDULE, - resourceId: scheduleId, - resourceName: schedule.jobTitle ?? undefined, - description: `Reactivated schedule "${schedule.jobTitle ?? scheduleId}"`, - metadata: { - operation: 'reactivate', - sourceType: schedule.sourceType, - cronExpression: schedule.cronExpression, - timezone: schedule.timezone, - }, - request, - }) - - return NextResponse.json({ message: 'Schedule activated successfully', nextRunAt }) - } catch (error) { - if (error instanceof WorkflowLockedError) { - return NextResponse.json({ error: error.message }, { status: error.status }) - } - - logger.error('Error updating schedule', error) - return NextResponse.json({ error: 'Failed to update schedule' }, { status: 500 }) } - } -) + if (!changed) return { message: 'Schedule is already active' } + return { + message: 'Schedule activated successfully', + ...(trigger.nextRunAt ? { nextRunAt: trigger.nextRunAt.toISOString() } : {}), + } + }, +}) export const DELETE = withRouteHandler( async (request: NextRequest, { params }: { params: Promise<{ id: string }> }) => { diff --git a/apps/sim/app/api/users/me/api-keys/route.ts b/apps/sim/app/api/users/me/api-keys/route.ts index de3fe9f43b0..f74c5a599ac 100644 --- a/apps/sim/app/api/users/me/api-keys/route.ts +++ b/apps/sim/app/api/users/me/api-keys/route.ts @@ -14,7 +14,7 @@ import { listPersonalApiKeys, personalApiKeyOperations, } from '@/lib/api-key/application/personal-api-keys' -import { performCreatePersonalApiKey } from '@/lib/api-key/orchestration' +import { apiKeyCreationErrorStatus, performCreatePersonalApiKey } from '@/lib/api-key/orchestration' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { capabilityRefusal } from '@/lib/permission-groups/capability-assertions' @@ -79,23 +79,29 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const parsed = await parseRequest(createPersonalApiKeyContract, request, {}) if (!parsed.success) return parsed.response - const { name } = parsed.data.body + const { name, access, expiresAt } = parsed.data.body const result = await performCreatePersonalApiKey({ userId, name, + access, + expiresAt, actorName: session.user.name, actorEmail: session.user.email, request, }) if (!result.success || !result.key) { - const status = result.errorCode === 'conflict' ? 409 : 500 - return NextResponse.json({ error: result.error }, { status }) + return NextResponse.json( + { error: result.error }, + { status: apiKeyCreationErrorStatus(result.errorCode) } + ) } captureServerEvent(userId, 'api_key_created', { key_name: name, scope: 'personal', + access, + expires: expiresAt != null, }) return NextResponse.json({ key: result.key }) diff --git a/apps/sim/app/api/v1/auth.test.ts b/apps/sim/app/api/v1/auth.test.ts index 4678f115e6d..7610abfabf3 100644 --- a/apps/sim/app/api/v1/auth.test.ts +++ b/apps/sim/app/api/v1/auth.test.ts @@ -21,6 +21,7 @@ describe('v1 API key authentication', () => { userId: 'user-1', keyId: 'key-1', keyType: 'personal', + access: 'write', }) await expect( @@ -32,7 +33,7 @@ describe('v1 API key authentication', () => { ) ).resolves.toMatchObject({ authenticated: true, - principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' }, + principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1', access: 'write' }, }) }) @@ -42,6 +43,7 @@ describe('v1 API key authentication', () => { userId: 'creator-1', keyId: 'key-1', keyType: 'workspace', + access: 'write', workspaceId: 'workspace-1', }) @@ -62,6 +64,7 @@ describe('v1 API key authentication', () => { userId: 'creator-1', keyId: 'key-1', keyType: 'workspace', + access: 'write', }) await expect( diff --git a/apps/sim/app/api/v1/auth.ts b/apps/sim/app/api/v1/auth.ts index 1d9d4d14d1d..f7ab808ded2 100644 --- a/apps/sim/app/api/v1/auth.ts +++ b/apps/sim/app/api/v1/auth.ts @@ -5,6 +5,7 @@ import { } from '@sim/auth/principal' import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' +import { apiKeyPrincipal } from '@/lib/api-key/principal' import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service' import { isAuthDisabled } from '@/lib/core/config/env-flags' @@ -29,6 +30,7 @@ export async function authenticateV1Request(request: NextRequest): Promise = new Set(['/api/v1/knowledge/search']) + +/** + * v1 authorizes in its handlers rather than through the operation funnel, so a + * read-only key is held to reads here, by method: every v1 `POST`, `PUT`, + * `PATCH`, and `DELETE` but search creates, changes, deploys, or deletes. + */ +function isV1ReadRequest(request: NextRequest): boolean { + if (request.method === 'GET' || request.method === 'HEAD') return true + return request.method === 'POST' && V1_READ_ONLY_POST_PATHS.has(request.nextUrl.pathname) } interface AuthorizedRequest { @@ -163,6 +185,21 @@ export async function checkRateLimit( } } + if (auth.principal?.access === 'read' && !isV1ReadRequest(request)) { + return { + allowed: false, + remaining: 0, + limit: 0, + resetAt: new Date(), + refusal: { + status: 403, + code: 'FORBIDDEN', + message: READ_ONLY_API_KEY_DENIED, + details: { code: 'READ_ONLY_API_KEY' }, + }, + } + } + const userId = auth.userId! const subscription = await getHighestPrioritySubscription(userId) @@ -240,6 +277,7 @@ export async function authenticateRequest( } export function createRateLimitResponse(result: RateLimitResult): NextResponse { + if (result.refusal) return workspaceAccessErrorResponse(result.refusal) /** * An authentication failure never reaches the token bucket, so there is no * limit to report. Publishing a placeholder told unauthenticated callers they diff --git a/apps/sim/app/api/v2/access-requests.test.ts b/apps/sim/app/api/v2/access-requests.test.ts index 3f339f51054..2a726160727 100644 --- a/apps/sim/app/api/v2/access-requests.test.ts +++ b/apps/sim/app/api/v2/access-requests.test.ts @@ -69,7 +69,12 @@ import { GET as listWorkspace, } from '@/app/api/v2/workspaces/[workspaceId]/access-requests/route' -const principal = { kind: 'personal_api_key', userId: 'caller', keyId: 'key' } as const +const principal = { + kind: 'personal_api_key', + userId: 'caller', + keyId: 'key', + access: 'write', +} as const const organizationId = 'organization-123' const workspaceId = 'workspace-123' const requestId = 'request-123' diff --git a/apps/sim/app/api/v2/audit-logs/route.test.ts b/apps/sim/app/api/v2/audit-logs/route.test.ts index 2383d574037..89801ae7efd 100644 --- a/apps/sim/app/api/v2/audit-logs/route.test.ts +++ b/apps/sim/app/api/v2/audit-logs/route.test.ts @@ -23,7 +23,12 @@ import { REFILTERED_CURSOR_MESSAGE } from '@/lib/api/cursor-binding' import { GET as listLogs } from '@/app/api/v2/audit-logs/route' const auth = { - principal: { kind: 'personal_api_key' as const, userId: 'admin-1', keyId: 'key-1' }, + principal: { + kind: 'personal_api_key' as const, + userId: 'admin-1', + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', 'user:admin-1'] as const, rateLimitSubscription: null, keyType: 'personal' as const, diff --git a/apps/sim/app/api/v2/billing/logs/route.test.ts b/apps/sim/app/api/v2/billing/logs/route.test.ts index 58c7b694916..0c069f6a078 100644 --- a/apps/sim/app/api/v2/billing/logs/route.test.ts +++ b/apps/sim/app/api/v2/billing/logs/route.test.ts @@ -33,7 +33,12 @@ function ledgerCursor( } const auth = { - principal: { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'key-1' }, + principal: { + kind: 'personal_api_key' as const, + userId: 'user-1', + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', 'user:user-1'] as const, rateLimitSubscription: null, keyType: 'personal' as const, diff --git a/apps/sim/app/api/v2/billing/status/route.test.ts b/apps/sim/app/api/v2/billing/status/route.test.ts index 1144c42668b..554460feea9 100644 --- a/apps/sim/app/api/v2/billing/status/route.test.ts +++ b/apps/sim/app/api/v2/billing/status/route.test.ts @@ -23,7 +23,12 @@ import { WorkspaceApiKeyScopeAuthorizationError } from '@/lib/core/application' import { GET } from '@/app/api/v2/billing/status/route' const auth = { - principal: { kind: 'workspace_api_key' as const, workspaceId: 'workspace-1', keyId: 'key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: 'workspace-1', + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', 'workspace:workspace-1'] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/blocks/route.test.ts b/apps/sim/app/api/v2/blocks/route.test.ts index 232f6daeb89..8e7972d2776 100644 --- a/apps/sim/app/api/v2/blocks/route.test.ts +++ b/apps/sim/app/api/v2/blocks/route.test.ts @@ -24,7 +24,12 @@ import { cursorSortKey, encodeOffsetCursor } from '@/app/api/v2/lib/response' const WORKSPACE_ID = '11111111-2222-4333-8444-555555555555' const auth = { - principal: { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: WORKSPACE_ID, + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/connector-types/route.test.ts b/apps/sim/app/api/v2/connector-types/route.test.ts index 3be63b5c984..be52fc0bf40 100644 --- a/apps/sim/app/api/v2/connector-types/route.test.ts +++ b/apps/sim/app/api/v2/connector-types/route.test.ts @@ -25,7 +25,12 @@ import { GET } from '@/app/api/v2/connector-types/route' const WORKSPACE_ID = '11111111-2222-4333-8444-555555555555' const auth = { - principal: { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: WORKSPACE_ID, + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1'] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/credentials/[credentialId]/route.test.ts b/apps/sim/app/api/v2/credentials/[credentialId]/route.test.ts index b490aae5611..ab46828a7a0 100644 --- a/apps/sim/app/api/v2/credentials/[credentialId]/route.test.ts +++ b/apps/sim/app/api/v2/credentials/[credentialId]/route.test.ts @@ -22,35 +22,31 @@ vi.mock('@/lib/credentials/application/service-account', () => ({ }, })) -import { CredentialProviderOperationError } from '@/lib/credentials/application/crud' +import { + CredentialProviderOperationError, + throwCredentialMutationFailure, +} from '@/lib/credentials/application/crud' +import { WorkloadIdentitySubjectError } from '@/lib/credentials/workload-identity/subject' import { PATCH } from '@/app/api/v2/credentials/[credentialId]/route' -vi.mock('@/lib/credentials/application/crud', async () => { - const { OrchestrationError: BaseError } = await import('@/lib/core/orchestration/types') - class MockCredentialProviderOperationError extends BaseError { - constructor( - message: string, - readonly providerErrorCode: string, - readonly providerUnavailable: boolean - ) { - super('validation', message) - this.name = 'CredentialProviderOperationError' - } - } - return { - CredentialProviderOperationError: MockCredentialProviderOperationError, - updateWorkspaceCredentialUseCase: { - operation: { id: 'credentials.update' }, - execute: mocks.update, - }, - } -}) +vi.mock('@/lib/credentials/application/crud', async (importOriginal) => ({ + ...(await importOriginal()), + updateWorkspaceCredentialUseCase: { + operation: { id: 'credentials.update' }, + execute: mocks.update, + }, +})) const WORKSPACE_ID = '11111111-2222-4333-8444-555555555555' const CREDENTIAL_ID = '7c9e6679-7425-40de-944b-e07fc1f90ae7' const auth = { - principal: { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'key-1' }, + principal: { + kind: 'personal_api_key' as const, + userId: 'user-1', + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', 'user:user-1'] as const, rateLimitSubscription: null, keyType: 'personal' as const, @@ -196,4 +192,29 @@ describe('PATCH /api/v2/credentials/[credentialId]', () => { }, }) }) + + it('reports a reconnect whose subject another credential holds with the create code', async () => { + mocks.update.mockImplementation(() => + throwCredentialMutationFailure({ + success: false, + error: 'Another credential already uses this trust setup.', + errorCode: 'conflict', + keylessRefusal: new WorkloadIdentitySubjectError( + 'Another credential already uses this trust setup.', + 'WORKLOAD_IDENTITY_SUBJECT_IN_USE' + ), + }) + ) + + const response = await PATCH(patchRequest({ serviceAccountJson: '{}' }), context) + + expect(response.status).toBe(409) + await expect(response.json()).resolves.toEqual({ + error: { + code: 'CONFLICT', + message: 'Another credential already uses this trust setup.', + details: { code: 'WORKLOAD_IDENTITY_SUBJECT_IN_USE' }, + }, + }) + }) }) diff --git a/apps/sim/app/api/v2/credentials/[credentialId]/route.ts b/apps/sim/app/api/v2/credentials/[credentialId]/route.ts index aa0d7c2eb97..e6a939ac71c 100644 --- a/apps/sim/app/api/v2/credentials/[credentialId]/route.ts +++ b/apps/sim/app/api/v2/credentials/[credentialId]/route.ts @@ -8,43 +8,19 @@ import { v2ApiKeyAuth, v2RateLimits, } from '@/lib/api/server/routes' -import { - CredentialProviderOperationError, - updateWorkspaceCredentialUseCase, -} from '@/lib/credentials/application/crud' +import { renderV2CredentialProviderError } from '@/lib/credentials/api/route-policies' +import { updateWorkspaceCredentialUseCase } from '@/lib/credentials/application/crud' import { credentialOperations } from '@/lib/credentials/application/operations' import { toV2Credential } from '@/lib/credentials/application/presentation' import { deleteCredentialUseCase } from '@/lib/credentials/application/service-account' -import { v2CaughtOrchestrationError, v2Error } from '@/app/api/v2/lib/response' +import { v2CaughtOrchestrationError } from '@/app/api/v2/lib/response' export const dynamic = 'force-dynamic' export const revalidate = 0 -/** - * Separates a provider rejecting the submitted secret from the provider being - * unreachable while it is verified. - * - * `CredentialProviderOperationError` extends `OrchestrationError('validation')`, - * so the default projection renders both as `400`. That tells a caller whose - * provider is merely down that its secret material is permanently wrong, which - * invites it to revoke a credential that is fine. `503` says the opposite, and - * `v2Error` attaches `Retry-After` from its status table. The outage message is - * deliberately generic — the underlying value is a provider transport failure, - * not anything the caller submitted — while a genuine rejection keeps the - * provider's own code in `details.providerErrorCode` so a client can map it. - */ -function renderCredentialProviderError(error: unknown) { - if (!(error instanceof CredentialProviderOperationError)) return null - return error.providerUnavailable - ? v2Error('SERVICE_UNAVAILABLE', 'Credential provider is temporarily unavailable') - : v2Error('BAD_REQUEST', error.message, { - details: { providerErrorCode: error.providerErrorCode }, - }) -} - const credentialErrorPolicy = createV2ResourceConcealmentPolicy({ notFoundMessage: 'Credential not found', - render: (error) => renderCredentialProviderError(error) ?? v2CaughtOrchestrationError(error), + render: (error) => renderV2CredentialProviderError(error) ?? v2CaughtOrchestrationError(error), }) /** diff --git a/apps/sim/app/api/v2/credentials/route.test.ts b/apps/sim/app/api/v2/credentials/route.test.ts index 24fe6683d16..3e3c3ec86b7 100644 --- a/apps/sim/app/api/v2/credentials/route.test.ts +++ b/apps/sim/app/api/v2/credentials/route.test.ts @@ -31,6 +31,7 @@ vi.mock('@/lib/credentials/application/service-account', () => ({ })) import { REFILTERED_CURSOR_MESSAGE } from '@/lib/api/cursor-binding' +import { KeylessCredentialRefusedError } from '@/lib/credentials/workload-identity/subject' import { GET, POST } from '@/app/api/v2/credentials/route' const WORKSPACE_ID = '11111111-2222-4333-8444-555555555555' @@ -39,6 +40,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, @@ -139,7 +141,7 @@ describe('POST /api/v2/credentials', () => { beforeEach(() => { v2RouteMocks.authenticate.mockResolvedValue({ ...auth, - principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' }, + principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1', access: 'write' }, keyType: 'personal', }) v2RouteMocks.preauthRate.mockResolvedValue(V2_PREAUTH_RATE_LIMIT_ALLOWED) @@ -184,7 +186,7 @@ describe('POST /api/v2/credentials', () => { expect(JSON.stringify(body)).not.toContain('client-secret') expect(JSON.stringify(body)).not.toContain('must-not-leak') expect(mocks.create).toHaveBeenCalledWith({ - principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' }, + principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1', access: 'write' }, input: { workspaceId: WORKSPACE_ID, providerId: 'zoom-service-account', @@ -236,7 +238,7 @@ describe('POST /api/v2/credentials', () => { expect(response.status).toBe(201) expect(mocks.create).toHaveBeenCalledWith({ - principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' }, + principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1', access: 'write' }, input: expect.objectContaining({ providerId: 'oci-api-key-service-account', tenancyOcid: 'ocid1.tenancy.oc1..tenant', @@ -420,4 +422,59 @@ describe('POST /api/v2/credentials', () => { }) expect(mocks.create).not.toHaveBeenCalled() }) + function createKeyless(providerId: string, serviceAccountJson: object) { + return POST( + new NextRequest('http://localhost:3000/api/v2/credentials', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + workspaceId: WORKSPACE_ID, + type: 'service_account', + providerId, + credentials: JSON.stringify({ serviceAccountJson: JSON.stringify(serviceAccountJson) }), + }), + }) + ) + } + + it('accepts a keyless Snowflake configuration in place of the token fields', async () => { + const response = await createKeyless('snowflake-service-account', { + authType: 'workload_identity', + host: 'myorg-myaccount.snowflakecomputing.com', + subject: 'sim:workspace:ws:credential:abcdef0123456789', + subjectProof: 'proof', + }) + + expect(response.status).toBe(201) + expect(mocks.create).toHaveBeenCalledOnce() + }) + + it.each([ + ['WORKLOAD_IDENTITY_NOT_CONFIGURED', 400, 'BAD_REQUEST'], + ['WORKLOAD_IDENTITY_PROOF_EXPIRED', 400, 'BAD_REQUEST'], + ['WORKLOAD_IDENTITY_PROOF_INVALID', 400, 'BAD_REQUEST'], + ['WORKLOAD_IDENTITY_SUBJECT_IN_USE', 409, 'CONFLICT'], + ['WORKLOAD_IDENTITY_SUBJECT_UNVERIFIABLE', 409, 'CONFLICT'], + ] as const)( + 'names a refused keyless credential %s in details.code', + async (detailCode, status, code) => { + mocks.create.mockRejectedValue( + new KeylessCredentialRefusedError('Start the keyless connection again.', detailCode) + ) + + const response = await createKeyless('aws-service-account', { + authType: 'oidc_role', + roleArn: 'arn:aws:iam::123456789012:role/Sim', + subject: 'sim:workspace:ws:credential:abcdef0123456789', + subjectProof: 'proof', + }) + + expect(response.status).toBe(status) + expect((await response.json()).error).toEqual({ + code, + message: 'Start the keyless connection again.', + details: { code: detailCode }, + }) + } + ) }) diff --git a/apps/sim/app/api/v2/credentials/route.ts b/apps/sim/app/api/v2/credentials/route.ts index 3f925f6db9e..7caa16ea94f 100644 --- a/apps/sim/app/api/v2/credentials/route.ts +++ b/apps/sim/app/api/v2/credentials/route.ts @@ -9,17 +9,23 @@ import { v2ApiKeyAuth, v2RateLimits, } from '@/lib/api/server/routes' +import { renderV2CredentialProviderError } from '@/lib/credentials/api/route-policies' import { listWorkspaceCredentials } from '@/lib/credentials/application/list-workspace-credentials' import { credentialOperations } from '@/lib/credentials/application/operations' import { toV2Credential } from '@/lib/credentials/application/presentation' import { createServiceAccountCredentialUseCase } from '@/lib/credentials/application/service-account' -import { readSortedCursor, writeSortedCursor } from '@/app/api/v2/lib/response' +import { + readSortedCursor, + v2CaughtOrchestrationError, + writeSortedCursor, +} from '@/app/api/v2/lib/response' export const dynamic = 'force-dynamic' export const revalidate = 0 const credentialWorkspaceErrorPolicy = createV2ResourceConcealmentPolicy({ notFoundMessage: 'Workspace not found', + render: (error) => renderV2CredentialProviderError(error) ?? v2CaughtOrchestrationError(error), }) /** Every param that changes which credentials, in which order, this list returns. */ diff --git a/apps/sim/app/api/v2/credentials/workload-identity/subjects/route.test.ts b/apps/sim/app/api/v2/credentials/workload-identity/subjects/route.test.ts new file mode 100644 index 00000000000..00f6c61d333 --- /dev/null +++ b/apps/sim/app/api/v2/credentials/workload-identity/subjects/route.test.ts @@ -0,0 +1,216 @@ +/** + * @vitest-environment node + */ +import { generateKeyPairSync } from 'node:crypto' +import type { Principal } from '@sim/auth/principal' +import { + V2_OPERATION_RATE_LIMIT_ALLOWED, + V2_PREAUTH_RATE_LIMIT_ALLOWED, + v2ApiKeyAuthModuleMock, + v2RateLimiterModuleMock, + v2RouteMocks, +} from '@sim/testing' +import { + createPersonalApiKeyPrincipal, + createWorkspaceApiKeyPrincipal, +} from '@sim/testing/factories/principal.factory' +import { workspaceAuthzMock, workspaceAuthzMockFns } from '@sim/testing/mocks/workspace-authz.mock' +import { + workspaceContextMock, + workspaceContextMockFns, +} from '@sim/testing/mocks/workspace-context.mock' +import { NextRequest } from 'next/server' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/api/server/routes/v2-api-key-auth', () => v2ApiKeyAuthModuleMock) +vi.mock('@/lib/core/rate-limiter', () => v2RateLimiterModuleMock) +vi.mock('@/lib/workspaces/application/context', () => workspaceContextMock) +vi.mock('@sim/platform-authz/workspace', () => workspaceAuthzMock) + +import { env } from '@/lib/core/config/env' +import { verifyWorkloadIdentitySubject } from '@/lib/credentials/workload-identity/subject' +import { POST } from '@/app/api/v2/credentials/workload-identity/subjects/route' + +const WORKSPACE_ID = '11111111-2222-4333-8444-555555555555' +const SIGNING_KEY = generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey.export({ + type: 'pkcs8', + format: 'pem', +}) + +function authenticateAs(principal: Principal) { + v2RouteMocks.authenticate.mockResolvedValue({ + principal, + rateLimitSubjectIds: ['subject-1'], + rateLimitSubscription: null, + keyType: principal.kind === 'workspace_api_key' ? 'workspace' : 'personal', + }) +} + +function issue(body: Record) { + return POST( + new NextRequest('http://localhost:3000/api/v2/credentials/workload-identity/subjects', { + method: 'POST', + body: JSON.stringify(body), + headers: { 'content-type': 'application/json', 'x-api-key': 'sk-sim-test' }, + }) + ) +} + +describe('POST /api/v2/credentials/workload-identity/subjects', () => { + beforeEach(() => { + Reflect.set(env, 'WORKLOAD_IDENTITY_SIGNING_KEY', SIGNING_KEY) + Reflect.set(env, 'WORKLOAD_IDENTITY_ISSUER', 'https://sim.example.com/oidc/workload') + v2RouteMocks.preauthRate.mockResolvedValue(V2_PREAUTH_RATE_LIMIT_ALLOWED) + v2RouteMocks.operationRate.mockResolvedValue(V2_OPERATION_RATE_LIMIT_ALLOWED) + workspaceContextMockFns.mockLoadActiveWorkspaceApplicationContext.mockResolvedValue({ + workspaceId: WORKSPACE_ID, + workspaceOrganizationId: null, + allowPersonalApiKeys: true, + billedAccountUserId: 'user-1', + }) + workspaceAuthzMockFns.mockResolveEffectiveWorkspacePermission.mockResolvedValue('write') + authenticateAs(createPersonalApiKeyPrincipal({ userId: 'user-1' })) + }) + + afterEach(() => { + Reflect.set(env, 'WORKLOAD_IDENTITY_SIGNING_KEY', undefined) + Reflect.set(env, 'WORKLOAD_IDENTITY_ISSUER', undefined) + }) + + it('issues a personal key a proof only that key’s user can create the credential with', async () => { + const response = await issue({ workspaceId: WORKSPACE_ID, providerId: 'aws-service-account' }) + + expect(response.status).toBe(200) + const { data } = await response.json() + expect(data).toEqual({ + issuer: 'https://sim.example.com/oidc/workload', + audience: 'sts.amazonaws.com', + subject: expect.stringMatching(`^sim:workspace:${WORKSPACE_ID}:credential:`), + subjectProof: expect.any(String), + expiresAt: expect.any(String), + keylessConfig: { + authType: 'oidc_role', + subject: data.subject, + subjectProof: data.subjectProof, + }, + requiredConfigFields: ['roleArn'], + optionalConfigFields: ['stsRegion', 'sessionDurationSeconds'], + }) + + const owner = { workspaceId: WORKSPACE_ID } + expect(() => + verifyWorkloadIdentitySubject('aws', owner, 'user-1', data.subject, data.subjectProof) + ).not.toThrow() + expect(() => + verifyWorkloadIdentitySubject('aws', owner, 'user-2', data.subject, data.subjectProof) + ).toThrow(expect.objectContaining({ detailCode: 'WORKLOAD_IDENTITY_PROOF_INVALID' })) + expect(() => + verifyWorkloadIdentitySubject('databricks', owner, 'user-1', data.subject, data.subjectProof) + ).toThrow(expect.objectContaining({ detailCode: 'WORKLOAD_IDENTITY_PROOF_INVALID' })) + }) + + it('leaves the audience out for Google Cloud, whose audience is per credential', async () => { + const response = await issue({ + workspaceId: WORKSPACE_ID, + providerId: 'google-service-account', + }) + + const { data } = await response.json() + expect(data).not.toHaveProperty('audience') + expect(data.requiredConfigFields).toEqual([ + 'projectNumber', + 'poolId', + 'providerId', + 'serviceAccountEmail', + ]) + }) + + it('refuses a workspace API key, whose subject would belong to no person', async () => { + authenticateAs(createWorkspaceApiKeyPrincipal({ workspaceId: WORKSPACE_ID })) + + const response = await issue({ workspaceId: WORKSPACE_ID, providerId: 'aws-service-account' }) + + expect(response.status).toBe(403) + expect((await response.json()).error).toMatchObject({ + code: 'FORBIDDEN', + details: { code: 'WORKSPACE_KEY_OPERATION_NOT_PERMITTED' }, + }) + }) + + it('refuses an OAuth token without the api:write scope', async () => { + authenticateAs({ + kind: 'oauth_access_token', + userId: 'user-1', + clientId: 'client-1', + tokenId: 'token-1', + scopes: ['api:read'], + expiresAt: new Date('2099-01-01T00:00:00.000Z'), + }) + + const response = await issue({ workspaceId: WORKSPACE_ID, providerId: 'aws-service-account' }) + + expect(response.status).toBe(403) + expect((await response.json()).error).toMatchObject({ + code: 'FORBIDDEN', + details: { code: 'INSUFFICIENT_SCOPE' }, + }) + }) + + it('rejects a provider with no keyless mode before issuing anything', async () => { + const response = await issue({ workspaceId: WORKSPACE_ID, providerId: 'zoom-service-account' }) + + expect(response.status).toBe(400) + expect((await response.json()).error.code).toBe('BAD_REQUEST') + expect(workspaceContextMockFns.mockLoadActiveWorkspaceApplicationContext).not.toHaveBeenCalled() + }) + + it('names the missing deployment support with a stable code', async () => { + Reflect.set(env, 'WORKLOAD_IDENTITY_SIGNING_KEY', undefined) + + const response = await issue({ workspaceId: WORKSPACE_ID, providerId: 'aws-service-account' }) + + expect(response.status).toBe(400) + expect((await response.json()).error).toMatchObject({ + code: 'BAD_REQUEST', + message: + 'Keyless connections are not available on this deployment. Use a key- or token-based credential.', + details: { code: 'WORKLOAD_IDENTITY_NOT_CONFIGURED' }, + }) + }) + + it.each([ + [ + 'the caller cannot reach', + () => workspaceAuthzMockFns.mockResolveEffectiveWorkspacePermission.mockResolvedValue(null), + ], + [ + 'does not exist', + () => + workspaceContextMockFns.mockLoadActiveWorkspaceApplicationContext.mockResolvedValue(null), + ], + ])('answers a workspace that %s as not found', async (_label, arrange) => { + arrange() + + const response = await issue({ workspaceId: WORKSPACE_ID, providerId: 'aws-service-account' }) + + expect(response.status).toBe(404) + expect((await response.json()).error).toEqual({ + code: 'NOT_FOUND', + message: 'Workspace not found', + }) + }) + + it('refuses a caller over that bucket before issuing a subject', async () => { + v2RouteMocks.operationRate.mockResolvedValue({ + allowed: false, + remaining: 0, + resetAt: new Date(Date.now() + 30_000), + }) + + const response = await issue({ workspaceId: WORKSPACE_ID, providerId: 'aws-service-account' }) + + expect(response.status).toBe(429) + expect((await response.json()).error.code).toBe('RATE_LIMITED') + expect(workspaceContextMockFns.mockLoadActiveWorkspaceApplicationContext).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/app/api/v2/credentials/workload-identity/subjects/route.ts b/apps/sim/app/api/v2/credentials/workload-identity/subjects/route.ts new file mode 100644 index 00000000000..6314776157d --- /dev/null +++ b/apps/sim/app/api/v2/credentials/workload-identity/subjects/route.ts @@ -0,0 +1,62 @@ +import { v2CreateCredentialWorkloadIdentitySubjectContract } from '@/lib/api/contracts/v2/credentials' +import { + createV2ResourceConcealmentPolicy, + defineV2JsonRoute, + v2ApiKeyAuth, + v2RateLimits, +} from '@/lib/api/server/routes' +import { renderV2CredentialProviderError } from '@/lib/credentials/api/route-policies' +import { credentialOperations } from '@/lib/credentials/application/operations' +import { prepareWorkloadIdentity } from '@/lib/credentials/application/prepare-workload-identity' +import { + WORKLOAD_IDENTITY_KEYLESS_CONFIGS, + workloadIdentityProviderFor, +} from '@/lib/credentials/workload-identity/audiences' +import { WorkloadIdentityNotConfiguredError } from '@/lib/credentials/workload-identity/subject' +import { v2CaughtOrchestrationError } from '@/app/api/v2/lib/response' + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +const workloadIdentitySubjectErrorPolicy = createV2ResourceConcealmentPolicy({ + notFoundMessage: 'Workspace not found', + render: (error) => renderV2CredentialProviderError(error) ?? v2CaughtOrchestrationError(error), +}) + +/** + * POST /api/v2/credentials/workload-identity/subjects — Issue the subject a + * cloud trust configuration pins, and the proof that attaches it to a new + * keyless credential. A deployment that cannot sign is refused exactly as + * Create service-account credential refuses a keyless configuration there. + */ +export const POST = defineV2JsonRoute({ + contract: v2CreateCredentialWorkloadIdentitySubjectContract, + auth: v2ApiKeyAuth, + operation: credentialOperations.prepareWorkloadIdentity, + rateLimit: v2RateLimits.publicApi, + errorPolicy: workloadIdentitySubjectErrorPolicy, + mapInput: ({ body }) => ({ + workspaceId: body.workspaceId, + provider: workloadIdentityProviderFor(body.providerId), + }), + useCase: prepareWorkloadIdentity, + present: (result, { body }) => { + if (!result.available) { + throw new WorkloadIdentityNotConfiguredError() + } + const { authType, requiredFields, optionalFields } = + WORKLOAD_IDENTITY_KEYLESS_CONFIGS[workloadIdentityProviderFor(body.providerId)] + return { + data: { + issuer: result.issuer, + ...(result.audience ? { audience: result.audience } : {}), + subject: result.subject, + subjectProof: result.subjectProof, + expiresAt: result.expiresAt, + keylessConfig: { authType, subject: result.subject, subjectProof: result.subjectProof }, + requiredConfigFields: [...requiredFields], + optionalConfigFields: [...optionalFields], + }, + } + }, +}) diff --git a/apps/sim/app/api/v2/files/[fileId]/content/route.test.ts b/apps/sim/app/api/v2/files/[fileId]/content/route.test.ts index bbdf598dfac..b5f2415c13d 100644 --- a/apps/sim/app/api/v2/files/[fileId]/content/route.test.ts +++ b/apps/sim/app/api/v2/files/[fileId]/content/route.test.ts @@ -51,6 +51,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts b/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts index 80a3a0cb24c..3a102dd1374 100644 --- a/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts +++ b/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts @@ -34,7 +34,12 @@ const WORKSPACE_ID = 'workspace-1' const FILE_ID = 'wf_1' const context = { params: Promise.resolve({ fileId: FILE_ID }) } const auth = { - principal: { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: WORKSPACE_ID, + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/files/[fileId]/route.test.ts b/apps/sim/app/api/v2/files/[fileId]/route.test.ts index 16a98c257ea..e5679223e51 100644 --- a/apps/sim/app/api/v2/files/[fileId]/route.test.ts +++ b/apps/sim/app/api/v2/files/[fileId]/route.test.ts @@ -57,6 +57,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/files/[fileId]/text/route.test.ts b/apps/sim/app/api/v2/files/[fileId]/text/route.test.ts index 796025d11ea..2f873258124 100644 --- a/apps/sim/app/api/v2/files/[fileId]/text/route.test.ts +++ b/apps/sim/app/api/v2/files/[fileId]/text/route.test.ts @@ -31,7 +31,12 @@ const FILE_ID = 'wf_doc' const context = { params: Promise.resolve({ fileId: FILE_ID }) } const AUTH = { - principal: { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: WORKSPACE_ID, + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/files/[fileId]/unzip/route.test.ts b/apps/sim/app/api/v2/files/[fileId]/unzip/route.test.ts index b4cefda7c95..7429a3fa486 100644 --- a/apps/sim/app/api/v2/files/[fileId]/unzip/route.test.ts +++ b/apps/sim/app/api/v2/files/[fileId]/unzip/route.test.ts @@ -31,7 +31,12 @@ const FILE_ID = 'wf_archive' const context = { params: Promise.resolve({ fileId: FILE_ID }) } const AUTH = { - principal: { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: WORKSPACE_ID, + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/files/[fileId]/versions/[version]/revert/route.test.ts b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/revert/route.test.ts index c6a62c7bcb1..4386becfa5c 100644 --- a/apps/sim/app/api/v2/files/[fileId]/versions/[version]/revert/route.test.ts +++ b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/revert/route.test.ts @@ -37,6 +37,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/files/route.test.ts b/apps/sim/app/api/v2/files/route.test.ts index e94b63a9cda..7b7bdc519c1 100644 --- a/apps/sim/app/api/v2/files/route.test.ts +++ b/apps/sim/app/api/v2/files/route.test.ts @@ -43,6 +43,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/files/uploads/[uploadId]/route.test.ts b/apps/sim/app/api/v2/files/uploads/[uploadId]/route.test.ts index 7a90a0f3ece..1fe7ef4ee48 100644 --- a/apps/sim/app/api/v2/files/uploads/[uploadId]/route.test.ts +++ b/apps/sim/app/api/v2/files/uploads/[uploadId]/route.test.ts @@ -51,6 +51,7 @@ const AUTH = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/files/uploads/route.test.ts b/apps/sim/app/api/v2/files/uploads/route.test.ts index 5801da94143..e6302263b9e 100644 --- a/apps/sim/app/api/v2/files/uploads/route.test.ts +++ b/apps/sim/app/api/v2/files/uploads/route.test.ts @@ -42,6 +42,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/documents/[documentId]/chunks/route.test.ts b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/documents/[documentId]/chunks/route.test.ts index 11dcbacec82..f0cd8852b37 100644 --- a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/documents/[documentId]/chunks/route.test.ts +++ b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/documents/[documentId]/chunks/route.test.ts @@ -36,7 +36,12 @@ import { const WORKSPACE_ID = 'workspace-1' const OTHER_WORKSPACE_ID = 'workspace-2' -const PRINCIPAL = { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' } as const +const PRINCIPAL = { + kind: 'personal_api_key', + userId: 'user-1', + keyId: 'key-1', + access: 'write', +} as const const context = { params: Promise.resolve({ knowledgeBaseId: 'kb-1', documentId: 'doc-1' }) } const siblingContext = { params: Promise.resolve({ knowledgeBaseId: 'kb-1', documentId: 'doc-2' }) } diff --git a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/documents/from-workspace-files/route.test.ts b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/documents/from-workspace-files/route.test.ts index 014eb7198dc..8312cea1f4a 100644 --- a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/documents/from-workspace-files/route.test.ts +++ b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/documents/from-workspace-files/route.test.ts @@ -40,7 +40,7 @@ beforeEach(() => { v2RouteMocks.preauthRate.mockResolvedValue(V2_PREAUTH_RATE_LIMIT_ALLOWED) v2RouteMocks.operationRate.mockResolvedValue(V2_OPERATION_RATE_LIMIT_ALLOWED) v2RouteMocks.authenticate.mockResolvedValue({ - principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' }, + principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1', access: 'write' }, rateLimitSubjectIds: ['api-key:key-1'], rateLimitSubscription: null, keyType: 'personal', diff --git a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/export/route.test.ts b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/export/route.test.ts index 27d9a86fed5..0bba6dfcd00 100644 --- a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/export/route.test.ts +++ b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/export/route.test.ts @@ -41,7 +41,12 @@ const FILE_NAME = 'Support docs.simkb.zip' const context = { params: Promise.resolve({ knowledgeBaseId: KNOWLEDGE_BASE_ID }) } const AUTH = { - principal: { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: WORKSPACE_ID, + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/restore/route.test.ts b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/restore/route.test.ts index ae0692714cc..203d6b1a677 100644 --- a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/restore/route.test.ts +++ b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/restore/route.test.ts @@ -59,7 +59,7 @@ beforeEach(() => { v2RouteMocks.preauthRate.mockResolvedValue(V2_PREAUTH_RATE_LIMIT_ALLOWED) v2RouteMocks.operationRate.mockResolvedValue(V2_OPERATION_RATE_LIMIT_ALLOWED) v2RouteMocks.authenticate.mockResolvedValue({ - principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' }, + principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1', access: 'write' }, rateLimitSubjectIds: ['api-key:key-1'], rateLimitSubscription: null, keyType: 'personal', diff --git a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/tags/[tagId]/route.test.ts b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/tags/[tagId]/route.test.ts index 07c388db409..2495df86e64 100644 --- a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/tags/[tagId]/route.test.ts +++ b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/tags/[tagId]/route.test.ts @@ -31,7 +31,7 @@ beforeEach(() => { v2RouteMocks.preauthRate.mockResolvedValue(V2_PREAUTH_RATE_LIMIT_ALLOWED) v2RouteMocks.operationRate.mockResolvedValue(V2_OPERATION_RATE_LIMIT_ALLOWED) v2RouteMocks.authenticate.mockResolvedValue({ - principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' }, + principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1', access: 'write' }, rateLimitSubjectIds: ['api-key:key-1'], rateLimitSubscription: null, keyType: 'personal', diff --git a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/tags/route.test.ts b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/tags/route.test.ts index 25b0d7b7b20..5b68bd4be2a 100644 --- a/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/tags/route.test.ts +++ b/apps/sim/app/api/v2/knowledge/[knowledgeBaseId]/tags/route.test.ts @@ -54,7 +54,7 @@ describe('DELETE /api/v2/knowledge/[knowledgeBaseId]/tags', () => { v2RouteMocks.preauthRate.mockResolvedValue(V2_PREAUTH_RATE_LIMIT_ALLOWED) v2RouteMocks.operationRate.mockResolvedValue(V2_OPERATION_RATE_LIMIT_ALLOWED) v2RouteMocks.authenticate.mockResolvedValue({ - principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' }, + principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1', access: 'write' }, rateLimitSubjectIds: ['api-key:key-1'], rateLimitSubscription: null, keyType: 'personal', diff --git a/apps/sim/app/api/v2/knowledge/search/route.test.ts b/apps/sim/app/api/v2/knowledge/search/route.test.ts index 141b92315cb..f6e9eb01ddf 100644 --- a/apps/sim/app/api/v2/knowledge/search/route.test.ts +++ b/apps/sim/app/api/v2/knowledge/search/route.test.ts @@ -23,7 +23,12 @@ import { POST, V2_KNOWLEDGE_SEARCH_MAX_BODY_BYTES } from '@/app/api/v2/knowledge const mockSearch = knowledgeSearchUseCaseMockFns.mockSearchKnowledgeExecute const WORKSPACE_ID = 'workspace-1' -const PRINCIPAL = { kind: 'workspace_api_key', workspaceId: WORKSPACE_ID, keyId: 'key-1' } as const +const PRINCIPAL = { + kind: 'workspace_api_key', + workspaceId: WORKSPACE_ID, + keyId: 'key-1', + access: 'write', +} as const function buildRequest(body: string, headers: Record = {}) { return new NextRequest('http://localhost/api/v2/knowledge/search', { method: 'POST', diff --git a/apps/sim/app/api/v2/logs/[runId]/route.test.ts b/apps/sim/app/api/v2/logs/[runId]/route.test.ts index bad5dd64acc..e2a5123e3b8 100644 --- a/apps/sim/app/api/v2/logs/[runId]/route.test.ts +++ b/apps/sim/app/api/v2/logs/[runId]/route.test.ts @@ -27,6 +27,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: 'workspace-1', keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', 'workspace:workspace-1'] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/logs/route.test.ts b/apps/sim/app/api/v2/logs/route.test.ts index a941da995fc..99afcf8b4bb 100644 --- a/apps/sim/app/api/v2/logs/route.test.ts +++ b/apps/sim/app/api/v2/logs/route.test.ts @@ -30,6 +30,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/logs/stats/route.test.ts b/apps/sim/app/api/v2/logs/stats/route.test.ts index f47a679ceb0..d0bfc25bdaa 100644 --- a/apps/sim/app/api/v2/logs/stats/route.test.ts +++ b/apps/sim/app/api/v2/logs/stats/route.test.ts @@ -21,7 +21,12 @@ import { GET } from '@/app/api/v2/logs/stats/route' const WORKSPACE_ID = '6fc7631d-88cd-46f8-9f0a-d4764daef7f8' const auth = { - principal: { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: WORKSPACE_ID, + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/meta/route.ts b/apps/sim/app/api/v2/meta/route.ts index 718b56305ea..39231e06382 100644 --- a/apps/sim/app/api/v2/meta/route.ts +++ b/apps/sim/app/api/v2/meta/route.ts @@ -24,7 +24,7 @@ export const GET = defineV2JsonRoute({ return { keyType: credential.keyType, expiresAt: credential.keyExpiresAt } }, useCase: readV2ApiCapabilities, - present: ({ v2Enabled, keyType, expiresAt }) => ({ - data: { v2Enabled, keyType, expiresAt: expiresAt?.toISOString() ?? null }, + present: ({ v2Enabled, keyType, expiresAt, scopes }) => ({ + data: { v2Enabled, keyType, expiresAt: expiresAt?.toISOString() ?? null, scopes }, }), }) diff --git a/apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions/route.ts b/apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions/route.ts new file mode 100644 index 00000000000..392fad7ba08 --- /dev/null +++ b/apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions/route.ts @@ -0,0 +1,33 @@ +import { + v2GetSearchSourcePermissionsContract, + v2ReplaceSearchSourcePermissionsContract, +} from '@/lib/api/contracts/v2/organizations' +import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { v2OrganizationErrorPolicy } from '@/lib/api/server/routes/organizations' +import { searchSourcePermissionOperations } from '@/lib/knowledge/application/operations' +import { + readSearchSourcePermissions, + replaceSearchSourcePermissions, +} from '@/lib/knowledge/application/source-permissions' + +export const GET = defineV2JsonRoute({ + contract: v2GetSearchSourcePermissionsContract, + operation: searchSourcePermissionOperations.read, + auth: v2ApiKeyAuth, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2OrganizationErrorPolicy, + mapInput: ({ params }) => params, + useCase: readSearchSourcePermissions, + present: (data) => ({ data }), +}) + +export const PUT = defineV2JsonRoute({ + contract: v2ReplaceSearchSourcePermissionsContract, + operation: searchSourcePermissionOperations.replace, + auth: v2ApiKeyAuth, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2OrganizationErrorPolicy, + mapInput: ({ params, body }) => ({ ...params, ...body }), + useCase: replaceSearchSourcePermissions, + present: (data) => ({ data }), +}) diff --git a/apps/sim/app/api/v2/tables/[tableId]/cancel-runs/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/cancel-runs/route.test.ts index b5e744ad025..03375299a63 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/cancel-runs/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/cancel-runs/route.test.ts @@ -27,6 +27,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/columns/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/columns/route.test.ts index e73299a888d..72439197bff 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/columns/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/columns/route.test.ts @@ -29,6 +29,7 @@ const principal = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const auth = { principal, diff --git a/apps/sim/app/api/v2/tables/[tableId]/dispatches/[dispatchId]/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/dispatches/[dispatchId]/route.test.ts index c858a26e029..a3fc4268677 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/dispatches/[dispatchId]/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/dispatches/[dispatchId]/route.test.ts @@ -24,6 +24,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/dispatches/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/dispatches/route.test.ts index c774aa3922c..84477e92b95 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/dispatches/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/dispatches/route.test.ts @@ -26,6 +26,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/groups/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/groups/route.test.ts index dab71e8084b..801a4df0d21 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/groups/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/groups/route.test.ts @@ -31,6 +31,7 @@ const principal = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const auth = { principal, diff --git a/apps/sim/app/api/v2/tables/[tableId]/query/count/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/query/count/route.test.ts index f54a7345b85..ae1fa00df8c 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/query/count/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/query/count/route.test.ts @@ -26,6 +26,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/query/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/query/route.test.ts index ad11d57cc83..14de92ad88e 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/query/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/query/route.test.ts @@ -33,6 +33,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/restore/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/restore/route.test.ts index add64eb7c14..92f7d1508f7 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/restore/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/restore/route.test.ts @@ -32,6 +32,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/enrichment/[groupId]/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/enrichment/[groupId]/route.test.ts index 800723672fd..7850fb2ffbf 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/enrichment/[groupId]/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/enrichment/[groupId]/route.test.ts @@ -32,6 +32,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/route.test.ts index 235cb3d123f..64408eb1f35 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/route.test.ts @@ -26,6 +26,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/rows/bulk-update/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/rows/bulk-update/route.test.ts index 4bebca009aa..fee099c329f 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/rows/bulk-update/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/rows/bulk-update/route.test.ts @@ -25,6 +25,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/rows/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/rows/route.test.ts index 1ec1ab6cb88..80838bdf17b 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/rows/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/rows/route.test.ts @@ -37,6 +37,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/rows/search/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/rows/search/route.test.ts index 57c0b572318..ed18bb14906 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/rows/search/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/rows/search/route.test.ts @@ -25,6 +25,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/rows/upsert/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/rows/upsert/route.test.ts index 387f3f78955..03e2bbee2b3 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/rows/upsert/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/rows/upsert/route.test.ts @@ -25,6 +25,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/[tableId]/views/[viewId]/route.test.ts b/apps/sim/app/api/v2/tables/[tableId]/views/[viewId]/route.test.ts index a32f08f7aa6..5c481ca230c 100644 --- a/apps/sim/app/api/v2/tables/[tableId]/views/[viewId]/route.test.ts +++ b/apps/sim/app/api/v2/tables/[tableId]/views/[viewId]/route.test.ts @@ -33,6 +33,7 @@ const principal = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const auth = { principal, diff --git a/apps/sim/app/api/v2/tables/folders/restore/route.test.ts b/apps/sim/app/api/v2/tables/folders/restore/route.test.ts index ff949051394..fc419b7ba6d 100644 --- a/apps/sim/app/api/v2/tables/folders/restore/route.test.ts +++ b/apps/sim/app/api/v2/tables/folders/restore/route.test.ts @@ -26,6 +26,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/move/route.test.ts b/apps/sim/app/api/v2/tables/move/route.test.ts index 68c40fcd4bb..b78232d2e40 100644 --- a/apps/sim/app/api/v2/tables/move/route.test.ts +++ b/apps/sim/app/api/v2/tables/move/route.test.ts @@ -25,6 +25,7 @@ const PRINCIPAL = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const AUTH = { principal: PRINCIPAL, diff --git a/apps/sim/app/api/v2/tables/route.test.ts b/apps/sim/app/api/v2/tables/route.test.ts index 411ca66e68b..5aac8ac7985 100644 --- a/apps/sim/app/api/v2/tables/route.test.ts +++ b/apps/sim/app/api/v2/tables/route.test.ts @@ -34,6 +34,7 @@ const principal = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, } const auth = { principal, diff --git a/apps/sim/app/api/v2/tools/[toolId]/execute/route.test.ts b/apps/sim/app/api/v2/tools/[toolId]/execute/route.test.ts index 60babfe6faf..f3cebdbe75d 100644 --- a/apps/sim/app/api/v2/tools/[toolId]/execute/route.test.ts +++ b/apps/sim/app/api/v2/tools/[toolId]/execute/route.test.ts @@ -23,7 +23,12 @@ import { POST } from '@/app/api/v2/tools/[toolId]/execute/route' const WORKSPACE_ID = '11111111-2222-4333-8444-555555555555' const auth = { - principal: { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'key-1' }, + principal: { + kind: 'personal_api_key' as const, + userId: 'user-1', + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1'] as const, rateLimitSubscription: null, keyType: 'personal' as const, diff --git a/apps/sim/app/api/v2/tools/route.test.ts b/apps/sim/app/api/v2/tools/route.test.ts index 5a788c2fc30..c9e867dc61f 100644 --- a/apps/sim/app/api/v2/tools/route.test.ts +++ b/apps/sim/app/api/v2/tools/route.test.ts @@ -24,7 +24,12 @@ import { GET } from '@/app/api/v2/tools/route' const WORKSPACE_ID = '11111111-2222-4333-8444-555555555555' const auth = { - principal: { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'key-1' }, + principal: { + kind: 'personal_api_key' as const, + userId: 'user-1', + keyId: 'key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:key-1'] as const, rateLimitSubscription: null, keyType: 'personal' as const, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/duplicate/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/duplicate/route.test.ts index af19ebb7de5..50516ca9ad9 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/duplicate/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/duplicate/route.test.ts @@ -24,7 +24,12 @@ import { POST } from '@/app/api/v2/workflows/[workflowId]/duplicate/route' const WORKFLOW_ID = 'workflow-1' const auth = { - principal: { kind: 'workspace_api_key' as const, workspaceId: 'workspace-1', keyId: 'ws-key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: 'workspace-1', + keyId: 'ws-key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:ws-key-1'] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/execute/route.ts b/apps/sim/app/api/v2/workflows/[workflowId]/execute/route.ts index 24fbbcd3417..a97fe7c4d0a 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/execute/route.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/execute/route.ts @@ -40,7 +40,10 @@ import { executeManualWorkflowFromBlockOperation, executeManualWorkflowOperation, } from '@/lib/workflows/application/execute-manual-workflow' -import { executeWorkflowOperation } from '@/lib/workflows/application/execute-workflow' +import { + executeWorkflowOperation, + executeWorkflowVersionOperation, +} from '@/lib/workflows/application/execute-workflow' import { workflowOperations } from '@/lib/workflows/application/operations' import { type ExecuteWorkflowServiceFailure, @@ -316,6 +319,8 @@ type V2ExecuteWorkflowRouteContext = { * * - Auth: `X-API-Key` (personal/workspace) or the anonymous public-API path for * workflows deployed with `isPublicApi` (actor = owner; sync/stream only). + * - `run: { source: 'deployment', version }` runs that deployment version under + * the write-role `workflows.execute_version` operation; keyed callers only. * - `async: true` (body flag — v2 has no mode headers) → 202 * `{ data: { runId, statusUrl } }`; poll the v2 runs resource. * - `stream: true` → SSE passthrough (no `{data}` envelope on event frames). @@ -422,6 +427,7 @@ export const POST = withRouteHandler( if (!parsed.success) return parsed.response const body = parsed.data.body const manualRun = body.run?.source === 'manual' ? body.run : undefined + const pinnedVersion = body.run?.source === 'deployment' ? body.run.version : undefined if (manualRun && !apiKeyPrincipal) { return v2Error( @@ -439,6 +445,9 @@ export const POST = withRouteHandler( ) { return v2Error('BAD_REQUEST', 'input and run.entry.useMockPayload cannot be combined') } + if (pinnedVersion !== undefined && isPublicApiAccess) { + return v2Error('BAD_REQUEST', 'run.version requires an OAuth access token or API key') + } const detachable = body.waitTimeoutSeconds !== undefined if (detachable && (body.async || body.stream)) { @@ -544,6 +553,17 @@ export const POST = withRouteHandler( }, request: req, }) + } else if (pinnedVersion !== undefined) { + result = await executeWorkflowVersionOperation.execute({ + principal: apiKeyPrincipal, + input: { + ...commonInput, + input: body.input ?? {}, + mode: body.async ? 'async' : body.stream ? 'stream' : syncMode, + version: pinnedVersion, + }, + request: req, + }) } else { result = await executeWorkflowOperation.execute({ principal: apiKeyPrincipal, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/operations/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/operations/route.test.ts index f5c47cb69e9..b23a9766fc3 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/operations/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/operations/route.test.ts @@ -45,7 +45,12 @@ const DROPPED_INPUT = { const LINT = createEmptyWorkflowLintReport() const auth = { - principal: { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'personal-key-1' }, + principal: { + kind: 'personal_api_key' as const, + userId: 'user-1', + keyId: 'personal-key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:personal-key-1', 'user:user-1'] as const, rateLimitSubscription: null, keyType: 'personal' as const, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/restore/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/restore/route.test.ts index fc110a96c34..48ca2c607c1 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/restore/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/restore/route.test.ts @@ -21,7 +21,12 @@ import { POST } from '@/app/api/v2/workflows/[workflowId]/restore/route' const WORKFLOW_ID = 'workflow-1' const auth = { - principal: { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'personal-key-1' }, + principal: { + kind: 'personal_api_key' as const, + userId: 'user-1', + keyId: 'personal-key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:personal-key-1', 'user:user-1'] as const, rateLimitSubscription: null, keyType: 'personal' as const, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/route.test.ts index b66fdfd2395..19fa5f2b54d 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/route.test.ts @@ -50,6 +50,7 @@ const auth = { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'personal-key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:personal-key-1', 'user:user-1'] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/cancel/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/cancel/route.test.ts index 0ae665d6231..3255af4de16 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/cancel/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/cancel/route.test.ts @@ -29,6 +29,7 @@ const principal = { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'key-1', + access: 'write' as const, } const auth = { principal, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/files/[fileId]/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/files/[fileId]/route.test.ts index c446d5adbe8..d5264deec6f 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/files/[fileId]/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/files/[fileId]/route.test.ts @@ -46,6 +46,7 @@ const workspaceKeyAuth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/route.test.ts index 3a78c29156b..54c0fc63784 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/route.test.ts @@ -31,6 +31,7 @@ const principal = { kind: 'workspace_api_key' as const, workspaceId: 'workspace-1', keyId: 'key-1', + access: 'write' as const, } const auth = { principal, @@ -58,6 +59,7 @@ const baseStatus = { startedAt: '2026-07-31T00:00:00.000Z', endedAt: '2026-07-31T00:00:05.000Z', totalDurationMs: 5000, + deploymentVersion: null, paused: null, cost: { total: 0.02 }, error: 'Send Email: Invalid credentials', diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/route.ts b/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/route.ts index dab2588457e..9bbf5c353a6 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/route.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/route.ts @@ -45,7 +45,13 @@ export const GET = defineV2JsonRoute({ startedAt: status.startedAt, endedAt: status.endedAt, durationMs: status.totalDurationMs, - paused: status.paused ? v2WorkflowRunStatusSchema.shape.paused.parse(status.paused) : null, + version: status.deploymentVersion ?? null, + paused: status.paused + ? v2WorkflowRunStatusSchema.shape.paused.parse({ + ...status.paused, + points: status.pausePoints ?? [], + }) + : null, cost: status.cost, error: status.error ? classifyExecutionError(new Error(status.error)) : null, output: status.finalOutput, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/runs/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/runs/route.test.ts index 5a01dd1a4eb..9f245d59492 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/runs/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/runs/route.test.ts @@ -30,6 +30,7 @@ const principal = { kind: 'workspace_api_key' as const, workspaceId: 'workspace-1', keyId: 'key-1', + access: 'write' as const, } const auth = { principal, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/state/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/state/route.test.ts index d4c1f6b9a3d..fe92314e2b3 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/state/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/state/route.test.ts @@ -48,7 +48,12 @@ const GRAPH = { } const personalAuth = { - principal: { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'personal-key-1' }, + principal: { + kind: 'personal_api_key' as const, + userId: 'user-1', + keyId: 'personal-key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:personal-key-1', 'user:user-1'] as const, rateLimitSubscription: null, keyType: 'personal' as const, diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/triggers/[blockId]/route.ts b/apps/sim/app/api/v2/workflows/[workflowId]/triggers/[blockId]/route.ts new file mode 100644 index 00000000000..00ca9b5c7d0 --- /dev/null +++ b/apps/sim/app/api/v2/workflows/[workflowId]/triggers/[blockId]/route.ts @@ -0,0 +1,26 @@ +import { v2UpdateWorkflowTriggerContract } from '@/lib/api/contracts/v2/workflows' +import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { presentV2WorkflowTrigger, v2WorkflowErrorPolicies } from '@/lib/workflows/api' +import { workflowOperations } from '@/lib/workflows/application/operations' +import { setWorkflowTriggerEnabled } from '@/lib/workflows/application/triggers' + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +/** + * PATCH /api/v2/workflows/[workflowId]/triggers/[blockId] — enable or disable + * one trigger of the live deployment without redeploying. + */ +export const PATCH = defineV2JsonRoute({ + contract: v2UpdateWorkflowTriggerContract, + auth: v2ApiKeyAuth, + operation: workflowOperations.setTriggerEnabled, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2WorkflowErrorPolicies.concealWorkflowAuthorization, + mapInput: ({ params, body }) => ({ + target: { kind: 'block' as const, workflowId: params.workflowId, blockId: params.blockId }, + enabled: body.enabled, + }), + useCase: setWorkflowTriggerEnabled, + present: ({ trigger }) => ({ data: presentV2WorkflowTrigger(trigger) }), +}) diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/triggers/route.ts b/apps/sim/app/api/v2/workflows/[workflowId]/triggers/route.ts new file mode 100644 index 00000000000..4ed3513fd75 --- /dev/null +++ b/apps/sim/app/api/v2/workflows/[workflowId]/triggers/route.ts @@ -0,0 +1,30 @@ +import { v2ListWorkflowTriggersContract } from '@/lib/api/contracts/v2/workflows' +import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { presentV2WorkflowTrigger, v2WorkflowErrorPolicies } from '@/lib/workflows/api' +import { workflowOperations } from '@/lib/workflows/application/operations' +import { listWorkflowTriggers } from '@/lib/workflows/application/triggers' + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +/** + * GET /api/v2/workflows/[workflowId]/triggers — health of every trigger the + * live deployment serves. + * + * Disabled triggers are listed, because finding the ones that switched + * themselves off after repeated failures is what this read is for; the + * deployment read publishes only the webhooks currently accepting deliveries. + */ +export const GET = defineV2JsonRoute({ + contract: v2ListWorkflowTriggersContract, + auth: v2ApiKeyAuth, + operation: workflowOperations.readTriggers, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2WorkflowErrorPolicies.concealWorkflowAuthorization, + mapInput: ({ params }) => ({ workflowId: params.workflowId }), + useCase: listWorkflowTriggers, + present: ({ triggers }) => ({ + data: triggers.map(presentV2WorkflowTrigger), + nextCursor: null, + }), +}) diff --git a/apps/sim/app/api/v2/workflows/[workflowId]/versions/route.test.ts b/apps/sim/app/api/v2/workflows/[workflowId]/versions/route.test.ts index b94a89bc1e1..5bdaf68c414 100644 --- a/apps/sim/app/api/v2/workflows/[workflowId]/versions/route.test.ts +++ b/apps/sim/app/api/v2/workflows/[workflowId]/versions/route.test.ts @@ -29,6 +29,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: 'workspace-1', keyId: 'workspace-key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:workspace-key-1', 'workspace:workspace-1'] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/workflows/move/route.test.ts b/apps/sim/app/api/v2/workflows/move/route.test.ts index 91be41c1891..8678a820ea7 100644 --- a/apps/sim/app/api/v2/workflows/move/route.test.ts +++ b/apps/sim/app/api/v2/workflows/move/route.test.ts @@ -20,7 +20,12 @@ import { POST } from '@/app/api/v2/workflows/move/route' const WORKSPACE_ID = 'workspace-1' const auth = { - principal: { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'ws-key-1' }, + principal: { + kind: 'workspace_api_key' as const, + workspaceId: WORKSPACE_ID, + keyId: 'ws-key-1', + access: 'write' as const, + }, rateLimitSubjectIds: ['api-key:ws-key-1'] as const, rateLimitSubscription: null, keyType: 'workspace' as const, diff --git a/apps/sim/app/api/v2/workflows/route.test.ts b/apps/sim/app/api/v2/workflows/route.test.ts index daea1653396..14b763c61cd 100644 --- a/apps/sim/app/api/v2/workflows/route.test.ts +++ b/apps/sim/app/api/v2/workflows/route.test.ts @@ -60,6 +60,7 @@ const workspaceAuth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'workspace-key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:workspace-key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/workspaces/[workspaceId]/invitations/route.test.ts b/apps/sim/app/api/v2/workspaces/[workspaceId]/invitations/route.test.ts index 55298f74ee7..854cf04c03d 100644 --- a/apps/sim/app/api/v2/workspaces/[workspaceId]/invitations/route.test.ts +++ b/apps/sim/app/api/v2/workspaces/[workspaceId]/invitations/route.test.ts @@ -23,7 +23,12 @@ import { OrchestrationError } from '@/lib/core/orchestration/types' import { POST } from '@/app/api/v2/workspaces/[workspaceId]/invitations/route' const workspaceId = 'workspace-123' -const principal = { kind: 'personal_api_key', userId: 'caller', keyId: 'key' } as const +const principal = { + kind: 'personal_api_key', + userId: 'caller', + keyId: 'key', + access: 'write', +} as const const call = (body: unknown, query = '') => POST( new NextRequest(`http://localhost/api/v2/workspaces/${workspaceId}/invitations${query}`, { diff --git a/apps/sim/app/api/v2/workspaces/[workspaceId]/members/[userId]/route.ts b/apps/sim/app/api/v2/workspaces/[workspaceId]/members/[userId]/route.ts new file mode 100644 index 00000000000..531dbda9fcf --- /dev/null +++ b/apps/sim/app/api/v2/workspaces/[workspaceId]/members/[userId]/route.ts @@ -0,0 +1,65 @@ +import { isUserCredentialPrincipal } from '@sim/auth/principal' +import { + v2RemoveWorkspaceMemberContract, + v2UpdateWorkspaceMemberContract, +} from '@/lib/api/contracts/v2/workspaces' +import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { presentWorkspaceMember } from '@/lib/api/server/workspace-presenters' +import { captureServerEvent } from '@/lib/posthog/server' +import { v2WorkspaceErrorPolicies } from '@/lib/workspaces/api/route-policies' +import { updateWorkspaceMember } from '@/lib/workspaces/application/manage-permissions' +import { + workspaceMemberOperations, + workspacePermissionOperations, +} from '@/lib/workspaces/application/operations' +import { removeWorkspaceMember } from '@/lib/workspaces/application/remove-member' + +/** PATCH /api/v2/workspaces/[workspaceId]/members/[userId] — Change one member's role. */ +export const PATCH = defineV2JsonRoute({ + contract: v2UpdateWorkspaceMemberContract, + operation: workspacePermissionOperations.update, + auth: v2ApiKeyAuth, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2WorkspaceErrorPolicies.manageWorkspaceMembers, + mapInput: ({ params, body }) => ({ ...params, ...body }), + useCase: updateWorkspaceMember, + present: ({ member }) => ({ data: presentWorkspaceMember(member) }), + onSuccess: ({ principal, input, result }) => { + if (!isUserCredentialPrincipal(principal)) return + for (const change of result.changes) { + captureServerEvent( + principal.userId, + 'workspace_member_role_changed', + { workspace_id: input.workspaceId, new_role: change.newRole }, + { groups: { workspace: input.workspaceId } } + ) + } + }, +}) + +/** DELETE /api/v2/workspaces/[workspaceId]/members/[userId] — Remove a member, or leave. */ +export const DELETE = defineV2JsonRoute({ + contract: v2RemoveWorkspaceMemberContract, + operation: workspaceMemberOperations.remove, + auth: v2ApiKeyAuth, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2WorkspaceErrorPolicies.manageWorkspaceMembers, + mapInput: ({ params }) => params, + useCase: removeWorkspaceMember, + present: (result) => ({ + data: { + userId: result.removedUserId, + deleted: true as const, + organizationMembershipRemoved: result.organizationRemoval, + }, + }), + onSuccess: ({ principal, input, result }) => { + if (!isUserCredentialPrincipal(principal)) return + captureServerEvent( + principal.userId, + 'workspace_member_removed', + { workspace_id: input.workspaceId, is_self_removal: result.selfRemoval }, + { groups: { workspace: input.workspaceId } } + ) + }, +}) diff --git a/apps/sim/app/api/v2/workspaces/[workspaceId]/members/route.ts b/apps/sim/app/api/v2/workspaces/[workspaceId]/members/route.ts index b12db8d4594..f047d313d36 100644 --- a/apps/sim/app/api/v2/workspaces/[workspaceId]/members/route.ts +++ b/apps/sim/app/api/v2/workspaces/[workspaceId]/members/route.ts @@ -4,6 +4,7 @@ import { } from '@/lib/api/contracts/v2/workspaces' import { cursorRoute, cursorScopeKey, UNREADABLE_CURSOR_MESSAGE } from '@/lib/api/cursor-binding' import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { presentWorkspaceMember } from '@/lib/api/server/workspace-presenters' import { OrchestrationError } from '@/lib/core/orchestration/types' import { v2WorkspaceErrorPolicies } from '@/lib/workspaces/api/route-policies' import { listPublicWorkspaceMembers } from '@/lib/workspaces/application/list-public-workspace-members' @@ -50,15 +51,7 @@ export const GET = defineV2JsonRoute({ }, useCase: listPublicWorkspaceMembers, present: ({ page }, { params }) => ({ - data: page.members.map((member) => ({ - userId: member.userId, - email: member.email, - name: member.name, - image: member.image, - role: member.role, - isExternal: member.isExternal, - joinedAt: member.joinedAt.toISOString(), - })), + data: page.members.map(presentWorkspaceMember), nextCursor: page.nextEmail ? encodeScopedCursor( memberCursorScope(params.workspaceId), diff --git a/apps/sim/app/api/v2/workspaces/[workspaceId]/permission-config/route.test.ts b/apps/sim/app/api/v2/workspaces/[workspaceId]/permission-config/route.test.ts index 514415f1da3..65fea9a3fb2 100644 --- a/apps/sim/app/api/v2/workspaces/[workspaceId]/permission-config/route.test.ts +++ b/apps/sim/app/api/v2/workspaces/[workspaceId]/permission-config/route.test.ts @@ -25,7 +25,12 @@ import { NoWorkspaceAccessError } from '@/lib/core/application' import { GET } from '@/app/api/v2/workspaces/[workspaceId]/permission-config/route' const workspaceId = 'workspace-123' -const principal = { kind: 'personal_api_key', userId: 'caller', keyId: 'key' } as const +const principal = { + kind: 'personal_api_key', + userId: 'caller', + keyId: 'key', + access: 'write', +} as const const result = { permissionGroupId: null, groupName: null, diff --git a/apps/sim/app/api/v2/workspaces/[workspaceId]/route.test.ts b/apps/sim/app/api/v2/workspaces/[workspaceId]/route.test.ts index c61f947421f..64bb92b2c8d 100644 --- a/apps/sim/app/api/v2/workspaces/[workspaceId]/route.test.ts +++ b/apps/sim/app/api/v2/workspaces/[workspaceId]/route.test.ts @@ -44,6 +44,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/v2/workspaces/route.test.ts b/apps/sim/app/api/v2/workspaces/route.test.ts index ea6b830c87d..3f91e60833a 100644 --- a/apps/sim/app/api/v2/workspaces/route.test.ts +++ b/apps/sim/app/api/v2/workspaces/route.test.ts @@ -47,6 +47,7 @@ const auth = { kind: 'workspace_api_key' as const, workspaceId: WORKSPACE_ID, keyId: 'key-1', + access: 'write' as const, }, rateLimitSubjectIds: ['api-key:key-1', `workspace:${WORKSPACE_ID}`] as const, rateLimitSubscription: null, diff --git a/apps/sim/app/api/webhooks/[id]/reactivation-gate.test.ts b/apps/sim/app/api/webhooks/[id]/reactivation-gate.test.ts deleted file mode 100644 index 9703530e464..00000000000 --- a/apps/sim/app/api/webhooks/[id]/reactivation-gate.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { webhook } from '@sim/db/schema' -import { - auditMock, - createMockRequest, - hybridAuthMockFns, - permissionGroupScopeMock, - permissionGroupScopeMockFns, - posthogServerMock, - queueTableRows, - resetDbChainMock, - telemetryMock, - workflowAuthzMockFns, -} from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('@sim/audit', () => auditMock) -vi.mock('@/lib/permission-groups/config-scope.server', () => permissionGroupScopeMock) -vi.mock('@/lib/core/telemetry', () => telemetryMock) -vi.mock('@/lib/posthog/server', () => posthogServerMock) -vi.mock('@/lib/webhooks/provider-subscriptions', () => ({ cleanupExternalWebhook: vi.fn() })) - -import { DEFAULT_PERMISSION_GROUP_CONFIG } from '@/lib/permission-groups/fields' -import { PATCH } from '@/app/api/webhooks/[id]/route' - -const ACTOR_ID = 'actor-1' - -function reactivate() { - return PATCH(createMockRequest('PATCH', { isActive: true }), { - params: Promise.resolve({ id: 'webhook-1' }), - }) -} - -/** The single joined read the PATCH handler issues. */ -function queueDormantWebhook(): void { - queueTableRows(webhook, [ - { - webhook: { id: 'webhook-1', isActive: false, failedCount: 0 }, - workflow: { id: 'workflow-1', userId: ACTOR_ID, workspaceId: 'workspace-1' }, - }, - ]) -} - -/** - * `triggers.webhook` is withheld from the actor's group. Flipping a dormant - * webhook back on is the act the key names, so a session belonging to that - * person must be refused — and an executor delegation carrying the same id - * must not be, since it holds the actor's role and none of their capabilities. - */ -describe('the subject the webhook reactivation gate is decided about', () => { - beforeEach(() => { - resetDbChainMock() - workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ - allowed: true, - status: 200, - workflow: { id: 'workflow-1' }, - workspacePermission: 'write', - }) - workflowAuthzMockFns.mockAssertWorkflowMutable.mockResolvedValue(undefined) - permissionGroupScopeMockFns.mockResolvePermissionGroupConfig.mockResolvedValue({ - ...DEFAULT_PERMISSION_GROUP_CONFIG, - disableWebhookTriggers: true, - }) - }) - - it('refuses the actor’s own session', async () => { - hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ - success: true, - userId: ACTOR_ID, - authType: 'session', - }) - queueDormantWebhook() - - const response = await reactivate() - - expect(response.status).toBe(403) - }) - - it('lets an internal executor JWT through without consulting that group', async () => { - hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ - success: true, - userId: ACTOR_ID, - authType: 'internal_jwt', - }) - queueDormantWebhook() - - const response = await reactivate() - - expect(response.status).toBe(200) - expect(permissionGroupScopeMockFns.mockResolvePermissionGroupConfig).not.toHaveBeenCalled() - }) -}) diff --git a/apps/sim/app/api/webhooks/[id]/route.ts b/apps/sim/app/api/webhooks/[id]/route.ts index 41803bcc8f3..68e03fa7939 100644 --- a/apps/sim/app/api/webhooks/[id]/route.ts +++ b/apps/sim/app/api/webhooks/[id]/route.ts @@ -7,6 +7,7 @@ import { authorizeWorkflowByWorkspacePermission, WorkflowLockedError, } from '@sim/platform-authz/workflow' +import { isRecordLike } from '@sim/utils/object' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { @@ -15,16 +16,24 @@ import { updateWebhookContract, } from '@/lib/api/contracts/webhooks' import { parseRequest } from '@/lib/api/server' -import { capabilityGovernedAuthUserId, checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { + createInternalResourceConcealmentPolicy, + defineInternalJsonRoute, + extendInternalErrorPolicy, + internalErrorResponse, + internalOrchestrationErrorPolicy, + internalRateLimits, +} from '@/lib/api/server/routes' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { InsufficientWorkspacePermissionsError } from '@/lib/core/application' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { - capabilityRefusal, - isWorkspaceCapabilityWithheld, -} from '@/lib/permission-groups/capability-assertions' import { captureServerEvent } from '@/lib/posthog/server' import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions' +import { internalWorkflowSessionOrExecutorAuth } from '@/lib/workflows/api' +import { workflowOperations } from '@/lib/workflows/application/operations' +import { setWorkflowTriggerEnabled } from '@/lib/workflows/application/triggers' const logger = createLogger('WebhookAPI') @@ -88,113 +97,47 @@ export const GET = withRouteHandler( } ) -export const PATCH = withRouteHandler( - async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { - try { - const parsed = await parseRequest(updateWebhookContract, request, context) - if (!parsed.success) return parsed.response - const { id } = parsed.data.params - - const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - if (!auth.success || !auth.userId) { - logger.warn('Unauthorized webhook update attempt') - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - const userId = auth.userId - - const { isActive, failedCount } = parsed.data.body - - const webhooks = await db - .select({ - webhook: webhook, - workflow: { - id: workflow.id, - userId: workflow.userId, - workspaceId: workflow.workspaceId, - }, - }) - .from(webhook) - .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) - .where(and(eq(webhook.id, id), isNull(webhook.archivedAt))) - .limit(1) - - if (webhooks.length === 0) { - logger.warn(`Webhook not found: ${id}`) - return NextResponse.json({ error: 'Webhook not found' }, { status: 404 }) - } - - const webhookData = webhooks[0] - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId: webhookData.workflow.id, - userId, - action: 'write', - }) - const canModify = authorization.allowed - - if (!canModify) { - logger.warn(`User ${userId} denied permission to modify webhook: ${id}`) - return NextResponse.json({ error: 'Access denied' }, { status: 403 }) - } - await assertWorkflowMutable(webhookData.workflow.id) - - const setClause: Partial = {} - if (isActive !== undefined && isActive !== webhooks[0].webhook.isActive) { - /** - * permission-group-enforced: triggers.webhook — reactivating is the act - * the key names, "prevent making a workflow reachable from an inbound - * webhook", so gating creation alone left it reachable by flipping a - * dormant webhook back on. Only this direction: deactivating must stay - * open, or a policy change would strand a member with a live webhook - * they cannot turn off. - * - * Keyed to the governed subject rather than `auth.userId`: an internal - * executor JWT embeds the run's actor, and gating on it would apply that - * person's capabilities to a delegation that carries only their role. - */ - const governedUserId = capabilityGovernedAuthUserId(auth) - if (isActive && governedUserId) { - const withheld = await isWorkspaceCapabilityWithheld( - governedUserId, - webhooks[0].workflow.workspaceId ?? '', - 'triggers.webhook' - ) - if (withheld) { - return NextResponse.json( - { error: capabilityRefusal('triggers.webhook') }, - { status: 403 } - ) - } - } - setClause.isActive = isActive - } - if (failedCount !== undefined && failedCount !== webhooks[0].webhook.failedCount) { - setClause.failedCount = failedCount - } - - if (Object.keys(setClause).length === 0) { - logger.info(`No-op webhook PATCH (no field changes): ${id}`) - return NextResponse.json({ webhook: webhooks[0].webhook }, { status: 200 }) - } - - setClause.updatedAt = new Date() - const updatedWebhook = await db - .update(webhook) - .set(setClause) - .where(eq(webhook.id, id)) - .returning() - - logger.info(`Successfully updated webhook: ${id}`) - return NextResponse.json({ webhook: updatedWebhook[0] }, { status: 200 }) - } catch (error) { - if (error instanceof WorkflowLockedError) { - return NextResponse.json({ error: error.message }, { status: error.status }) - } - - logger.error('Error updating webhook', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) +/** + * Pauses or resumes a webhook trigger through the same use case as + * `PATCH /api/v2/workflows/{workflowId}/triggers/{blockId}`, which also owns the + * `triggers.webhook` gate on re-enabling. The id must name the live + * deployment's row: a candidate, retired, or older-version registration answers + * 404 instead of being revived. + * Admits a session or a workflow run's executor delegation; the executor is not + * judged against the triggering member's permission group. + */ +export const PATCH = defineInternalJsonRoute({ + contract: updateWebhookContract, + auth: internalWorkflowSessionOrExecutorAuth, + operation: workflowOperations.setTriggerEnabled, + rateLimit: internalRateLimits.none({ + reason: 'Webhook enablement retains its existing session admission policy.', + }), + errorPolicy: createInternalResourceConcealmentPolicy({ + notFoundMessage: 'Webhook not found', + base: extendInternalErrorPolicy(internalOrchestrationErrorPolicy, (error) => + error instanceof InsufficientWorkspacePermissionsError + ? internalErrorResponse(403, { error: 'Access denied' }) + : null + ), + }), + mapInput: ({ params, body }) => ({ + target: { kind: 'row' as const, table: 'webhook' as const, id: params.id }, + enabled: body.isActive, + }), + useCase: setWorkflowTriggerEnabled, + present: ({ record }) => { + if (record.table !== 'webhook') throw new Error('Webhook update resolved a schedule row') + const { row } = record + return { + webhook: { + ...row, + path: row.path ?? undefined, + providerConfig: isRecordLike(row.providerConfig) ? row.providerConfig : undefined, + }, } - } -) + }, +}) // Delete a webhook export const DELETE = withRouteHandler( diff --git a/apps/sim/app/api/workflows/[id]/execute/route.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.test.ts index a6ce1ca061d..deef9d33abf 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.test.ts @@ -269,12 +269,14 @@ const PERSONAL_API_KEY_PRINCIPAL = { kind: 'personal_api_key', userId: 'personal-key-user-1', keyId: 'personal-key-1', + access: 'write', } as const const WORKSPACE_API_KEY_PRINCIPAL = { kind: 'workspace_api_key', workspaceId: 'workspace-1', keyId: 'workspace-key-1', + access: 'write', } as const const EXECUTION_CALLERS: ExecutionCallerCase[] = [ @@ -2161,6 +2163,7 @@ describe('workflow execute async route', () => { kind: 'personal_api_key', userId: 'api-user-1', keyId: 'personal-key-1', + access: 'write', }, }) workflowsUtilsMockFns.mockWorkflowHasResponseBlock.mockReturnValueOnce(true) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 94d7625a68f..195fc27607d 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -1,4 +1,4 @@ -import type { WorkflowExecutionPrincipal } from '@sim/auth/principal' +import { isReadOnlyApiKeyPrincipal, type WorkflowExecutionPrincipal } from '@sim/auth/principal' import { db } from '@sim/db' import { workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -16,7 +16,11 @@ import { WORKFLOW_EXECUTION_TIMEOUT_SECONDS_HEADER, } from '@/lib/api/contracts/workflows' import { hasExternalApiCredentials } from '@/lib/api/server/credential-headers' -import { PERSONAL_KEY_DENIED, WORKSPACE_KEY_SCOPE_DENIED } from '@/lib/api-key/policy-messages' +import { + PERSONAL_KEY_DENIED, + READ_ONLY_API_KEY_DENIED, + WORKSPACE_KEY_SCOPE_DENIED, +} from '@/lib/api-key/policy-messages' import { AuthType, checkHybridAuth } from '@/lib/auth/hybrid' import { releaseExecutionSlot } from '@/lib/billing/calculations/usage-reservation' import { @@ -550,6 +554,14 @@ async function handleExecutePost( return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } + /** Running a workflow changes state, and this route authorizes outside the operation funnel. */ + if (auth.success && auth.principal && isReadOnlyApiKeyPrincipal(auth.principal)) { + return NextResponse.json( + { error: READ_ONLY_API_KEY_DENIED, details: { code: 'READ_ONLY_API_KEY' } }, + { status: 403 } + ) + } + const executionModeHeader = req.headers.get('X-Execution-Mode') const isAsyncMode = executionModeHeader === 'async' const trustedDeadlineHeader = diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.test.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.test.ts index f6984733410..1a5ffa73f25 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.test.ts @@ -53,6 +53,7 @@ describe('internal execution status route projection subject', () => { startedAt: '2026-08-05T12:00:00.000Z', endedAt: null, totalDurationMs: null, + deploymentVersion: null, paused: null, cost: null, error: null, diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts index d91f113e2e3..3191d34e469 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts @@ -23,7 +23,7 @@ export const GET = withRouteHandler( const { id: workflowId, executionId } = parsed.data.params const { includeOutput, selectedOutputs } = parsed.data.query - const access = await validateWorkflowAccess(request, workflowId, false) + const access = await validateWorkflowAccess(request, workflowId) if (access.error) { return NextResponse.json({ error: access.error.message }, { status: access.error.status }) } diff --git a/apps/sim/app/api/workflows/[id]/paused/route.ts b/apps/sim/app/api/workflows/[id]/paused/route.ts index 74fa1cd4b51..bd1491bdc33 100644 --- a/apps/sim/app/api/workflows/[id]/paused/route.ts +++ b/apps/sim/app/api/workflows/[id]/paused/route.ts @@ -14,7 +14,7 @@ export const GET = withRouteHandler( if (!parsed.success) return parsed.response const { id: workflowId } = parsed.data.params - const access = await validateWorkflowAccess(request, workflowId, false) + const access = await validateWorkflowAccess(request, workflowId) if (access.error) { return NextResponse.json({ error: access.error.message }, { status: access.error.status }) } diff --git a/apps/sim/app/api/workflows/middleware.test.ts b/apps/sim/app/api/workflows/middleware.test.ts index 6f8ef626c36..d5d25e16dbc 100644 --- a/apps/sim/app/api/workflows/middleware.test.ts +++ b/apps/sim/app/api/workflows/middleware.test.ts @@ -1,6 +1,6 @@ /** * Tests for workflow access middleware — focused on the workspace-scoped - * API key boundary check in the `requireDeployment=false` branch. + * API key boundary check. */ import { @@ -13,10 +13,6 @@ import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) -vi.mock('@/lib/api-key/service', () => ({ - authenticateApiKeyFromHeader: vi.fn(), - updateApiKeyLastUsed: vi.fn(), -})) import { WORKSPACE_KEY_SCOPE_DENIED } from '@/lib/api-key/policy-messages' import { validateWorkflowAccess } from '@/app/api/workflows/middleware' @@ -25,7 +21,7 @@ function makeRequest() { return new NextRequest(new URL('https://example.com/api/workflows/wf-1')) } -describe('validateWorkflowAccess (requireDeployment=false)', () => { +describe('validateWorkflowAccess', () => { beforeEach(() => { workflowsUtilsMockFns.mockGetWorkflowById.mockResolvedValue({ id: 'wf-1', @@ -48,7 +44,7 @@ describe('validateWorkflowAccess (requireDeployment=false)', () => { workspaceId: 'ws-B', }) - const result = await validateWorkflowAccess(makeRequest(), 'wf-1', false) + const result = await validateWorkflowAccess(makeRequest(), 'wf-1') expect(result.error).toEqual({ message: WORKSPACE_KEY_SCOPE_DENIED, @@ -70,7 +66,7 @@ describe('validateWorkflowAccess (requireDeployment=false)', () => { message: 'Access denied', }) - const result = await validateWorkflowAccess(makeRequest(), 'wf-1', false) + const result = await validateWorkflowAccess(makeRequest(), 'wf-1') expect(result.error).toEqual({ message: 'Access denied', status: 403 }) }) diff --git a/apps/sim/app/api/workflows/middleware.ts b/apps/sim/app/api/workflows/middleware.ts index 56caf31ae08..1d6becef3d1 100644 --- a/apps/sim/app/api/workflows/middleware.ts +++ b/apps/sim/app/api/workflows/middleware.ts @@ -2,11 +2,6 @@ import { createLogger } from '@sim/logger' import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import type { NextRequest } from 'next/server' import { WORKSPACE_KEY_SCOPE_DENIED } from '@/lib/api-key/policy-messages' -import { - type ApiKeyAuthResult, - authenticateApiKeyFromHeader, - updateApiKeyLastUsed, -} from '@/lib/api-key/service' import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid' import { getWorkflowById } from '@/lib/workflows/utils' @@ -20,8 +15,7 @@ export interface ValidationResult { export async function validateWorkflowAccess( request: NextRequest, - workflowId: string, - requireDeployment = true + workflowId: string ): Promise { try { const workflow = await getWorkflowById(workflowId) @@ -44,95 +38,40 @@ export async function validateWorkflowAccess( } } - if (!requireDeployment) { - const auth = await checkHybridAuth(request, { requireWorkflowId: false }) - if (!auth.success || !auth.userId) { - return { - error: { - message: auth.error || 'Unauthorized', - status: 401, - }, - } - } - - if (auth.apiKeyType === 'workspace' && auth.workspaceId !== workflow.workspaceId) { - return { - error: { - message: WORKSPACE_KEY_SCOPE_DENIED, - status: 403, - }, - } - } - - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId, - userId: auth.userId, - action: 'read', - }) - if (!authorization.allowed) { - return { - error: { - message: authorization.message || 'Access denied', - status: authorization.status, - }, - } + const auth = await checkHybridAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return { + error: { + message: auth.error || 'Unauthorized', + status: 401, + }, } - - return { workflow, auth } } - if (requireDeployment) { - if (!workflow.isDeployed) { - return { - error: { - message: 'Workflow is not deployed', - status: 403, - }, - } - } - - let apiKeyHeader = null - for (const [key, value] of request.headers.entries()) { - if (key.toLowerCase() === 'x-api-key' && value) { - apiKeyHeader = value - break - } - } - - if (!apiKeyHeader) { - return { - error: { - message: 'Unauthorized: API key required', - status: 401, - }, - } - } - - let validResult: ApiKeyAuthResult | null = null - - const workspaceResult = await authenticateApiKeyFromHeader(apiKeyHeader, { - workspaceId: workflow.workspaceId as string, - keyTypes: ['workspace', 'personal'], - }) - - if (workspaceResult.success) { - validResult = workspaceResult - } - - if (!validResult) { - return { - error: { - message: 'Unauthorized: Invalid API key', - status: 401, - }, - } + if (auth.apiKeyType === 'workspace' && auth.workspaceId !== workflow.workspaceId) { + return { + error: { + message: WORKSPACE_KEY_SCOPE_DENIED, + status: 403, + }, } + } - if (validResult.keyId) { - updateApiKeyLastUsed(validResult.keyId) + const authorization = await authorizeWorkflowByWorkspacePermission({ + workflowId, + userId: auth.userId, + action: 'read', + }) + if (!authorization.allowed) { + return { + error: { + message: authorization.message || 'Access denied', + status: authorization.status, + }, } } - return { workflow } + + return { workflow, auth } } catch (error) { logger.error('Validation error:', { error }) return { diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/route.test.ts b/apps/sim/app/api/workspaces/[id]/api-keys/route.test.ts index b4f988ef9b6..8376cb94f9f 100644 --- a/apps/sim/app/api/workspaces/[id]/api-keys/route.test.ts +++ b/apps/sim/app/api/workspaces/[id]/api-keys/route.test.ts @@ -57,6 +57,7 @@ describe('GET /api/workspaces/[id]/api-keys', () => { { id: 'key-1', name: 'Legacy key', + access: 'write', key: 'sim_plaintext_legacy_secret', createdAt: new Date('2026-07-01T00:00:00.000Z'), lastUsed: null, @@ -79,6 +80,7 @@ describe('GET /api/workspaces/[id]/api-keys', () => { { id: 'key-1', name: 'Legacy key', + access: 'write', displayKey: 'sim_••••legacy', createdAt: '2026-07-01T00:00:00.000Z', lastUsed: null, diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts index 2dc2ea4ffdf..04b6bf2d816 100644 --- a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts +++ b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts @@ -21,7 +21,10 @@ import { workspaceApiKeyOperations, } from '@/lib/api-key/application/workspace-api-keys' import { workspaceApiKeyErrorPolicy } from '@/lib/api-key/management-error-policy' -import { performCreateWorkspaceApiKey } from '@/lib/api-key/orchestration' +import { + apiKeyCreationErrorStatus, + performCreateWorkspaceApiKey, +} from '@/lib/api-key/orchestration' import { getSession } from '@/lib/auth' import { PlatformEvents } from '@/lib/core/telemetry' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -90,25 +93,29 @@ export const POST = withRouteHandler( const parsed = await parseRequest(createWorkspaceApiKeyContract, request, context) if (!parsed.success) return parsed.response - const { name, source } = parsed.data.body + const { name, source, access, expiresAt } = parsed.data.body const result = await performCreateWorkspaceApiKey({ workspaceId, userId, name, source, + access, + expiresAt, actorName: session.user.name, actorEmail: session.user.email, }) if (!result.success || !result.key) { - const status = result.errorCode === 'conflict' ? 409 : 500 - return NextResponse.json({ error: result.error }, { status }) + return NextResponse.json( + { error: result.error }, + { status: apiKeyCreationErrorStatus(result.errorCode) } + ) } captureServerEvent( userId, 'api_key_created', - { workspace_id: workspaceId, key_name: name, source }, + { workspace_id: workspaceId, key_name: name, source, access, expires: expiresAt != null }, { groups: { workspace: workspaceId }, setOnce: { first_api_key_created_at: new Date().toISOString() }, diff --git a/apps/sim/app/api/workspaces/[id]/permissions/route.ts b/apps/sim/app/api/workspaces/[id]/permissions/route.ts index a8b28c7547f..d165badf853 100644 --- a/apps/sim/app/api/workspaces/[id]/permissions/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permissions/route.ts @@ -22,8 +22,8 @@ import { captureServerEvent } from '@/lib/posthog/server' import { readWorkspacePermissions, updateWorkspacePermissions, - workspacePermissionOperations, } from '@/lib/workspaces/application/manage-permissions' +import { workspacePermissionOperations } from '@/lib/workspaces/application/operations' import { WorkspacePermissionError } from '@/lib/workspaces/permissions/management-store' function permissionErrorResponse(error: unknown): NextResponse { diff --git a/apps/sim/app/api/workspaces/members/[id]/route.ts b/apps/sim/app/api/workspaces/members/[id]/route.ts index 0c9a109c23c..5c0b9cb6cff 100644 --- a/apps/sim/app/api/workspaces/members/[id]/route.ts +++ b/apps/sim/app/api/workspaces/members/[id]/route.ts @@ -11,16 +11,14 @@ import { NoWorkspaceAccessError, } from '@/lib/core/application/workspace-authorization' import { captureServerEvent } from '@/lib/posthog/server' -import { - removeWorkspaceMember, - removeWorkspaceMemberOperation, -} from '@/lib/workspaces/application/remove-member' +import { workspaceMemberOperations } from '@/lib/workspaces/application/operations' +import { removeWorkspaceMember } from '@/lib/workspaces/application/remove-member' import { WorkspaceBillingAccountRemovalError } from '@/lib/workspaces/utils' export const DELETE = defineInternalJsonRoute({ contract: removeWorkspaceMemberContract, auth: internalSessionAuth, - operation: removeWorkspaceMemberOperation, + operation: workspaceMemberOperations.remove, rateLimit: internalRateLimits.none({ reason: 'Preserve existing workspace member removal policy.', }), diff --git a/apps/sim/app/index.md/route.ts b/apps/sim/app/index.md/route.ts index aa23f7cd198..51749fc2be6 100644 --- a/apps/sim/app/index.md/route.ts +++ b/apps/sim/app/index.md/route.ts @@ -46,12 +46,6 @@ export async function GET() { `${DOCS_URL}/api-reference/getting-started`, 'Create an API key and run a deployed workflow' ), - linkLine( - 'TypeScript SDK', - `${DOCS_URL}/api-reference/typescript`, - '`bun add simstudio-ts-sdk`' - ), - linkLine('Python SDK', `${DOCS_URL}/api-reference/python`, '`pip install simstudio-sdk`'), linkLine('CLI', `${DOCS_URL}/cli`, '`bun add -g sim`'), linkLine('MCP server', `${DOCS_URL}/mcp/overview`, 'Connect an AI app to Sim'), ]), diff --git a/apps/sim/app/o/[organizationId]/layout.tsx b/apps/sim/app/o/[organizationId]/layout.tsx index ddaa31bf583..4f9527a7af7 100644 --- a/apps/sim/app/o/[organizationId]/layout.tsx +++ b/apps/sim/app/o/[organizationId]/layout.tsx @@ -3,6 +3,7 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider' +import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag' import { getSession } from '@/lib/auth' import { getActiveOrganizationId } from '@/lib/auth/session-response' import { canUseBenchmarks } from '@/lib/benchmarks/config' @@ -89,6 +90,7 @@ export default async function OrganizationLayout({ simLinksEnabled, peopleEnabled, simLinkOrganizationId, + readOnlyApiKeysEnabled, ] = await Promise.all([ prefetchOrganizationSidebar( queryClient, @@ -113,6 +115,7 @@ export default async function OrganizationLayout({ userId: session.user.id, sessionId: session.session.id, }), + isReadOnlyApiKeyCreationEnabled(), ]) const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1' @@ -128,6 +131,7 @@ export default async function OrganizationLayout({ 'mothership-plan-mode': benchmarkEnabled, 'mothership-memory-spaces': benchmarkEnabled, projects: projectsEnabled, + 'api-key-read-only': readOnlyApiKeysEnabled, }} > diff --git a/apps/sim/app/oidc/workload/.well-known/openid-configuration/route.ts b/apps/sim/app/oidc/workload/.well-known/openid-configuration/route.ts new file mode 100644 index 00000000000..0e312e42534 --- /dev/null +++ b/apps/sim/app/oidc/workload/.well-known/openid-configuration/route.ts @@ -0,0 +1,5 @@ +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { workloadIdentityDiscoveryResponse } from '@/lib/credentials/workload-identity/responses' + +/** OpenID Connect discovery for Sim's workload-identity issuer (keyless cloud credentials). */ +export const GET = withRouteHandler(workloadIdentityDiscoveryResponse) diff --git a/apps/sim/app/oidc/workload/jwks/route.ts b/apps/sim/app/oidc/workload/jwks/route.ts new file mode 100644 index 00000000000..b990886e686 --- /dev/null +++ b/apps/sim/app/oidc/workload/jwks/route.ts @@ -0,0 +1,5 @@ +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { workloadIdentityJwksResponse } from '@/lib/credentials/workload-identity/responses' + +/** Public signing keys for Sim's workload-identity issuer. */ +export const GET = withRouteHandler(workloadIdentityJwksResponse) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index a082d663828..b5d209fccd7 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -2540,6 +2540,7 @@ function ServiceAccountConnectDisplay({ atlassianProduct={match?.providerId === 'confluence' ? 'confluence' : 'jira'} serviceName={target.serviceName} serviceIcon={target.serviceIcon} + oauthServiceId={service?.providerId} credentialId={reconnectCredentialId} credentialDisplayName={reconnectCredential?.displayName ?? undefined} onCreated={() => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/connect-candidates.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/connect-candidates.test.ts new file mode 100644 index 00000000000..93b4fa67358 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/connect-candidates.test.ts @@ -0,0 +1,32 @@ +import { INTEGRATION_METADATA } from '@sim/deployment-config/integration-metadata' +import { describe, expect, it } from 'vitest' +import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth' +import { unconnectedIntegrations } from '@/app/workspace/[workspaceId]/home/components/suggested-actions/connect-candidates' + +const SERVICES = Object.values(OAUTH_PROVIDERS).flatMap((provider) => + Object.entries(provider.services).map(([id, service]) => ({ ...service, id })) +) + +function githubCandidates(connected: string[]) { + return unconnectedIntegrations(SERVICES, new Set(connected), INTEGRATION_METADATA).filter( + ({ slug }) => slug === 'github' + ) +} + +describe('suggested connect rows', () => { + it('offers GitHub once, through the service its catalog entry names', () => { + expect(githubCandidates([]).map(({ service }) => service.id)).toEqual(['github-token']) + }) + + it.each([ + ['a saved personal access token', 'github-service-account'], + ['a Search OAuth account', 'github-repositories'], + ['a GitHub App installation', 'github-app-installation'], + ])('treats GitHub as connected by %s', (_, providerId) => { + expect(githubCandidates([providerId])).toEqual([]) + }) + + it("still offers GitHub when only another integration's credential is saved", () => { + expect(githubCandidates(['slack', 'gitlab-service-account'])).toHaveLength(1) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/connect-candidates.ts b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/connect-candidates.ts new file mode 100644 index 00000000000..22d68f500ac --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/connect-candidates.ts @@ -0,0 +1,50 @@ +import { credentialProviderMatchesService, type ServiceProviderIdentity } from '@/lib/oauth/utils' + +interface ConnectableService extends ServiceProviderIdentity { + /** Service key in `OAUTH_PROVIDERS`. */ + id: string + name: string +} + +interface CatalogIntegration { + name: string + slug: string + oauthServiceId?: string +} + +/** + * One connect candidate per catalog integration that no saved credential powers + * yet. Several services can share an integration (GitHub has its Search App and + * its saved tokens), so an integration counts as connected when a credential + * matches any of them — by OAuth id or service-account id — and is offered once, + * through the service its catalog entry names. + */ +export function unconnectedIntegrations( + services: readonly S[], + connectedProviderIds: ReadonlySet, + catalog: readonly CatalogIntegration[] +): { service: S; slug: string }[] { + const integrationByName = new Map(catalog.map((entry) => [entry.name.toLowerCase(), entry])) + const servicesBySlug = new Map() + for (const service of services) { + const integration = integrationByName.get(service.name.toLowerCase()) + if (!integration) continue + const group = servicesBySlug.get(integration.slug) + if (group) group.push(service) + else servicesBySlug.set(integration.slug, [service]) + } + + const candidates: { service: S; slug: string }[] = [] + for (const [slug, group] of servicesBySlug) { + const connected = group.some((service) => + [...connectedProviderIds].some((providerId) => + credentialProviderMatchesService(providerId, service) + ) + ) + if (connected) continue + const oauthServiceId = catalog.find((entry) => entry.slug === slug)?.oauthServiceId + const service = group.find((candidate) => candidate.id === oauthServiceId) ?? group[0] + candidates.push({ service, slug }) + } + return candidates +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx index 980eccdcc43..6b1eace7ee0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx @@ -15,6 +15,7 @@ import { } from '@/lib/integrations/oauth-service' import { captureEvent } from '@/lib/posthog/client' import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal' +import { unconnectedIntegrations } from '@/app/workspace/[workspaceId]/home/components/suggested-actions/connect-candidates' import type { Action, ActionIcon, @@ -29,11 +30,6 @@ import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' import { useOAuthConnections } from '@/hooks/queries/oauth/connections' import { useTablesList } from '@/hooks/queries/tables' -/** Lookup integration slug by OAuth service display name (case-insensitive). */ -const SLUG_BY_LOWER_NAME: ReadonlyMap = new Map( - INTEGRATION_METADATA.map((i) => [i.name.toLowerCase(), i.slug]) -) - /** Lookup base block type by catalog slug, for the connect-row popularity weight. */ const TYPE_BY_SLUG: ReadonlyMap = new Map( INTEGRATION_METADATA.map((i) => [i.slug, stripVersionSuffix(i.type)]) @@ -177,11 +173,11 @@ function toIntegrationAction(service: ServiceInfo, slug: string): Action { * prompts; fresh workspaces get two of each. */ function computeActions(services: readonly ServiceInfo[], signals: Signals): Action[] { - const connectCandidates = services.flatMap((s) => { - if (signals.connectedProviders.has(s.providerId)) return [] - const slug = SLUG_BY_LOWER_NAME.get(s.name.toLowerCase()) - return slug ? [{ service: s, slug }] : [] - }) + const connectCandidates = unconnectedIntegrations( + services, + signals.connectedProviders, + INTEGRATION_METADATA + ) const connectCount = signals.connectedProviders.size === 0 ? 2 : 1 const integrations = weightedSample( connectCandidates, diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx index 747d3b1fb60..6056fc92067 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx @@ -1,6 +1,6 @@ 'use client' -import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react' +import { type ReactNode, useEffect, useRef, useState } from 'react' import { Chip, ChipDropdown, ChipLink, cn } from '@sim/emcn' import { ArrowLeft, Plus } from '@sim/emcn/icons' import { useRouter } from 'next/navigation' @@ -174,9 +174,9 @@ function IntegrationBlockDetailContent({ integration, workspaceId }: Integration const oauthAvailable = Boolean(oauthService) && (availability?.oauthAvailable ?? true) const [oauthOpen, setOAuthOpen] = useState(false) const [personalTokenOpen, setPersonalTokenOpen] = useState(false) - const personalTokenAvailable = - integration.type === 'gitlab' && - (availability?.state === 'ready' || availability?.state === 'limited') + const isGitLab = integration.slug === 'gitlab' + const deploymentAvailable = availability?.state === 'ready' || availability?.state === 'limited' + const personalTokenAvailable = isGitLab && deploymentAvailable const { data: credentials = [], isPending: credentialsLoading } = useWorkspaceCredentials({ workspaceId, @@ -190,29 +190,29 @@ function IntegrationBlockDetailContent({ integration, workspaceId }: Integration * credential lists on every product it powers. Comparing resolved * `providerId`s instead hides it from all of them. */ - const connectedCredentials = useMemo(() => { - if (integration.type === 'gitlab') - return credentials.filter((c) => c.type === 'personal_token' && c.providerId === 'gitlab') - if (!oauthService) return [] - return credentials.filter( - (c) => - (c.type === 'oauth' || c.type === 'service_account') && - c.providerId && - credentialProviderMatchesService(c.providerId, oauthService) - ) - }, [credentials, oauthService, integration.type]) + const serviceCredentials = oauthService + ? credentials.filter( + (c) => + (c.type === 'oauth' || c.type === 'service_account') && + c.providerId && + credentialProviderMatchesService(c.providerId, oauthService) + ) + : [] + // GitLab also has personal tokens, which Copilot acts with on the person's behalf. + const connectedCredentials = isGitLab + ? [ + ...credentials.filter((c) => c.type === 'personal_token' && c.providerId === 'gitlab'), + ...serviceCredentials, + ] + : serviceCredentials const [serviceAccountOpen, setServiceAccountOpen] = useState(false) const serviceAccountTarget = useServiceAccountConnectTarget({ serviceAccountProviderId: oauthService?.serviceAccountProviderId, serviceName: oauthService?.serviceName, serviceIcon: oauthService?.serviceIcon, }) - const serviceAccountDeploymentAvailable = - availability?.state === 'ready' || availability?.state === 'limited' const hasServiceAccount = - serviceAccountDeploymentAvailable && - Boolean(serviceAccountTarget) && - !serviceAccountTarget?.hidden + deploymentAvailable && Boolean(serviceAccountTarget) && !serviceAccountTarget?.hidden const serviceAccountConnectLabel = serviceAccountTarget?.label ?? 'Add service account' const hasHandledConnectQueryRef = useRef(false) @@ -246,32 +246,42 @@ function IntegrationBlockDetailContent({ integration, workspaceId }: Integration setConnectMode, ]) - const connectOptions = oauthService - ? [ - ...(oauthAvailable - ? [ - { - value: CONNECT_MODE.oauth, - label: 'Connect with OAuth', - icon: oauthService.serviceIcon, - }, - ] - : []), - ...(hasServiceAccount - ? [ - { - value: CONNECT_MODE.serviceAccount, - label: serviceAccountConnectLabel, - icon: serviceAccountTarget?.serviceIcon ?? oauthService.serviceIcon, - }, - ] - : []), - ] - : [] + const connectOptions = [ + ...(personalTokenAvailable + ? [ + { + value: CONNECT_MODE.personalToken, + label: 'Add personal token', + icon: oauthService?.serviceIcon ?? Icon, + }, + ] + : []), + ...(oauthService && oauthAvailable + ? [ + { + value: CONNECT_MODE.oauth, + label: 'Connect with OAuth', + icon: oauthService.serviceIcon, + }, + ] + : []), + ...(oauthService && hasServiceAccount + ? [ + { + value: CONNECT_MODE.serviceAccount, + label: serviceAccountConnectLabel, + icon: serviceAccountTarget?.serviceIcon ?? oauthService.serviceIcon, + }, + ] + : []), + ] + + const singleConnectOption = connectOptions.length === 1 ? connectOptions[0] : undefined const handleSelectConnectOption = (value: string) => { if (value === CONNECT_MODE.oauth) setOAuthOpen(true) else if (value === CONNECT_MODE.serviceAccount) setServiceAccountOpen(true) + else if (value === CONNECT_MODE.personalToken) setPersonalTokenOpen(true) } const handleAddInChat = () => { @@ -286,32 +296,28 @@ function IntegrationBlockDetailContent({ integration, workspaceId }: Integration Integrations
- {personalTokenAvailable ? ( - setPersonalTokenOpen(true)}> - Add personal token + {connectOptions.length > 1 ? ( + + ) : singleConnectOption ? ( + handleSelectConnectOption(singleConnectOption.value)} + > + {singleConnectOption.value === CONNECT_MODE.oauth + ? 'Add to Sim' + : singleConnectOption.label} ) : oauthService ? ( - connectOptions.length > 1 ? ( - - ) : oauthAvailable ? ( - setOAuthOpen(true)}> - Add to Sim - - ) : hasServiceAccount ? ( - setServiceAccountOpen(true)}> - {serviceAccountConnectLabel} - - ) : ( - Unavailable - ) + Unavailable ) : chatEnabled ? ( Add to Sim @@ -349,6 +355,7 @@ function IntegrationBlockDetailContent({ integration, workspaceId }: Integration atlassianProduct={oauthService?.providerId === 'confluence' ? 'confluence' : 'jira'} serviceName={serviceAccountTarget.serviceName} serviceIcon={serviceAccountTarget.serviceIcon} + oauthServiceId={oauthService?.providerId} /> )}
void + workspaceId?: string + organizationId?: string + serviceIcon: ComponentType<{ className?: string }> + credentialId?: string + initialDisplayName?: string + initialDescription?: string + onCreated?: (credentialId: string) => void +} + +type AwsAuthType = 'oidc_role' | 'access_keys' | 'assume_role' + +const ROLE_ARN_PLACEHOLDER = 'arn:aws:iam::123456789012:role/sim-workflows' +const ACCOUNT_ID_FROM_ROLE_ARN = /^arn:aws[\w-]*:iam::(\d{12}):role\// + +const AUTH_TYPE_HINTS: Record = { + oidc_role: 'No keys are stored. Sim signs a short-lived token that your role trusts.', + assume_role: 'Sim uses the keys only to assume the role, then works with temporary credentials.', + access_keys: 'Sim signs requests with these long-lived keys.', +} + +/** The trust policy a customer attaches to the role so it trusts exactly this credential. */ +function buildTrustPolicy(issuer: string, audience: string, subject: string, roleArn: string) { + const issuerHostPath = issuer.replace(/^https:\/\//, '') + const trimmedArn = roleArn.trim() + const accountId = ACCOUNT_ID_FROM_ROLE_ARN.exec(trimmedArn)?.[1] ?? '' + const partition = trimmedArn.startsWith('arn:aws-') ? trimmedArn.split(':')[1] : 'aws' + return JSON.stringify( + { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Federated: `arn:${partition}:iam::${accountId}:oidc-provider/${issuerHostPath}`, + }, + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + StringEquals: { + [`${issuerHostPath}:aud`]: audience, + [`${issuerHostPath}:sub`]: subject, + }, + }, + }, + ], + }, + null, + 2 + ) +} + +/** Saved AWS identities share the standard credential create/reconnect lifecycle. */ +export function AwsAccountModal(props: AwsAccountModalProps) { + if (!props.open) return null + return ( + + ) +} + +function AwsAccountForm({ + open, + onOpenChange, + workspaceId, + organizationId, + serviceIcon: ServiceIcon, + credentialId, + initialDisplayName, + initialDescription, + onCreated, +}: AwsAccountModalProps) { + const keyless = useKeylessSetup({ provider: 'aws', workspaceId, organizationId, credentialId }) + const { submit, error, clearError, isPending } = useSavedConnectionSubmit({ + providerId: AWS_SERVICE_ACCOUNT_PROVIDER_ID, + workspaceId, + organizationId, + credentialId, + onCreated, + onClose: () => onOpenChange(false), + }) + const [selectedAuthType, setSelectedAuthType] = useState(null) + const [roleArn, setRoleArn] = useState('') + const [accessKeyId, setAccessKeyId] = useState('') + const [secretAccessKey, setSecretAccessKey] = useState('') + const [externalId, setExternalId] = useState('') + const [stsRegion, setStsRegion] = useState('') + const [displayName, setDisplayName] = useState(initialDisplayName ?? '') + const [description, setDescription] = useState(initialDescription ?? '') + + // A reconnect opens on the credential's stored mode, so it never switches auth by default; + // a new connection defaults to keyless wherever this deployment supports it. + const defaultAuthType: AwsAuthType = credentialId + ? (keyless.reconnectAuthType ?? 'access_keys') + : keyless.available + ? 'oidc_role' + : 'access_keys' + const authType: AwsAuthType = selectedAuthType ?? defaultAuthType + const needsKeys = authType === 'access_keys' || authType === 'assume_role' + const needsRole = authType === 'oidc_role' || authType === 'assume_role' + const disabled = + isPending || + (needsKeys && (!accessKeyId.trim() || !secretAccessKey)) || + (needsRole && !roleArn.trim()) || + (authType === 'oidc_role' && !keyless.ready) + + const edit = (setter: (value: string) => void) => (value: string) => { + setter(value) + clearError() + } + + const buildConfig = (): Record => { + if (authType === 'oidc_role') { + return { authType, roleArn: roleArn.trim(), ...keylessSubjectFields(keyless) } + } + const keys = { accessKeyId: accessKeyId.trim(), secretAccessKey } + if (authType === 'assume_role') { + return { + authType, + ...keys, + roleArn: roleArn.trim(), + ...(externalId.trim() ? { externalId: externalId.trim() } : {}), + } + } + return { authType, ...keys, ...(stsRegion.trim() ? { stsRegion: stsRegion.trim() } : {}) } + } + + const trustPolicy = + authType === 'oidc_role' && keyless.trust?.audience + ? buildTrustPolicy( + keyless.trust.issuer, + keyless.trust.audience, + keyless.trust.subject, + roleArn + ) + : null + const action = credentialId ? 'Reconnect' : 'Add' + const authOptions = [ + ...(keyless.available + ? [{ value: 'oidc_role', label: 'IAM role (keyless, via Sim OIDC)' }] + : []), + { value: 'access_keys', label: 'Access keys' }, + { value: 'assume_role', label: 'Access keys that assume an IAM role' }, + ] + + return ( + + onOpenChange(false)}> + {action} AWS credential + + + { + setSelectedAuthType(value as AwsAuthType) + clearError() + }} + options={authOptions} + placeholder='Choose how Sim signs in to AWS' + required + hint={AUTH_TYPE_HINTS[authType]} + /> + + {authType === 'oidc_role' && } + + {needsKeys && ( + <> + + + {(aria) => ( + + )} + + + )} + + {authType === 'access_keys' && ( + + )} + + {needsRole && ( + + )} + + {authType === 'assume_role' && ( + + )} + + {trustPolicy && ( + + {(aria) => ( + + )} + + )} + + + + {error} + + onOpenChange(false)} + secondaryActions={[ + { + label: 'Setup guide', + onClick: () => + window.open( + 'https://docs.sim.ai/integrations/aws-credentials', + '_blank', + 'noopener,noreferrer' + ), + }, + ]} + primaryAction={{ + label: isPending ? 'Verifying...' : `${action} credential`, + onClick: () => { + if (!disabled) void submit({ config: buildConfig(), displayName, description }) + }, + disabled, + }} + /> + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal.tsx index dcdc3920d4a..e13bbc273e1 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal.tsx @@ -9,6 +9,7 @@ import { ChipModalFooter, ChipModalHeader, ChipTextarea, + CopyCodeButton, SecretInput, } from '@sim/emcn' import { createLogger } from '@sim/logger' @@ -35,6 +36,14 @@ import { const logger = createLogger('ClientCredentialAccountModal') +type FieldValues = Partial> + +/** Submit-error copy, with an optional provider link the user must open to recover. */ +interface ClientCredentialErrorCopy { + message: string + link?: { href: string; label: string } +} + const FALLBACK_ERROR_MESSAGE = "We couldn't save this credential. Try again in a moment." /** @@ -46,37 +55,69 @@ function messageForClientCredentialError( err: unknown, descriptor: ClientCredentialAccountDescriptor, requiredFields: ClientCredentialAccountField[], + values: FieldValues, fallbackMessage = FALLBACK_ERROR_MESSAGE -): string { +): ClientCredentialErrorCopy { if (isApiClientError(err) && err.code) { // Names the fields the *selected* auth method needed, so a JWT failure // doesn't tell the user to check a consumer secret they never entered. const fieldLabels = requiredFields.map((field) => field.label).join(', ') switch (err.code) { case 'invalid_credentials': - return `We couldn't authenticate with those credentials. Check that the ${fieldLabels} all belong to the same ${descriptor.serviceLabel} app and that the app is authorized.` + return { + message: `We couldn't authenticate with those credentials. Check that the ${fieldLabels} all belong to the same ${descriptor.serviceLabel} app and that the app is authorized.`, + } case 'permission_conflict': - return 'Use the same permissions for connections to this application, or create a separate application for different permissions.' + return { + message: + 'Use the same permissions for connections to this application, or create a separate application for different permissions.', + } case 'site_not_found': { // "host field" named a label no provider renders — Salesforce calls it // My Domain host, and Zoom/Box/Zoho Desk have no host field at all. const hostFieldLabel = descriptor.fields.find((field) => field.id === 'orgId')?.label ?? 'host' - return `We couldn't find a ${descriptor.serviceLabel} account at that host. Check the ${hostFieldLabel} field and try again.` + return { + message: `We couldn't find a ${descriptor.serviceLabel} account at that host. Check the ${hostFieldLabel} field and try again.`, + } } case 'provider_unavailable': - return `We couldn't reach ${descriptor.serviceLabel} to verify these credentials. Try again in a moment.` + return { + message: `We couldn't reach ${descriptor.serviceLabel} to verify these credentials. Try again in a moment.`, + } + case 'consent_required': { + const redirectUri = window.location.origin + const consentUrl = descriptor.consentUrl?.(values, redirectUri) + return consentUrl + ? { + message: `${descriptor.serviceLabel} needs the impersonated user's consent first. Add ${redirectUri} as a redirect URI on the integration key, then open the consent page signed in as that user, accept, and try again.`, + link: { href: consentUrl, label: 'Open consent page' }, + } + : { + message: `${descriptor.serviceLabel} needs the impersonated user's consent first. See the setup guide.`, + } + } + case 'account_not_found': { + if (!values.orgId?.trim()) { + return { + message: `That user has no ${descriptor.serviceLabel} account in this environment. Check the environment and user ID.`, + } + } + const accountFieldLabel = + descriptor.fields.find((field) => field.id === 'orgId')?.label ?? 'account' + return { + message: `That user isn't a member of the ${accountFieldLabel} you entered. Check it, or leave it blank to use the user's default account.`, + } + } case 'duplicate_display_name': - return 'A credential with that name already exists in this workspace.' + return { message: 'A credential with that name already exists in this workspace.' } default: - return fallbackMessage + return { message: fallbackMessage } } } - return fallbackMessage + return { message: fallbackMessage } } -type FieldValues = Partial> - function openDocs(url: string): void { window.open(url, '_blank', 'noopener,noreferrer') } @@ -142,7 +183,7 @@ function ClientCredentialAccountModalForm({ const [values, setValues] = useState({}) const [displayName, setDisplayName] = useState(initialDisplayName ?? '') const [description, setDescription] = useState(initialDescription ?? '') - const [error, setError] = useState(null) + const [error, setError] = useState(null) const createCredential = useCreateScopedCredential() const updateCredential = useUpdateScopedCredential() @@ -242,6 +283,7 @@ function ClientCredentialAccountModalForm({ err, descriptor, required, + values, onSubmitCredentials ? getErrorMessage(err, FALLBACK_ERROR_MESSAGE) : undefined ) ) @@ -383,7 +425,26 @@ function ClientCredentialAccountModalForm({ minHeight={80} /> - {error} + + {error && ( + <> + {error.message} + {error.link && ( + + + {error.link.label} + + + + )} + + )} + onOpenChange(false)} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx index b9d825364c3..15249331c63 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx @@ -4,6 +4,7 @@ import { type ComponentType, useState } from 'react' import { ChipModal, ChipModalBody, + ChipModalDescription, ChipModalError, ChipModalField, ChipModalFooter, @@ -25,21 +26,44 @@ import { type ClientCredentialAccountProviderId, getClientCredentialAccountDescriptor, } from '@/lib/credentials/client-credential-accounts/descriptors' +import { + GOOGLE_KEYLESS_SERVICES, + googleKeylessOffer, +} from '@/lib/credentials/google-workload-identity' import type { ServiceAccountSecretFields } from '@/lib/credentials/service-account-secret' import { getTokenServiceAccountDescriptor, + SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, type TokenServiceAccountProviderId, } from '@/lib/credentials/token-service-accounts/descriptors' import { getServiceAccountCoverageSentence } from '@/lib/integrations/credential-display' import { ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID, + AWS_SERVICE_ACCOUNT_PROVIDER_ID, + DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, + MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID, ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID, SLACK_CUSTOM_BOT_PROVIDER_ID, } from '@/lib/oauth/types' +import { AwsAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/aws-account-modal' import { ClientCredentialAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal' +import { DatabricksAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/databricks-account-modal' +import { + buildGoogleKeylessConfig, + EMPTY_GOOGLE_KEYLESS_INPUT, + GoogleKeylessFields, + type GoogleKeylessInput, + isGoogleKeylessInputComplete, +} from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/google-keyless-fields' +import { + useKeylessSetup, + useSavedConnectionSubmit, +} from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/keyless-connection' +import { MicrosoftAppAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/microsoft-app-account-modal' import { OracleDatabaseAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/oracle-database-account-modal' +import { SnowflakeAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/snowflake-account-modal' import { TokenServiceAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal' import { ConnectSlackBotModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal' import { withBrandIcon } from '@/blocks/brand-icon' @@ -56,6 +80,9 @@ export type ServiceAccountProviderId = | typeof SLACK_CUSTOM_BOT_PROVIDER_ID | typeof OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID | typeof ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof AWS_SERVICE_ACCOUNT_PROVIDER_ID + | typeof DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID + | typeof MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID | TokenServiceAccountProviderId | ClientCredentialAccountProviderId @@ -138,6 +165,11 @@ interface ConnectServiceAccountModalProps { credentialDisplayName?: string /** Existing description, used to seed reconnect-capable modals. */ credentialDescription?: string + /** + * The OAuth service the dialog was opened for (`google-bigquery`). A Google service account + * offers keyless setup only for a Google Cloud service. + */ + oauthServiceId?: string /** Called with the new credential id after a successful create (token-paste providers). */ onCreated?: (credentialId: string) => void onSubmitCredentials?: ( @@ -173,10 +205,56 @@ export function ConnectServiceAccountModal({ credentialId, credentialDisplayName, credentialDescription, + oauthServiceId, onCreated, onSubmitCredentials, submitPending, }: ConnectServiceAccountModalProps) { + if (serviceAccountProviderId === DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID) { + return ( + + ) + } + if (serviceAccountProviderId === MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID) { + return ( + + ) + } + if (serviceAccountProviderId === AWS_SERVICE_ACCOUNT_PROVIDER_ID) { + return ( + + ) + } if (serviceAccountProviderId === ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID) { return ( ) } + // A caller that saves the fields itself (a connect draft) takes the token only. + if (serviceAccountProviderId === SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID && !onSubmitCredentials) { + return ( + + ) + } const tokenDescriptor = getTokenServiceAccountDescriptor(serviceAccountProviderId) if (tokenDescriptor) { return ( @@ -292,6 +387,7 @@ export function ConnectServiceAccountModal({ credentialId={credentialId} initialDisplayName={credentialDisplayName} initialDescription={credentialDescription} + oauthServiceId={oauthServiceId} onCreated={onCreated} /> ) @@ -524,12 +620,20 @@ function OciApiKeyServiceAccountModal({ ) } +interface GoogleServiceAccountModalProps extends ProviderModalProps { + oauthServiceId?: string +} + +type GoogleAuthType = 'json_key' | 'workload_identity' + /** * Google service-account flow. Accepts the raw JSON key (paste or drag/drop) * and validates against the shared `serviceAccountJsonSchema` so the same - * shape errors render here as in the server route. + * shape errors render here as in the server route. For a Google Cloud service + * (or a keyless credential being reconnected) on a deployment that can sign + * workload-identity tokens, it also offers keyless Workload Identity Federation. */ -function GoogleServiceAccountModal(props: ProviderModalProps) { +function GoogleServiceAccountModal(props: GoogleServiceAccountModalProps) { if (!props.open) return null return ( onOpenChange(false), + }) + const keylessOffer = googleKeylessOffer({ + oauthServiceId, + available: Boolean(keyless.available), + reconnectsKeyless: keyless.reconnectAuthType === 'workload_identity', + }) + const offerKeyless = keylessOffer === 'offer' + const [selectedAuthType, setSelectedAuthType] = useState(null) + const authType: GoogleAuthType = offerKeyless + ? (selectedAuthType ?? keyless.reconnectAuthType ?? 'json_key') + : 'json_key' + const [keylessInput, setKeylessInput] = useState(EMPTY_GOOGLE_KEYLESS_INPUT) const [jsonInput, setJsonInput] = useState('') const [uploadedFileName, setUploadedFileName] = useState(null) const [displayName, setDisplayName] = useState(initialDisplayName ?? '') @@ -639,8 +771,15 @@ function GoogleServiceAccountModalForm({ } } - const isPending = createCredential.isPending || updateCredential.isPending - const isDisabled = !jsonInput.trim() || isPending + const isPending = + createCredential.isPending || updateCredential.isPending || keylessSubmit.isPending + const cloudOnly = keylessOffer === 'cloud_only' + const isDisabled = + cloudOnly || + (authType === 'workload_identity' + ? isPending || !keyless.ready || !isGoogleKeylessInputComplete(keylessInput) + : !jsonInput.trim() || isPending) + const submitError = authType === 'workload_identity' ? keylessSubmit.error : error return ( - { - setJsonInput(value) - if (uploadedFileName) setUploadedFileName(null) - if (error) setError(null) - maybeFillDisplayNameFromJson(value) - }} - placeholder='Paste your service account JSON key here' - minHeight={120} - required - /> + {offerKeyless && ( + { + setSelectedAuthType(value as GoogleAuthType) + setError(null) + keylessSubmit.clearError() + }} + options={[ + { value: 'json_key', label: 'JSON key' }, + { + value: 'workload_identity', + label: 'Workload Identity Federation (keyless, via Sim OIDC)', + }, + ]} + required + hint={ + authType === 'workload_identity' + ? 'No key is stored. Sim signs a short-lived token your workload identity provider trusts, then impersonates the service account. Google Cloud APIs only, without domain-wide delegation.' + : undefined + } + /> + )} - + {authType === 'workload_identity' && ( + { + setKeylessInput(next) + keylessSubmit.clearError() + }} + setup={keyless} + /> + )} + + {cloudOnly && ( + + This keyless credential works only with Google Cloud services (BigQuery, Vertex AI). + Reconnect it from one of those integrations or from the credential’s own page. + + )} + + {authType === 'json_key' && !cloudOnly && ( + <> + { + setJsonInput(value) + if (uploadedFileName) setUploadedFileName(null) + if (error) setError(null) + maybeFillDisplayNameFromJson(value) + }} + placeholder='Paste your service account JSON key here' + minHeight={120} + required + /> + + + + )} @@ -698,7 +889,7 @@ function GoogleServiceAccountModalForm({ minHeight={80} /> - {error} + {submitError} onOpenChange(false)} @@ -709,8 +900,24 @@ function GoogleServiceAccountModalForm({ }, ]} primaryAction={{ - label: isPending ? 'Adding...' : 'Add service account', - onClick: handleSubmit, + label: isPending + ? authType === 'workload_identity' + ? 'Verifying...' + : 'Adding...' + : 'Add service account', + onClick: () => { + if (authType === 'json_key') { + void handleSubmit() + return + } + if (!isDisabled) { + void keylessSubmit.submit({ + config: buildGoogleKeylessConfig(keylessInput, keyless), + displayName, + description, + }) + } + }, disabled: isDisabled, }} /> diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/databricks-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/databricks-account-modal.tsx new file mode 100644 index 00000000000..ade9ff7ff35 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/databricks-account-modal.tsx @@ -0,0 +1,278 @@ +'use client' + +import { type ComponentType, useState } from 'react' +import { + ChipModal, + ChipModalBody, + ChipModalError, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + ChipTextarea, + SecretInput, +} from '@sim/emcn' +import { resourceScopeFromOwner, resourceScopeKey } from '@/lib/core/resource-scope' +import { DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/oauth/types' +import { + KeylessTrustFields, + keylessSubjectFields, + useKeylessSetup, + useSavedConnectionSubmit, +} from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/keyless-connection' +import { withBrandIcon } from '@/blocks/brand-icon' + +interface DatabricksAccountModalProps { + open: boolean + onOpenChange: (open: boolean) => void + workspaceId?: string + organizationId?: string + serviceIcon: ComponentType<{ className?: string }> + credentialId?: string + initialDisplayName?: string + initialDescription?: string + onCreated?: (credentialId: string) => void +} + +type DatabricksAuthType = 'oidc_federation' | 'service_principal' | 'personal_access_token' + +const AUTH_TYPE_HINTS: Record = { + oidc_federation: + 'No secret is stored. Sim signs a short-lived token your service principal trusts.', + service_principal: + 'Sim exchanges the OAuth secret for a short-lived workspace token on each run.', + personal_access_token: 'Sim sends this token on every request.', +} + +/** The account-level CLI command that makes the service principal trust this credential. */ +function buildFederationPolicyCommand(issuer: string, audience: string, subject: string) { + const policy = JSON.stringify({ oidc_policy: { issuer, audiences: [audience], subject } }) + return `databricks account service-principal-federation-policy create --json '${policy}'` +} + +/** Saved Databricks connections share the standard credential create/reconnect lifecycle. */ +export function DatabricksAccountModal(props: DatabricksAccountModalProps) { + if (!props.open) return null + return ( + + ) +} + +function DatabricksAccountForm({ + open, + onOpenChange, + workspaceId, + organizationId, + serviceIcon: ServiceIcon, + credentialId, + initialDisplayName, + initialDescription, + onCreated, +}: DatabricksAccountModalProps) { + const keyless = useKeylessSetup({ + provider: 'databricks', + workspaceId, + organizationId, + credentialId, + }) + const { submit, error, clearError, isPending } = useSavedConnectionSubmit({ + providerId: DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, + workspaceId, + organizationId, + credentialId, + onCreated, + onClose: () => onOpenChange(false), + }) + const [selectedAuthType, setSelectedAuthType] = useState(null) + const [host, setHost] = useState('') + const [token, setToken] = useState('') + const [clientId, setClientId] = useState('') + const [clientSecret, setClientSecret] = useState('') + const [displayName, setDisplayName] = useState(initialDisplayName ?? '') + const [description, setDescription] = useState(initialDescription ?? '') + + // A reconnect opens on the credential's stored mode, so it never switches auth by default; + // a new connection defaults to keyless wherever this deployment supports it. + const defaultAuthType: DatabricksAuthType = credentialId + ? (keyless.reconnectAuthType ?? 'service_principal') + : keyless.available + ? 'oidc_federation' + : 'service_principal' + const authType: DatabricksAuthType = selectedAuthType ?? defaultAuthType + const usesServicePrincipal = authType === 'service_principal' || authType === 'oidc_federation' + const disabled = + isPending || + !host.trim() || + (authType === 'personal_access_token' && !token.trim()) || + (usesServicePrincipal && !clientId.trim()) || + (authType === 'service_principal' && !clientSecret) || + (authType === 'oidc_federation' && !keyless.ready) + + const edit = (setter: (value: string) => void) => (value: string) => { + setter(value) + clearError() + } + + const buildConfig = (): Record => { + const base = { authType, host: host.trim() } + if (authType === 'oidc_federation') { + return { ...base, clientId: clientId.trim(), ...keylessSubjectFields(keyless) } + } + if (authType === 'service_principal') { + return { ...base, clientId: clientId.trim(), clientSecret } + } + return { ...base, token: token.trim() } + } + + const federationCommand = + authType === 'oidc_federation' && keyless.trust?.audience + ? buildFederationPolicyCommand( + keyless.trust.issuer, + keyless.trust.audience, + keyless.trust.subject + ) + : null + const action = credentialId ? 'Reconnect' : 'Add' + const authOptions = [ + ...(keyless.available + ? [{ value: 'oidc_federation', label: 'Service principal (keyless, via Sim OIDC)' }] + : []), + { value: 'service_principal', label: 'Service principal OAuth secret' }, + { value: 'personal_access_token', label: 'Personal access token' }, + ] + + return ( + + onOpenChange(false)}> + {action} Databricks connection + + + { + setSelectedAuthType(value as DatabricksAuthType) + clearError() + }} + options={authOptions} + placeholder='Choose how Sim signs in to Databricks' + required + hint={AUTH_TYPE_HINTS[authType]} + /> + + + {authType === 'personal_access_token' && ( + + {(aria) => ( + + )} + + )} + + {usesServicePrincipal && ( + + )} + + {authType === 'service_principal' && ( + + {(aria) => ( + + )} + + )} + + {authType === 'oidc_federation' && } + + {federationCommand && ( + + {(aria) => ( + + )} + + )} + + + + {error} + + onOpenChange(false)} + secondaryActions={[ + { + label: 'Setup guide', + onClick: () => + window.open( + 'https://docs.sim.ai/integrations/databricks-credentials', + '_blank', + 'noopener,noreferrer' + ), + }, + ]} + primaryAction={{ + label: isPending ? 'Verifying...' : `${action} connection`, + onClick: () => { + if (!disabled) void submit({ config: buildConfig(), displayName, description }) + }, + disabled, + }} + /> + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/google-keyless-fields.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/google-keyless-fields.tsx new file mode 100644 index 00000000000..8ae307ddf2a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/google-keyless-fields.tsx @@ -0,0 +1,145 @@ +'use client' + +import { ChipModalField, ChipTextarea } from '@sim/emcn' +import { + googleWorkloadIdentityAudience, + googleWorkloadIdentitySetupCommands, + isGoogleProjectNumber, + isGoogleServiceAccountEmail, + isGoogleWorkloadIdentityId, +} from '@/lib/credentials/google-workload-identity' +import { + type KeylessSetup, + KeylessTrustFields, + keylessSubjectFields, +} from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/keyless-connection' + +/** What the person enters for a keyless Google Cloud credential. */ +export interface GoogleKeylessInput { + projectNumber: string + poolId: string + providerId: string + serviceAccountEmail: string +} + +export const EMPTY_GOOGLE_KEYLESS_INPUT: GoogleKeylessInput = { + projectNumber: '', + poolId: '', + providerId: '', + serviceAccountEmail: '', +} + +function normalize(input: GoogleKeylessInput): GoogleKeylessInput { + return { + projectNumber: input.projectNumber.trim(), + poolId: input.poolId.trim(), + providerId: input.providerId.trim(), + serviceAccountEmail: input.serviceAccountEmail.trim().toLowerCase(), + } +} + +/** Whether every field has Google's documented format, so the commands can be shown. */ +export function isGoogleKeylessInputComplete(input: GoogleKeylessInput): boolean { + const value = normalize(input) + return ( + isGoogleProjectNumber(value.projectNumber) && + isGoogleWorkloadIdentityId(value.poolId) && + isGoogleWorkloadIdentityId(value.providerId) && + isGoogleServiceAccountEmail(value.serviceAccountEmail) + ) +} + +/** The `serviceAccountJson` a keyless Google credential is submitted as. */ +export function buildGoogleKeylessConfig(input: GoogleKeylessInput, setup: KeylessSetup) { + return { authType: 'workload_identity', ...normalize(input), ...keylessSubjectFields(setup) } +} + +interface GoogleKeylessFieldsProps { + input: GoogleKeylessInput + onChange: (input: GoogleKeylessInput) => void + setup: KeylessSetup +} + +/** + * The workload identity provider and service account of a keyless Google + * Cloud credential, plus the trust values and gcloud commands to paste. + */ +export function GoogleKeylessFields({ input, onChange, setup }: GoogleKeylessFieldsProps) { + const complete = isGoogleKeylessInputComplete(input) + const value = normalize(input) + const audience = complete ? googleWorkloadIdentityAudience(value) : undefined + const commands = + complete && setup.trust + ? googleWorkloadIdentitySetupCommands({ + ref: value, + serviceAccountEmail: value.serviceAccountEmail, + issuer: setup.trust.issuer, + subject: setup.trust.subject, + }) + : null + const edit = (field: keyof GoogleKeylessInput) => (next: string) => + onChange({ ...input, [field]: next }) + + return ( + <> + + + + + + {commands && ( + + {(aria) => ( + + )} + + )} + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/keyless-connection.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/keyless-connection.tsx new file mode 100644 index 00000000000..f9abe525ff8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/keyless-connection.tsx @@ -0,0 +1,261 @@ +'use client' + +import { useState } from 'react' +import { ChipModalError, ChipModalField } from '@sim/emcn' +import { generateShortId } from '@sim/utils/id' +import { isApiClientError } from '@/lib/api/client/errors' +import { resourceScopeFields, resourceScopeFromOwner } from '@/lib/core/resource-scope' +import { + WORKLOAD_IDENTITY_KEYLESS_CONFIGS, + WORKLOAD_IDENTITY_PROVIDER_AUTH_TYPES, + type WorkloadIdentityProvider, + type WorkloadIdentityProviderAuthType, +} from '@/lib/credentials/workload-identity/audiences' +import { + useCreateScopedCredential, + useUpdateScopedCredential, + useWorkloadIdentitySetup, +} from '@/hooks/queries/scoped-credentials' + +/** + * Shared plumbing for connect dialogs whose credential is a JSON configuration + * that may be keyless (trusting Sim's workload-identity issuer): the AWS and + * Databricks dialogs differ only in their own fields and trust snippet. + */ + +export type TrustSubjectChoice = 'keep' | 'new' + +interface KeylessSetupInput

{ + provider: P + workspaceId?: string + organizationId?: string + credentialId?: string + /** False when the dialog cannot offer keyless at all (a Google Workspace service). */ + enabled?: boolean +} + +/** + * Whether this dialog can offer a keyless mode, and the trust setup to show. + * + * Keyless subjects bind to a workspace, so organization credentials never + * offer it, and a deployment without a signing key reports it unavailable. On + * reconnect the configured trust is kept by default: issuing a new subject + * would break the role or federation policy the customer already set up. + */ +export function useKeylessSetup

({ + provider, + workspaceId, + organizationId, + credentialId, + enabled = true, +}: KeylessSetupInput

) { + const [sessionKey] = useState(() => generateShortId()) + const [subjectChoice, setSubjectChoice] = useState( + credentialId ? 'keep' : 'new' + ) + const workspaceScoped = enabled && Boolean(workspaceId) && !organizationId + const setup = useWorkloadIdentitySetup({ + workspaceId, + provider, + sessionKey, + enabled: workspaceScoped, + credentialId, + }) + const trust = setup.data?.available ? setup.data : undefined + const storedAuthType = providerAuthType(provider, setup.data?.reconnectAuthType) + // A keyless credential reopens on keyless only where this deployment can still sign. + const reconnectAuthType = + storedAuthType === WORKLOAD_IDENTITY_KEYLESS_CONFIGS[provider].authType && !trust + ? undefined + : storedAuthType + return { + /** Undefined while this deployment's support is still loading. */ + available: workspaceScoped ? setup.data?.available : false, + /** The stored mode of the credential being reconnected; a reconnect reopens on it. */ + reconnectAuthType, + isReconnect: Boolean(credentialId), + subjectChoice, + setSubjectChoice, + /** The new subject to submit, or undefined when keeping the stored one. */ + trust: subjectChoice === 'new' ? trust : undefined, + /** Ready to submit a keyless configuration. */ + ready: subjectChoice === 'keep' ? Boolean(credentialId) : Boolean(trust), + error: setup.isError ? setup.error : null, + } +} + +export type KeylessSetup

= + ReturnType> + +function providerAuthType

( + provider: P, + value: string | undefined +): WorkloadIdentityProviderAuthType

| undefined { + const modes: readonly WorkloadIdentityProviderAuthType

[] = + WORKLOAD_IDENTITY_PROVIDER_AUTH_TYPES[provider] + return modes.find((mode) => mode === value) +} + +/** The subject fields of a keyless config: a new subject with its proof, or none to keep the stored one. */ +export function keylessSubjectFields(setup: KeylessSetup) { + return setup.trust ? { subject: setup.trust.subject, subjectProof: setup.trust.subjectProof } : {} +} + +interface KeylessTrustFieldsProps { + setup: KeylessSetup + /** The credential's own audience, for a provider without a fixed one (Google). */ + audience?: string + /** Where in the provider's console the values below are pasted. */ + pasteHint?: string +} + +/** Reconnect choice plus the issuer, audience, and subject the customer configures. */ +export function KeylessTrustFields({ + setup, + audience = setup.trust?.audience, + pasteHint, +}: KeylessTrustFieldsProps) { + return ( + <> + {setup.isReconnect && ( + setup.setSubjectChoice(value as TrustSubjectChoice)} + options={[ + { value: 'keep', label: 'Keep the subject this credential already uses' }, + { value: 'new', label: 'Issue a new subject' }, + ]} + hint={ + setup.subjectChoice === 'keep' + ? 'Your existing trust configuration keeps working.' + : 'You must update your trust configuration to the new subject before saving.' + } + /> + )} + {setup.error && ( + + {isApiClientError(setup.error) && setup.error.message + ? setup.error.message + : 'Could not start a keyless connection.'} + + )} + {setup.trust && ( + <> + + {audience && ( + + )} + + + )} + + ) +} + +interface SavedConnectionSubmitInput { + providerId: string + workspaceId?: string + organizationId?: string + credentialId?: string + onCreated?: (credentialId: string) => void + onClose: () => void + /** + * Copy for a shared verification code (`invalid_credentials`, …) the + * provider reports by code rather than with its own message; undefined + * keeps the server's message. + */ + messageForCode?: (code: string) => string | undefined +} + +/** + * Creates or reconnects a service-account credential from its JSON + * configuration. Verification failures surface the server's own message — for + * these providers it names what to fix (the trust policy, the role, the host). + */ +export function useSavedConnectionSubmit({ + providerId, + workspaceId, + organizationId, + credentialId, + onCreated, + onClose, + messageForCode, +}: SavedConnectionSubmitInput) { + const [error, setError] = useState(null) + const createCredential = useCreateScopedCredential() + const updateCredential = useUpdateScopedCredential() + + const submit = async (input: { + config: Record + displayName: string + description: string + }) => { + setError(null) + const fields = { + ...resourceScopeFields(resourceScopeFromOwner({ workspaceId, organizationId })), + serviceAccountJson: JSON.stringify(input.config), + displayName: input.displayName.trim() || undefined, + } + try { + let connectedId = credentialId + if (credentialId) { + await updateCredential.mutateAsync({ + ...fields, + credentialId, + description: input.description.trim() || null, + }) + } else { + const created = await createCredential.mutateAsync({ + ...fields, + description: input.description.trim() || undefined, + type: 'service_account', + providerId, + }) + connectedId = created.credential.id + } + if (connectedId) onCreated?.(connectedId) + onClose() + } catch (submitError) { + if (isApiClientError(submitError) && submitError.code === 'duplicate_display_name') { + setError('A credential with that name already exists. Choose a different display name.') + return + } + const codeMessage = + isApiClientError(submitError) && submitError.code + ? messageForCode?.(submitError.code) + : undefined + setError( + codeMessage ?? + (isApiClientError(submitError) && submitError.message + ? submitError.message + : 'Could not verify the credential. Check the details and try again.') + ) + } + } + + return { + submit, + error, + clearError: () => setError(null), + isPending: createCredential.isPending || updateCredential.isPending, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/microsoft-app-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/microsoft-app-account-modal.tsx new file mode 100644 index 00000000000..52df79fa08b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/microsoft-app-account-modal.tsx @@ -0,0 +1,268 @@ +'use client' + +import { type ComponentType, useState } from 'react' +import { + ChipModal, + ChipModalBody, + ChipModalError, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + SecretInput, +} from '@sim/emcn' +import { resourceScopeFromOwner, resourceScopeKey } from '@/lib/core/resource-scope' +import { MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/oauth/types' +import { + KeylessTrustFields, + keylessSubjectFields, + useKeylessSetup, + useSavedConnectionSubmit, +} from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/keyless-connection' +import { withBrandIcon } from '@/blocks/brand-icon' + +interface MicrosoftAppAccountModalProps { + open: boolean + onOpenChange: (open: boolean) => void + workspaceId?: string + organizationId?: string + serviceIcon: ComponentType<{ className?: string }> + credentialId?: string + initialDisplayName?: string + initialDescription?: string + onCreated?: (credentialId: string) => void +} + +type MicrosoftAuthType = 'federated' | 'client_secret' | 'certificate' + +const AUTH_TYPE_HINTS: Record = { + federated: + 'No secret is stored. Sim signs a short-lived token a federated credential on your app trusts.', + client_secret: 'Sim exchanges the client secret for a short-lived token on each run.', + certificate: 'Sim signs each token request with the certificate’s private key.', +} + +const DOCS_URL = 'https://docs.sim.ai/integrations/microsoft-app-credentials' + +/** Saved Microsoft app credentials share the standard credential create/reconnect lifecycle. */ +export function MicrosoftAppAccountModal(props: MicrosoftAppAccountModalProps) { + if (!props.open) return null + return ( + + ) +} + +function MicrosoftAppAccountForm({ + open, + onOpenChange, + workspaceId, + organizationId, + serviceIcon: ServiceIcon, + credentialId, + initialDisplayName, + initialDescription, + onCreated, +}: MicrosoftAppAccountModalProps) { + const keyless = useKeylessSetup({ + provider: 'microsoft', + workspaceId, + organizationId, + credentialId, + }) + const { submit, error, clearError, isPending } = useSavedConnectionSubmit({ + providerId: MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, + workspaceId, + organizationId, + credentialId, + onCreated, + onClose: () => onOpenChange(false), + }) + const [selectedAuthType, setSelectedAuthType] = useState(null) + const [tenantId, setTenantId] = useState('') + const [clientId, setClientId] = useState('') + const [environmentUrl, setEnvironmentUrl] = useState('') + const [clientSecret, setClientSecret] = useState('') + const [privateKey, setPrivateKey] = useState('') + const [certificate, setCertificate] = useState('') + const [displayName, setDisplayName] = useState(initialDisplayName ?? '') + const [description, setDescription] = useState(initialDescription ?? '') + + // A reconnect opens on the credential's stored mode, so it never switches auth by default; + // a new connection defaults to keyless wherever this deployment supports it. + const defaultAuthType: MicrosoftAuthType = credentialId + ? (keyless.reconnectAuthType ?? 'client_secret') + : keyless.available + ? 'federated' + : 'client_secret' + const authType: MicrosoftAuthType = selectedAuthType ?? defaultAuthType + const disabled = + isPending || + !tenantId.trim() || + !clientId.trim() || + (authType === 'client_secret' && !clientSecret.trim()) || + (authType === 'certificate' && (!privateKey.trim() || !certificate.trim())) || + (authType === 'federated' && !keyless.ready) + + const edit = (setter: (value: string) => void) => (value: string) => { + setter(value) + clearError() + } + + const buildConfig = (): Record => { + const base = { + authType, + tenantId: tenantId.trim(), + clientId: clientId.trim(), + ...(environmentUrl.trim() ? { environmentUrl: environmentUrl.trim() } : {}), + } + if (authType === 'federated') return { ...base, ...keylessSubjectFields(keyless) } + if (authType === 'certificate') return { ...base, privateKey, certificate } + return { ...base, clientSecret } + } + + const action = credentialId ? 'Reconnect' : 'Add' + const authOptions = [ + ...(keyless.available + ? [{ value: 'federated', label: 'Federated credential (keyless, via Sim OIDC)' }] + : []), + { value: 'client_secret', label: 'Client secret' }, + { value: 'certificate', label: 'Certificate' }, + ] + + return ( + + onOpenChange(false)}> + {action} Microsoft app credential + + + { + setSelectedAuthType(value as MicrosoftAuthType) + clearError() + }} + options={authOptions} + placeholder='Choose how the app signs in' + required + hint={AUTH_TYPE_HINTS[authType]} + /> + + + + + {authType === 'client_secret' && ( + + {(aria) => ( + + )} + + )} + + {authType === 'certificate' && ( + <> + + + + )} + + {authType === 'federated' && ( + + )} + + + + {error} + + onOpenChange(false)} + secondaryActions={[ + { + label: 'Setup guide', + onClick: () => window.open(DOCS_URL, '_blank', 'noopener,noreferrer'), + }, + ]} + primaryAction={{ + label: isPending ? 'Verifying...' : `${action} credential`, + onClick: () => { + if (!disabled) void submit({ config: buildConfig(), displayName, description }) + }, + disabled, + }} + /> + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/snowflake-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/snowflake-account-modal.tsx new file mode 100644 index 00000000000..7bba14c85c6 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/snowflake-account-modal.tsx @@ -0,0 +1,349 @@ +'use client' + +import { type ComponentType, useState } from 'react' +import { + ChipModal, + ChipModalBody, + ChipModalError, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + ChipTextarea, + SecretInput, +} from '@sim/emcn' +import { isApiClientError } from '@/lib/api/client/errors' +import { + resourceScopeFields, + resourceScopeFromOwner, + resourceScopeKey, +} from '@/lib/core/resource-scope' +import { + getTokenServiceAccountDescriptor, + getTokenServiceAccountErrorMessage, + SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, +} from '@/lib/credentials/token-service-accounts/descriptors' +import { + type KeylessSetup, + KeylessTrustFields, + keylessSubjectFields, + useKeylessSetup, + useSavedConnectionSubmit, +} from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/keyless-connection' +import { TokenServiceAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal' +import { withBrandIcon } from '@/blocks/brand-icon' +import { + useCreateScopedCredential, + useUpdateScopedCredential, +} from '@/hooks/queries/scoped-credentials' + +interface SnowflakeAccountModalProps { + open: boolean + onOpenChange: (open: boolean) => void + workspaceId?: string + organizationId?: string + serviceName: string + serviceIcon: ComponentType<{ className?: string }> + credentialId?: string + initialDisplayName?: string + initialDescription?: string + onCreated?: (credentialId: string) => void +} + +type SnowflakeAuthType = 'workload_identity' | 'programmatic_access_token' + +const SNOWFLAKE_DESCRIPTOR = getTokenServiceAccountDescriptor(SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID) +const SNOWFLAKE_DOCS_URL = 'https://docs.sim.ai/integrations/snowflake-service-account' +const HOST_PATTERN = /^[a-z0-9][a-z0-9.-]*\.snowflakecomputing\.(com|cn)$/i + +const AUTH_TYPE_HINTS: Record = { + workload_identity: + 'No secret is stored. Sim signs a short-lived token your Snowflake service user trusts.', + programmatic_access_token: 'Sim sends this token on every request.', +} + +/** Copy for the shared verification codes a keyless save reports. */ +function keylessErrorMessage(code: string): string | undefined { + switch (code) { + case 'invalid_credentials': + return 'Snowflake did not accept Sim’s token. Check that a service user on this account was created with exactly the issuer and subject shown, that a network policy allows Sim to reach the account, and that no authentication policy blocks workload identity.' + case 'site_not_found': + return "We couldn't find a Snowflake account at that host. It should look like myorg-myaccount.snowflakecomputing.com." + case 'provider_unavailable': + return "We couldn't reach Snowflake to verify this connection. Try again in a moment." + default: + return undefined + } +} + +/** The statement that creates a service user trusting only this credential's subject. */ +function buildCreateUserStatement(issuer: string, subject: string) { + return [ + 'CREATE USER SIM_WORKFLOWS', + ' WORKLOAD_IDENTITY = (', + ' TYPE = OIDC', + ` ISSUER = '${issuer}'`, + ` SUBJECT = '${subject}'`, + ' )', + ' TYPE = SERVICE;', + ].join('\n') +} + +function normalizeHostInput(raw: string): string { + return raw + .trim() + .replace(/^https?:\/\//i, '') + .replace(/[/?#].*$/, '') +} + +/** + * Snowflake connections: a programmatic access token, or keyless workload + * identity where this deployment can sign tokens. Where it cannot (or for an + * organization credential, which keyless never binds to), the standard + * token dialog is shown unchanged. + */ +export function SnowflakeAccountModal(props: SnowflakeAccountModalProps) { + if (!props.open) return null + return ( + + ) +} + +function SnowflakeAccountGate(props: SnowflakeAccountModalProps) { + const keyless = useKeylessSetup({ + provider: 'snowflake', + workspaceId: props.workspaceId, + organizationId: props.organizationId, + credentialId: props.credentialId, + }) + if (keyless.available === false && SNOWFLAKE_DESCRIPTOR) { + return + } + return +} + +function SnowflakeAccountForm({ + open, + onOpenChange, + workspaceId, + organizationId, + serviceIcon: ServiceIcon, + credentialId, + initialDisplayName, + initialDescription, + onCreated, + keyless, +}: SnowflakeAccountModalProps & { keyless: KeylessSetup<'snowflake'> }) { + const close = () => onOpenChange(false) + const keylessSubmit = useSavedConnectionSubmit({ + providerId: SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, + workspaceId, + organizationId, + credentialId, + onCreated, + onClose: close, + messageForCode: keylessErrorMessage, + }) + const createCredential = useCreateScopedCredential() + const updateCredential = useUpdateScopedCredential() + const [tokenError, setTokenError] = useState(null) + const [selectedAuthType, setSelectedAuthType] = useState(null) + const [host, setHost] = useState('') + const [token, setToken] = useState('') + const [displayName, setDisplayName] = useState(initialDisplayName ?? '') + const [description, setDescription] = useState(initialDescription ?? '') + + // A reconnect opens on the credential's stored mode, so it never switches auth by default; + // a new connection defaults to keyless wherever this deployment supports it. + const defaultAuthType: SnowflakeAuthType = credentialId + ? (keyless.reconnectAuthType ?? 'programmatic_access_token') + : keyless.available + ? 'workload_identity' + : 'programmatic_access_token' + const authType: SnowflakeAuthType = selectedAuthType ?? defaultAuthType + const normalizedHost = normalizeHostInput(host) + const isPending = + keylessSubmit.isPending || createCredential.isPending || updateCredential.isPending + const disabled = + isPending || + !normalizedHost || + (authType === 'programmatic_access_token' && !token.trim()) || + (authType === 'workload_identity' && !keyless.ready) + const error = authType === 'workload_identity' ? keylessSubmit.error : tokenError + + const clearErrors = () => { + keylessSubmit.clearError() + setTokenError(null) + } + const edit = (setter: (value: string) => void) => (value: string) => { + setter(value) + clearErrors() + } + + const submitToken = async () => { + const fields = { + ...resourceScopeFields(resourceScopeFromOwner({ workspaceId, organizationId })), + apiToken: token.trim(), + domain: normalizedHost, + displayName: displayName.trim() || undefined, + description: description.trim() || undefined, + } + try { + if (credentialId) { + await updateCredential.mutateAsync({ ...fields, credentialId }) + onCreated?.(credentialId) + } else { + const created = await createCredential.mutateAsync({ + ...fields, + type: 'service_account', + providerId: SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, + }) + onCreated?.(created.credential.id) + } + close() + } catch (submitError) { + const code = isApiClientError(submitError) ? submitError.code : undefined + setTokenError( + SNOWFLAKE_DESCRIPTOR + ? getTokenServiceAccountErrorMessage(SNOWFLAKE_DESCRIPTOR, code) + : 'Could not verify the credential. Check the details and try again.' + ) + } + } + + const handleSubmit = () => { + if (disabled) return + clearErrors() + if (authType === 'programmatic_access_token') { + void submitToken() + return + } + void keylessSubmit.submit({ + config: { + authType: 'workload_identity', + host: normalizedHost, + ...keylessSubjectFields(keyless), + }, + displayName, + description, + }) + } + + const createUserStatement = + authType === 'workload_identity' && keyless.trust + ? buildCreateUserStatement(keyless.trust.issuer, keyless.trust.subject) + : null + const action = credentialId ? 'Reconnect' : 'Add' + const authOptions = [ + ...(keyless.available + ? [{ value: 'workload_identity', label: 'Service user (keyless, via Sim OIDC)' }] + : []), + { value: 'programmatic_access_token', label: 'Programmatic access token' }, + ] + + return ( + + + {action} Snowflake connection + + + { + setSelectedAuthType(value as SnowflakeAuthType) + clearErrors() + }} + options={authOptions} + placeholder='Choose how Sim signs in to Snowflake' + required + hint={AUTH_TYPE_HINTS[authType]} + /> + + + {authType === 'programmatic_access_token' && ( + + {(aria) => ( + + )} + + )} + + {authType === 'workload_identity' && } + + {createUserStatement && ( + + {(aria) => ( + + )} + + )} + + + + {error} + + window.open(SNOWFLAKE_DOCS_URL, '_blank', 'noopener,noreferrer'), + }, + ]} + primaryAction={{ + label: isPending ? 'Verifying...' : `${action} connection`, + onClick: handleSubmit, + disabled, + }} + /> + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal.tsx index 590b0e19bc3..4822a473ae8 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal.tsx @@ -110,7 +110,7 @@ function TokenServiceAccountModalForm({ const workspaceField = descriptor.fields.find((field) => field.id === 'providerWorkspaceId') const trimmedToken = apiToken.trim() - const normalizedDomain = normalizeDomainInput(domain) + const normalizedDomain = domainField?.url ? domain.trim() : normalizeDomainInput(domain) const isPending = createCredential.isPending || updateCredential.isPending || submitPending === true const isDisabled = diff --git a/apps/sim/app/workspace/[workspaceId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx index fb9ac90703f..bac856a0651 100644 --- a/apps/sim/app/workspace/[workspaceId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx @@ -4,6 +4,7 @@ import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider' import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces' +import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag' import { getSession } from '@/lib/auth' import { getActiveOrganizationId } from '@/lib/auth/session-response' import { isChangelogEnabled } from '@/lib/changelog/feature-flag' @@ -137,6 +138,7 @@ async function WorkspaceContent({ computerUseEnabled, projectsEnabled, simLinkOrganizationId, + readOnlyApiKeysEnabled, ] = await Promise.all([ cookies(), orgSettings, @@ -157,6 +159,7 @@ async function WorkspaceContent({ isComputerUseAvailable(hostContext.hostOrganizationId), isProjectsEnabled(), resolveSimLinkOrganization(principal), + isReadOnlyApiKeyCreationEnabled(), prefetchWorkspaceAccess(queryClient, workspaceId, principal), prefetchWorkspaceForkAvailability(queryClient, workspaceId, principal, hostContext), ]) @@ -173,6 +176,7 @@ async function WorkspaceContent({ 'mothership-computer-use': computerUseEnabled, 'mothership-plan-mode': planModeEnabled, projects: projectsEnabled, + 'api-key-read-only': readOnlyApiKeysEnabled, }} > diff --git a/apps/sim/app/workspace/[workspaceId]/providers/feature-flags-provider.tsx b/apps/sim/app/workspace/[workspaceId]/providers/feature-flags-provider.tsx index f33fd8f230f..083d412a7e4 100644 --- a/apps/sim/app/workspace/[workspaceId]/providers/feature-flags-provider.tsx +++ b/apps/sim/app/workspace/[workspaceId]/providers/feature-flags-provider.tsx @@ -11,6 +11,7 @@ export interface WorkspaceFeatureFlags { 'mothership-computer-use'?: boolean 'mothership-memory-spaces'?: boolean projects?: boolean + 'api-key-read-only'?: boolean } const FeatureFlagsContext = createContext(null) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx index 88a2a9afe14..6e3119b7230 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx @@ -1,7 +1,7 @@ 'use client' import { useMemo, useState } from 'react' -import { Chip, ChipConfirmModal, Label, Switch, Tooltip, toast } from '@sim/emcn' +import { Chip, ChipConfirmModal, ChipTag, Label, Switch, Tooltip, toast } from '@sim/emcn' import { CircleInfo, Plus } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' @@ -49,6 +49,31 @@ function formatLastUsed(dateString?: string | null): string { return formatDate(new Date(dateString)) } +interface ApiKeyRowBadgeProps { + apiKey: ApiKey +} + +/** The row caption: last use and expiry, after a tag when the key is read-only. */ +function ApiKeyRowBadge({ apiKey }: ApiKeyRowBadgeProps) { + const parts = [`last used ${formatLastUsed(apiKey.lastUsed).toLowerCase()}`] + if (apiKey.expiresAt) { + const expiresAt = new Date(apiKey.expiresAt) + parts.push( + expiresAt.getTime() <= Date.now() + ? 'expired' + : `expires ${formatDate(expiresAt).toLowerCase()}` + ) + } + return ( + + {apiKey.access === 'read' && Read only} + + {parts.join(' · ')} + + + ) +} + interface ApiKeyRowMenuProps { keyName: string onDelete: () => void @@ -265,11 +290,7 @@ export function ApiKeys({ scope = 'workspace' }: ApiKeysProps) { key={key.id} title={key.name} description={key.displayKey} - badge={ - - {`last used ${formatLastUsed(key.lastUsed).toLowerCase()}`} - - } + badge={} trailing={ - {`last used ${formatLastUsed(key.lastUsed).toLowerCase()}`} - - } + badge={} trailing={ - {`last used ${formatLastUsed(key.lastUsed).toLowerCase()}`} - - } + badge={} trailing={ void @@ -27,6 +44,8 @@ interface CreateApiKeyModalProps { canManageWorkspaceKeys?: boolean defaultKeyType?: 'personal' | 'workspace' source?: 'settings' | 'deploy_modal' + /** For a key created to run workflows: hides the access choice and always issues full access. */ + fullAccessOnly?: boolean onKeyCreated?: (key: CreatedApiKey) => void } @@ -45,14 +64,19 @@ export function CreateApiKeyModal({ canManageWorkspaceKeys = false, defaultKeyType = 'personal', source = 'settings', + fullAccessOnly = false, onKeyCreated, }: CreateApiKeyModalProps) { const [keyName, setKeyName] = useState('') const [keyType, setKeyType] = useState<'personal' | 'workspace'>(defaultKeyType) + const [access, setAccess] = useState('write') + const [expiry, setExpiry] = useState(DEFAULT_EXPIRY) const [createError, setCreateError] = useState(null) const [newKey, setNewKey] = useState(null) const [showNewKeyDialog, setShowNewKeyDialog] = useState(false) const createApiKeyMutation = useCreateApiKey() + const readOnlyKeysEnabled = useFeatureFlag('api-key-read-only') + const canChooseAccess = !fullAccessOnly && readOnlyKeysEnabled /** * The form is seeded when the modal OPENS, not when this component mounts. @@ -73,6 +97,8 @@ export function CreateApiKeyModal({ if (open) { setKeyName('') setKeyType(defaultKeyType) + setAccess('write') + setExpiry(DEFAULT_EXPIRY) setCreateError(null) } } @@ -99,15 +125,16 @@ export function CreateApiKeyModal({ setCreateError(null) try { if (keyType === 'workspace' && !workspaceId) return + const settings = { + name: trimmedName, + source, + access: canChooseAccess ? access : 'write', + expiresAt: expiresAtFor(expiry), + } as const const data = await createApiKeyMutation.mutateAsync( keyType === 'workspace' && workspaceId - ? { - workspaceId, - name: trimmedName, - keyType, - source, - } - : { keyType: 'personal', name: trimmedName, source } + ? { ...settings, workspaceId, keyType } + : { ...settings, keyType: 'personal' } ) setNewKey(data.key) @@ -169,6 +196,25 @@ export function CreateApiKeyModal({ autoComplete='off' required /> + {canChooseAccess && ( + + setAccess(value as ApiKeyAccessLevel)} + > + Full access + Read only + + + )} + setExpiry(value as ExpiryOption)} + options={EXPIRY_OPTIONS} + align='start' + /> {/* Hidden decoy fields to prevent browser autofill */} )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx index 37e61f5fcb4..7b3d143108a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx @@ -738,6 +738,7 @@ export function DeployModal({ canManageWorkspaceKeys={canManageWorkspaceKeys} defaultKeyType={defaultKeyType} source='deploy_modal' + fullAccessOnly /> {workflowId && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index 5d65eb013a5..3061fb6958f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -563,6 +563,7 @@ export function CredentialSelector({ } serviceName={serviceAccountTarget.serviceName} serviceIcon={serviceAccountTarget.serviceIcon} + oauthServiceId={serviceId} onCreated={(newCredentialId) => { setStoreValue(newCredentialId) refetchCredentials() diff --git a/apps/sim/background/async-preprocessing-correlation.test.ts b/apps/sim/background/async-preprocessing-correlation.test.ts index a69f018bc3b..74f64da1bd8 100644 --- a/apps/sim/background/async-preprocessing-correlation.test.ts +++ b/apps/sim/background/async-preprocessing-correlation.test.ts @@ -1,3 +1,4 @@ +import { parsePrincipal, serializePrincipal } from '@sim/auth/principal' import { billingUsageReservationMock, billingUsageReservationMockFns, @@ -207,10 +208,9 @@ describe('async preprocessing correlation threading', () => { it.each([ { name: 'workspace API key', - serializedPrincipal: { - version: 1 as const, - principal: createWorkspaceApiKeyPrincipal({ keyId: 'workspace-key-1' }), - }, + serializedPrincipal: serializePrincipal( + createWorkspaceApiKeyPrincipal({ keyId: 'workspace-key-1' }) + ), isPublicApiAccess: false, }, { @@ -262,7 +262,7 @@ describe('async preprocessing correlation threading', () => { const executionMetadata = mockExecutionSnapshot.mock.calls[0]?.[0] expect(executionMetadata.userId).toBe('actor-1') - expect(executionMetadata.principal).toEqual(serializedPrincipal.principal) + expect(executionMetadata.principal).toEqual(parsePrincipal(serializedPrincipal)) expect(executionMetadata.isPublicApiAccess).toBe(isPublicApiAccess) expect(executionMetadata.principal).not.toHaveProperty('userId') } diff --git a/apps/sim/background/deployed-run-variables.integration.ts b/apps/sim/background/deployed-run-variables.integration.ts new file mode 100644 index 00000000000..1875397aee3 --- /dev/null +++ b/apps/sim/background/deployed-run-variables.integration.ts @@ -0,0 +1,287 @@ +/** + * Scheduled and webhook runs against real PostgreSQL: each executes its deployed version with + * the variables that version was deployed with, not the editor's saved ones, except for a + * version deployed before deployments snapshotted variables, which keeps the saved ones. + */ +import { serializePrincipal } from '@sim/auth/principal' +import { db } from '@sim/db' +import { + permissions, + user, + webhook, + workflow, + workflowDeploymentVersion, + workflowExecutionLogs, + workflowExecutionSnapshots, + workflowSchedule, + workspace, +} from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' +import { generateId } from '@sim/utils/id' +import { eq, inArray } from 'drizzle-orm' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +import { resolveBillingAttribution } from '@/lib/billing/core/attribution' +import { createWebhookExecutionPrincipal } from '@/lib/webhooks/execution-principal' +import { getWorkflowExecutionStatus } from '@/lib/workflows/executor/execution-status' +import { executeScheduleJob } from '@/background/schedule-execution' +import { executeWebhookJob } from '@/background/webhook-execution' + +const ownerId = generateId() +const workspaceId = generateId() +const workflowIds: string[] = [] + +function variables(workflowId: string, greeting: string) { + return { + greeting: { id: 'greeting', workflowId, name: 'greeting', type: 'plain', value: greeting }, + } +} + +const SCHEDULE_TRIGGER = { + id: 'trigger', + type: 'schedule', + name: 'Schedule', + enabled: true, + position: { x: 0, y: 0 }, + outputs: {}, + subBlocks: { + scheduleType: { id: 'scheduleType', type: 'dropdown', value: 'custom' }, + cronExpression: { id: 'cronExpression', type: 'short-input', value: '0 0 * * *' }, + }, +} + +const WEBHOOK_TRIGGER = { + id: 'trigger', + type: 'generic_webhook', + name: 'Webhook', + enabled: true, + position: { x: 0, y: 0 }, + outputs: {}, + subBlocks: {}, + triggerMode: true, +} + +/** Trigger into a Response echoing the `greeting` variable; `variables` omitted for a legacy snapshot. */ +function deployedState( + trigger: typeof SCHEDULE_TRIGGER | typeof WEBHOOK_TRIGGER, + snapshotVariables: Record | undefined +) { + return { + blocks: { + trigger, + respond: { + id: 'respond', + type: 'response', + name: 'Respond', + enabled: true, + position: { x: 300, y: 0 }, + outputs: {}, + subBlocks: { + dataMode: { id: 'dataMode', type: 'dropdown', value: 'json' }, + data: { id: 'data', type: 'code', value: '{"greeting": ""}' }, + status: { id: 'status', type: 'short-input', value: '200' }, + }, + }, + }, + edges: [{ id: 'trigger-respond', source: 'trigger', target: 'respond' }], + loops: {}, + parallels: {}, + ...(snapshotVariables ? { variables: snapshotVariables } : {}), + } +} + +/** + * A workflow whose saved variables differ from the deployed ones. With `superseded`, the + * returned version is no longer active: a later one with its own variables is. + */ +async function deployWorkflow( + trigger: typeof SCHEDULE_TRIGGER | typeof WEBHOOK_TRIGGER, + { snapshotsVariables, superseded = false }: { snapshotsVariables: boolean; superseded?: boolean } +) { + const workflowId = generateId() + const versionId = generateId() + workflowIds.push(workflowId) + const now = new Date() + await db.insert(workflow).values({ + id: workflowId, + userId: ownerId, + workspaceId, + name: `Deployed variables fixture ${workflowId}`, + isDeployed: true, + deployedAt: now, + variables: variables(workflowId, 'saved'), + lastSynced: now, + createdAt: now, + updatedAt: now, + }) + await db.insert(workflowDeploymentVersion).values({ + id: versionId, + workflowId, + version: 1, + state: deployedState( + trigger, + snapshotsVariables ? variables(workflowId, 'deployed') : undefined + ), + isActive: !superseded, + }) + if (superseded) { + await db.insert(workflowDeploymentVersion).values({ + id: generateId(), + workflowId, + version: 2, + state: deployedState(trigger, variables(workflowId, 'active')), + isActive: true, + }) + } + return { workflowId, versionId } +} + +async function settledOutput(workflowId: string, executionId: string) { + return vi.waitFor( + async () => { + const status = await getWorkflowExecutionStatus({ + workflowId, + executionId, + includeOutput: true, + selectedOutputs: [], + workspaceId, + viewerUserId: ownerId, + }) + if (status?.status !== 'completed') { + throw new Error(`Run ${executionId} has not completed: ${status?.status} ${status?.error}`) + } + return status.finalOutput + }, + { timeout: 20_000, interval: 250 } + ) +} + +async function runSchedule(workflowId: string, versionId: string) { + const scheduleId = generateId() + const executionId = generateId() + const now = new Date() + await db.insert(workflowSchedule).values({ + id: scheduleId, + workflowId, + deploymentVersionId: versionId, + blockId: 'trigger', + cronExpression: '0 0 * * *', + triggerType: 'schedule', + lastQueuedAt: now, + createdAt: now, + updatedAt: now, + }) + await executeScheduleJob({ + scheduleId, + workflowId, + workspaceId, + billingAttribution: await resolveBillingAttribution({ actorUserId: ownerId, workspaceId }), + executionId, + blockId: 'trigger', + deploymentVersionId: versionId, + cronExpression: '0 0 * * *', + now: now.toISOString(), + }) + return settledOutput(workflowId, executionId) +} + +async function runWebhook(workflowId: string, versionId: string) { + const webhookId = generateId() + const executionId = generateId() + await db.insert(webhook).values({ + id: webhookId, + workflowId, + deploymentVersionId: versionId, + blockId: 'trigger', + path: `deployed-variables-${webhookId}`, + provider: 'generic', + providerConfig: {}, + }) + await executeWebhookJob({ + webhookId, + workflowId, + principal: serializePrincipal( + createWebhookExecutionPrincipal({ webhookId, workflowId, workspaceId, provider: 'generic' }) + ), + userId: ownerId, + billingAttribution: await resolveBillingAttribution({ actorUserId: ownerId, workspaceId }), + executionId, + provider: 'generic', + body: { event: 'ping' }, + headers: {}, + method: 'POST', + path: `deployed-variables-${webhookId}`, + blockId: 'trigger', + deploymentVersionId: versionId, + workspaceId, + }) + return settledOutput(workflowId, executionId) +} + +beforeAll(async () => { + const now = new Date() + await db.insert(user).values({ + id: ownerId, + name: 'Deployed variables fixture', + email: `${ownerId}@deployed-variables.test`, + emailVerified: true, + createdAt: now, + updatedAt: now, + }) + await insertWorkspaceFixture(db, { + id: workspaceId, + name: 'Deployed variables fixture', + ownerId, + billedAccountUserId: ownerId, + }) + await db.insert(permissions).values({ + id: generateId(), + userId: ownerId, + entityType: 'workspace', + entityId: workspaceId, + permissionType: 'admin', + }) +}) + +afterAll(async () => { + if (workflowIds.length > 0) { + await db + .delete(workflowExecutionLogs) + .where(inArray(workflowExecutionLogs.workflowId, workflowIds)) + await db + .delete(workflowExecutionSnapshots) + .where(inArray(workflowExecutionSnapshots.workflowId, workflowIds)) + } + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) + await db.delete(user).where(eq(user.id, ownerId)) + await db.$client.end() +}) + +describe('variables a queued deployed run executes with', () => { + it('runs a schedule with its deployed variables, not the saved ones', async () => { + const { workflowId, versionId } = await deployWorkflow(SCHEDULE_TRIGGER, { + snapshotsVariables: true, + }) + await expect(runSchedule(workflowId, versionId)).resolves.toMatchObject({ + data: { greeting: 'deployed' }, + }) + }) + + it('runs a webhook delivery admitted on an earlier version with that version variables', async () => { + const { workflowId, versionId } = await deployWorkflow(WEBHOOK_TRIGGER, { + snapshotsVariables: true, + superseded: true, + }) + await expect(runWebhook(workflowId, versionId)).resolves.toMatchObject({ + data: { greeting: 'deployed' }, + }) + }) + + it('keeps the saved variables for a version deployed before variables were snapshotted', async () => { + const { workflowId, versionId } = await deployWorkflow(SCHEDULE_TRIGGER, { + snapshotsVariables: false, + }) + await expect(runSchedule(workflowId, versionId)).resolves.toMatchObject({ + data: { greeting: 'saved' }, + }) + }) +}) diff --git a/apps/sim/background/schedule-execution.ts b/apps/sim/background/schedule-execution.ts index 533d5eb0063..eeabc810762 100644 --- a/apps/sim/background/schedule-execution.ts +++ b/apps/sim/background/schedule-execution.ts @@ -37,7 +37,10 @@ import { cleanupExecutionBase64Cache } from '@/lib/uploads/utils/user-file-base6 import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core' import { handlePostExecutionPauseState } from '@/lib/workflows/executor/pause-persistence' import { isRecordedWorkflowFailure } from '@/lib/workflows/executor/recorded-failure' -import { loadDeployedWorkflowState } from '@/lib/workflows/persistence/utils' +import { + loadDeployedWorkflowState, + resolveDeployedRunVariables, +} from '@/lib/workflows/persistence/utils' import { notifyScheduleAutoDisabled } from '@/lib/workflows/schedules/disable-notifications' import type { ScheduleDisableReason } from '@/lib/workflows/schedules/disable-reasons' import { @@ -622,7 +625,7 @@ async function runWorkflowExecution({ metadata, workflowRecord, input, - workflowRecord.variables || {}, + resolveDeployedRunVariables(deployedData, workflowRecord.variables), [] ) diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index 660840b0cf2..bd4387132b7 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -84,6 +84,7 @@ import { import { loadDeployedWorkflowState, loadWorkflowDeploymentVersionState, + resolveDeployedRunVariables, } from '@/lib/workflows/persistence/utils' import { WEBHOOK_EXECUTION_CONCURRENCY_LIMIT } from '@/background/concurrency-limits' import { getBlock } from '@/blocks' @@ -867,8 +868,6 @@ async function executeWebhookJobInternal( throw new Error(`Workflow ${payload.workflowId} has no associated workspace`) } - const workflowVariables = (workflowRecord.variables as Record) || {} - let deploymentVersionId: string | undefined /** * Flipped immediately before `executeWorkflowCore` is invoked. While false, @@ -917,6 +916,7 @@ async function executeWebhookJobInternal( } const { blocks, edges, loops, parallels } = workflowData + const workflowVariables = resolveDeployedRunVariables(workflowData, workflowRecord.variables) deploymentVersionId = 'deploymentVersionId' in workflowData ? (workflowData.deploymentVersionId as string) diff --git a/apps/sim/background/workflow-execution.ts b/apps/sim/background/workflow-execution.ts index 0aa2c684e8a..567c716eef3 100644 --- a/apps/sim/background/workflow-execution.ts +++ b/apps/sim/background/workflow-execution.ts @@ -32,6 +32,7 @@ import { RESERVATION_TTL_BUFFER_MS, } from '@/lib/core/execution-limits' import type { RequestAttribution } from '@/lib/core/utils/request-attribution' +import { withDatabaseReadRetry } from '@/lib/db/read-retry' import { isWorkflowStateRejection, type PreprocessExecutionError, @@ -47,6 +48,11 @@ import { isRecordedWorkflowFailure, resolveDeletedExecutionScope, } from '@/lib/workflows/executor/recorded-failure' +import { + loadDeployedWorkflowState, + loadWorkflowDeploymentVersionState, + resolveDeployedRunVariables, +} from '@/lib/workflows/persistence/utils' import { WORKFLOW_EXECUTION_CONCURRENCY_LIMIT } from '@/background/concurrency-limits' import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { ExecutionMetadata } from '@/executor/execution/types' @@ -106,6 +112,11 @@ export type WorkflowExecutionPayload = { */ enforceCredentialAccess?: boolean isPublicApiAccess?: boolean + /** + * Deployment version validated by the enqueuing use case. Only the id rides the + * payload; jobs queued without it run the active deployment. + */ + pinnedDeploymentVersionId?: string } type LegacyWorkflowExecutionPayload = Omit & { @@ -272,6 +283,24 @@ export async function executeWorkflowJob( logger.info(`Preprocessing passed. Using actor: ${actorUserId}`, { requestId }) const workflow = preprocessResult.workflowRecord! + /** + * The version this run executes, loaded once: the core runs this same version's + * blocks, so a deploy landing mid-setup cannot pair one version's variables with + * another's graph. A pinned version runs with only its own snapshot, as a sync run + * of it does, never the editor's current variables. + */ + const pinnedDeploymentVersionId = payload.pinnedDeploymentVersionId + const deployedState = await withDatabaseReadRetry( + () => + pinnedDeploymentVersionId + ? loadWorkflowDeploymentVersionState( + workflowId, + pinnedDeploymentVersionId, + workspaceId + ) + : loadDeployedWorkflowState(workflowId, workspaceId), + { label: 'loadDeployedRunState' } + ) const metadata: ExecutionMetadata = { requestId, @@ -286,6 +315,7 @@ export async function executeWorkflowJob( triggerType: payload.triggerType || 'api', triggerBlockId: payload.triggerBlockId, useDraftState: false, + pinnedDeploymentVersionId: deployedState.deploymentVersionId, startTime: new Date().toISOString(), isClientSession: false, enforceCredentialAccess: payload.enforceCredentialAccess ?? false, @@ -299,7 +329,9 @@ export async function executeWorkflowJob( metadata, workflow, payload.input, - workflow.variables || {}, + pinnedDeploymentVersionId + ? (deployedState.variables ?? {}) + : resolveDeployedRunVariables(deployedState, workflow.variables), [] ) diff --git a/apps/sim/blocks/blocks/affinity.ts b/apps/sim/blocks/blocks/affinity.ts index 9691e670530..6bc3c72ed38 100644 --- a/apps/sim/blocks/blocks/affinity.ts +++ b/apps/sim/blocks/blocks/affinity.ts @@ -1,7 +1,11 @@ import { AffinityIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' -import { parseOptionalBooleanInput, parseOptionalNumberInput } from '@/blocks/utils' +import { + parseOptionalBooleanInput, + parseOptionalNumberInput, + withSavedCredential, +} from '@/blocks/utils' /** Operations scoped to a company or person through the shared field endpoints. */ const FIELD_ENTITY_OPERATIONS = [ @@ -305,6 +309,8 @@ function affinityToolIds(): ReadonlySet { export const AffinityBlock: BlockConfig = { type: 'affinity', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'affinity_v2' }, name: 'Affinity', description: 'Read and write companies, people, lists, notes, and relationships in Affinity', longDescription: @@ -1765,3 +1771,17 @@ export const AffinityBlockMeta = { }, ], } as const satisfies BlockMeta + +export const AffinityV2Block: BlockConfig = { + ...AffinityBlock, + type: 'affinity_v2', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + sunset: undefined, + ...withSavedCredential(AffinityBlock, { + id: 'credential', + type: 'oauth-input', + serviceId: 'affinity', + title: 'Affinity Credential', + }), +} diff --git a/apps/sim/blocks/blocks/agentmail.ts b/apps/sim/blocks/blocks/agentmail.ts index 26c9b5a99a6..db6048ac1d3 100644 --- a/apps/sim/blocks/blocks/agentmail.ts +++ b/apps/sim/blocks/blocks/agentmail.ts @@ -1,9 +1,12 @@ import { AgentMailIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { withSavedCredential } from '@/blocks/utils' export const AgentMailBlock: BlockConfig = { type: 'agentmail', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'agentmail_v2' }, name: 'AgentMail', description: 'Manage email inboxes, threads, and messages with AgentMail', longDescription: @@ -807,3 +810,19 @@ export const AgentMailBlockMeta = { }, ], } as const satisfies BlockMeta + +export const AgentMailV2Block: BlockConfig = { + ...AgentMailBlock, + type: 'agentmail_v2', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + sunset: undefined, + longDescription: + 'Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels.', + ...withSavedCredential(AgentMailBlock, { + id: 'credential', + type: 'oauth-input', + serviceId: 'agentmail', + title: 'AgentMail Credential', + }), +} diff --git a/apps/sim/blocks/blocks/agentphone.ts b/apps/sim/blocks/blocks/agentphone.ts index 0aadd385071..2212123f5dc 100644 --- a/apps/sim/blocks/blocks/agentphone.ts +++ b/apps/sim/blocks/blocks/agentphone.ts @@ -2,6 +2,7 @@ import { toError } from '@sim/utils/errors' import { AgentPhoneIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { withSavedCredential } from '@/blocks/utils' const CONVERSATION_OPS = [ 'get_conversation', @@ -26,6 +27,8 @@ const CURSOR_MESSAGE_OPS = ['get_number_messages', 'get_conversation_messages'] export const AgentPhoneBlock: BlockConfig = { type: 'agentphone', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'agentphone_v2' }, name: 'AgentPhone', description: 'Provision numbers, send SMS and iMessage, and place voice calls with AgentPhone', longDescription: @@ -933,3 +936,19 @@ export const AgentPhoneBlockMeta = { }, ], } as const satisfies BlockMeta + +export const AgentPhoneV2Block: BlockConfig = { + ...AgentPhoneBlock, + type: 'agentphone_v2', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + sunset: undefined, + longDescription: + 'Provision SMS- and voice-enabled numbers, send messages and tapback reactions, place outbound calls, manage conversations and contacts, and inspect usage.', + ...withSavedCredential(AgentPhoneBlock, { + id: 'credential', + type: 'oauth-input', + serviceId: 'agentphone', + title: 'AgentPhone Credential', + }), +} diff --git a/apps/sim/blocks/blocks/ahrefs.ts b/apps/sim/blocks/blocks/ahrefs.ts index 9cad0561fda..7ed13eea720 100644 --- a/apps/sim/blocks/blocks/ahrefs.ts +++ b/apps/sim/blocks/blocks/ahrefs.ts @@ -1,6 +1,7 @@ import { AhrefsIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { withSavedCredential } from '@/blocks/utils' const COUNTRY_OPTIONS = [ { label: 'United States', id: 'us' }, @@ -89,6 +90,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n export const AhrefsBlock: BlockConfig = { type: 'ahrefs', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'ahrefs_v2' }, name: 'Ahrefs', description: 'SEO analysis with Ahrefs', authMode: AuthMode.ApiKey, @@ -1558,3 +1561,19 @@ export const AhrefsBlockMeta = { }, ], } as const satisfies BlockMeta + +export const AhrefsV2Block: BlockConfig = { + ...AhrefsBlock, + type: 'ahrefs_v2', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + sunset: undefined, + longDescription: + 'Analyze domain ratings, backlinks, organic keywords, top pages, and more. API v3 is available on Lite and higher plans.', + ...withSavedCredential(AhrefsBlock, { + id: 'credential', + type: 'oauth-input', + serviceId: 'ahrefs', + title: 'Ahrefs Credential', + }), +} diff --git a/apps/sim/blocks/blocks/algolia.ts b/apps/sim/blocks/blocks/algolia.ts index 8844c848648..fbcbe319e9e 100644 --- a/apps/sim/blocks/blocks/algolia.ts +++ b/apps/sim/blocks/blocks/algolia.ts @@ -1,9 +1,12 @@ import { AlgoliaIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { withSavedCredential } from '@/blocks/utils' export const AlgoliaBlock: BlockConfig = { type: 'algolia', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'algolia_v2' }, name: 'Algolia', description: 'Search and manage Algolia indices', longDescription: @@ -847,3 +850,17 @@ export const AlgoliaBlockMeta = { }, ], } as const satisfies BlockMeta + +export const AlgoliaV2Block: BlockConfig = { + ...AlgoliaBlock, + type: 'algolia_v2', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + sunset: undefined, + ...withSavedCredential(AlgoliaBlock, { + id: 'credential', + type: 'oauth-input', + serviceId: 'algolia', + title: 'Algolia Credential', + }), +} diff --git a/apps/sim/blocks/blocks/apify.ts b/apps/sim/blocks/blocks/apify.ts index 6cdbaff28fb..8b3c9a74579 100644 --- a/apps/sim/blocks/blocks/apify.ts +++ b/apps/sim/blocks/blocks/apify.ts @@ -1,12 +1,15 @@ import { ApifyIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { withSavedCredential } from '@/blocks/utils' const RUN_OPERATIONS = ['apify_run_actor_sync', 'apify_run_actor_async'] const RUN_OR_TASK_OPERATIONS = [...RUN_OPERATIONS, 'apify_run_task'] export const ApifyBlock: BlockConfig = { type: 'apify', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'apify_v2' }, name: 'Apify', description: 'Run Apify actors and retrieve results', authMode: AuthMode.ApiKey, @@ -373,3 +376,17 @@ export const ApifyBlockMeta = { }, ], } as const satisfies BlockMeta + +export const ApifyV2Block: BlockConfig = { + ...ApifyBlock, + type: 'apify_v2', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + sunset: undefined, + ...withSavedCredential(ApifyBlock, { + id: 'credential', + type: 'oauth-input', + serviceId: 'apify', + title: 'Apify Credential', + }), +} diff --git a/apps/sim/blocks/blocks/apollo.ts b/apps/sim/blocks/blocks/apollo.ts index 0abf080bd29..df3cc143894 100644 --- a/apps/sim/blocks/blocks/apollo.ts +++ b/apps/sim/blocks/blocks/apollo.ts @@ -3,12 +3,15 @@ import { getErrorMessage } from '@sim/utils/errors' import { ApolloIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { withSavedCredential } from '@/blocks/utils' /** Identifies a person by email when available, otherwise by given name. */ const PERSON_IDENTITY_FIELD = ['email', 'first_name'] as const export const ApolloBlock: BlockConfig = { type: 'apollo', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'apollo_v2' }, name: 'Apollo', description: 'Search, enrich, and manage contacts with Apollo.io', authMode: AuthMode.ApiKey, @@ -1585,3 +1588,17 @@ export const ApolloBlockMeta = { }, ], } as const satisfies BlockMeta + +export const ApolloV2Block: BlockConfig = { + ...ApolloBlock, + type: 'apollo_v2', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + sunset: undefined, + ...withSavedCredential(ApolloBlock, { + id: 'credential', + type: 'oauth-input', + serviceId: 'apollo', + title: 'Apollo Credential', + }), +} diff --git a/apps/sim/blocks/blocks/appconfig.ts b/apps/sim/blocks/blocks/appconfig.ts index 4627d32bafc..9df239fa950 100644 --- a/apps/sim/blocks/blocks/appconfig.ts +++ b/apps/sim/blocks/blocks/appconfig.ts @@ -1,9 +1,12 @@ import { AppConfigIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { withSavedCredential } from '@/blocks/utils' export const AppConfigBlock: BlockConfig = { type: 'appconfig', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'appconfig_v2' }, name: 'AWS AppConfig', description: 'Manage and retrieve configuration with AWS AppConfig', longDescription: @@ -810,3 +813,19 @@ export const AppConfigBlockMeta = { }, ], } as const satisfies BlockMeta + +export const AppConfigV2Block: BlockConfig = { + ...AppConfigBlock, + type: 'appconfig_v2', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + sunset: undefined, + longDescription: + 'Manage applications, environments, and configuration profiles, create and read hosted configuration versions, run and inspect deployments, and retrieve the latest deployed configuration at runtime.', + ...withSavedCredential(AppConfigBlock, { + id: 'credential', + type: 'oauth-input', + serviceId: 'aws', + title: 'AWS Credential', + }), +} diff --git a/apps/sim/blocks/blocks/ashby.ts b/apps/sim/blocks/blocks/ashby.ts index 1aaa985d031..7c3d6af7950 100644 --- a/apps/sim/blocks/blocks/ashby.ts +++ b/apps/sim/blocks/blocks/ashby.ts @@ -1,7 +1,7 @@ import { getErrorMessage } from '@sim/utils/errors' import { AshbyIcon } from '@/components/icons' import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' -import { normalizeFileInput } from '@/blocks/utils' +import { normalizeFileInput, withSavedCredential } from '@/blocks/utils' import { getTrigger } from '@/triggers' function parseStringListInput(value: unknown): string[] { @@ -137,6 +137,8 @@ function parseJsonObjectInput(value: unknown, label: string): Record

| undefined +} = { + aws: storedAwsAuthType, + databricks: storedDatabricksAuthType, + snowflake: storedSnowflakeAuthType, + microsoft: storedMicrosoftAppAuthType, + google: (decrypted) => + isGoogleWorkloadIdentitySecret(decrypted) ? 'workload_identity' : 'json_key', +} + +/** + * The stored auth mode of a credential of this provider in this workspace, which + * this person administers. Only the mode leaves the stored secret, and only for + * someone allowed to reconnect the credential. + */ +async function storedReconnectAuthType( + provider: WorkloadIdentityProvider, + credentialId: string, + workspaceId: string, + userId: string +): Promise { + const access = await getCredentialActorContext(credentialId, userId, { workspaceId }) + const row = access.credential + if ( + !access.isAdmin || + !row || + row.workspaceId !== workspaceId || + row.type !== 'service_account' || + !isWorkloadIdentityServiceAccountProviderId(row.providerId) || + workloadIdentityProviderFor(row.providerId) !== provider || + !row.encryptedServiceAccountKey + ) { + return undefined + } + const { decrypted } = await decryptSecret(row.encryptedServiceAccountKey) + return STORED_AUTH_TYPE[provider](decrypted) +} + +/** + * Starts a keyless cloud connection (an AWS role, a Databricks service + * principal, a Snowflake service user, a Microsoft app, or a Google service account): the subject the customer's trust + * configuration will pin, signed for this person and workspace so only they can attach it to the credential + * they are about to create. Reports `available: false` when this deployment + * cannot sign tokens, so the connect dialog offers only key-based modes. On a + * reconnect it also reports the credential's stored mode, signing or not. + */ +export const prepareWorkloadIdentity = defineAuthorizedWorkspaceUseCase({ + operation: credentialOperations.prepareWorkloadIdentity, + resolveContext: async ({ input }: { input: PrepareWorkloadIdentityInput }) => { + const context = await loadActiveWorkspaceApplicationContext(input.workspaceId) + if (!context) throw new OrchestrationError('not_found', 'Workspace not found') + return context + }, + authorizationOptions: {}, + execute: async ({ principal, input }): Promise => { + const reconnectAuthType = input.credentialId + ? await storedReconnectAuthType( + input.provider, + input.credentialId, + input.workspaceId, + requirePrincipalSubjectUserId(principal) + ) + : undefined + const reconnect = reconnectAuthType ? { reconnectAuthType } : {} + // A deployment without a signing key offers only key- and secret-based credentials. + if (!(await isWorkloadIdentityConfigured())) return { available: false, ...reconnect } + const userId = requirePrincipalSubjectUserId(principal) + const grant = issueWorkloadIdentitySubject( + input.provider, + { workspaceId: input.workspaceId }, + userId + ) + const audience = fixedWorkloadIdentityAudience(input.provider) + return { + available: true, + issuer: getWorkloadIdentityIssuer(), + ...(audience ? { audience } : {}), + ...grant, + ...reconnect, + } + }, +}) diff --git a/apps/sim/lib/credentials/application/provider-catalog-contract.test.ts b/apps/sim/lib/credentials/application/provider-catalog-contract.test.ts index 85447ca97ec..af9fef25fec 100644 --- a/apps/sim/lib/credentials/application/provider-catalog-contract.test.ts +++ b/apps/sim/lib/credentials/application/provider-catalog-contract.test.ts @@ -1,3 +1,4 @@ +import { generateKeyPairSync } from 'node:crypto' import { createSessionPrincipal } from '@sim/testing/factories/principal.factory' import { blockVisibilityMock } from '@sim/testing/mocks/block-visibility.mock' import { workspaceAuthzMock, workspaceAuthzMockFns } from '@sim/testing/mocks/workspace-authz.mock' @@ -5,7 +6,7 @@ import { workspaceContextMock, workspaceContextMockFns, } from '@sim/testing/mocks/workspace-context.mock' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const hoisted = vi.hoisted(() => ({ allowedIntegrationTypes: vi.fn(), @@ -21,13 +22,24 @@ vi.mock('@/lib/integrations/principal-scope.server', () => ({ })) import { v2ListCredentialProvidersContract } from '@/lib/api/contracts/v2/credentials' +import { env } from '@/lib/core/config/env' import { listCredentialProviders } from '@/lib/credentials/application/list-credential-providers' import { listCredentialProviderCatalog } from '@/lib/credentials/application/provider-catalog' +import { getMissingServiceAccountFields } from '@/lib/credentials/service-account-fields' +import { + isWorkloadIdentityServiceAccountProviderId, + WORKLOAD_IDENTITY_KEYLESS_CONFIGS, + workloadIdentityProviderFor, +} from '@/lib/credentials/workload-identity/audiences' const CLAUDE_PROVIDER_ID = 'claude-platform-service-account' const GITHUB_INSTALLATION_PROVIDER_ID = 'github-app-installation' const context = { workspaceId: 'workspace-1', workspaceOrganizationId: null } const responseSchema = v2ListCredentialProvidersContract.response.schema +const SIGNING_KEY = generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey.export({ + type: 'pkcs8', + format: 'pem', +}) async function listProviders(search?: string) { const { providers } = await listCredentialProviders.execute({ @@ -38,6 +50,11 @@ async function listProviders(search?: string) { } describe('v2 credential provider catalog contract', () => { + afterEach(() => { + Reflect.set(env, 'WORKLOAD_IDENTITY_SIGNING_KEY', undefined) + Reflect.set(env, 'WORKLOAD_IDENTITY_ISSUER', undefined) + }) + beforeEach(() => { hoisted.allowedIntegrationTypes.mockResolvedValue(null) workspaceContextMockFns.mockLoadActiveWorkspaceApplicationContext.mockResolvedValue({ @@ -74,6 +91,69 @@ describe('v2 credential provider catalog contract', () => { expect(responseSchema.safeParse({ data: catalog, nextCursor: null }).success).toBe(false) }) + it('advertises a keyless mode the API accepts, on exactly the keyless providers', async () => { + Reflect.set(env, 'WORKLOAD_IDENTITY_SIGNING_KEY', SIGNING_KEY) + Reflect.set(env, 'WORKLOAD_IDENTITY_ISSUER', 'https://sim.example.com/oidc/workload') + const providers = await listProviders() + expect(responseSchema.safeParse({ data: providers, nextCursor: null }).success).toBe(true) + const keyless = providers.flatMap((provider) => + provider.type === 'service_account' && provider.keyless ? [provider] : [] + ) + + expect(keyless.length).toBeGreaterThan(0) + for (const provider of keyless) { + if (!isWorkloadIdentityServiceAccountProviderId(provider.providerId)) { + throw new Error( + `${provider.providerId} advertises keyless without a workload identity provider` + ) + } + expect(provider.keyless).toEqual({ + authType: + WORKLOAD_IDENTITY_KEYLESS_CONFIGS[workloadIdentityProviderFor(provider.providerId)] + .authType, + requiredConfigFields: expect.any(Array), + optionalConfigFields: expect.any(Array), + }) + expect(provider.fields.map((field) => field.id)).toContain('serviceAccountJson') + expect( + getMissingServiceAccountFields(provider.providerId, { serviceAccountJson: '{}' }) + ).toEqual([]) + } + const google = keyless.find((provider) => provider.providerId === 'google-service-account') + expect(google?.description).toContain(WORKLOAD_IDENTITY_KEYLESS_CONFIGS.google.scope) + expect(google?.fields.find((field) => field.id === 'serviceAccountJson')?.hint).toContain( + WORKLOAD_IDENTITY_KEYLESS_CONFIGS.google.scope + ) + const snowflake = keyless.find( + (provider) => provider.providerId === 'snowflake-service-account' + ) + expect(snowflake?.fields.find((field) => field.id === 'serviceAccountJson')).toMatchObject({ + required: false, + secret: true, + }) + }) + + it('offers no keyless mode where this deployment cannot sign', async () => { + const providers = await listProviders() + const keylessProviders = providers.filter( + (provider) => + provider.type === 'service_account' && + (provider.providerId === 'aws-service-account' || + provider.providerId === 'snowflake-service-account' || + provider.providerId === 'google-service-account') + ) + + for (const provider of keylessProviders) { + if (provider.type !== 'service_account') continue + expect(provider.keyless).toBeUndefined() + } + const snowflake = keylessProviders.find( + (provider) => + provider.type === 'service_account' && provider.providerId === 'snowflake-service-account' + ) + expect(snowflake?.fields.map((field) => field.id)).toEqual(['domain', 'apiToken']) + }) + it('lists the native Claude Platform provider when filtered', async () => { const providers = await listProviders('claude') diff --git a/apps/sim/lib/credentials/application/provider-catalog.ts b/apps/sim/lib/credentials/application/provider-catalog.ts index 9fdb84ab4d9..a688811e19f 100644 --- a/apps/sim/lib/credentials/application/provider-catalog.ts +++ b/apps/sim/lib/credentials/application/provider-catalog.ts @@ -9,6 +9,12 @@ import { TOKEN_SERVICE_ACCOUNT_DESCRIPTORS, type TokenServiceAccountField, } from '@/lib/credentials/token-service-accounts/descriptors' +import { + isWorkloadIdentityServiceAccountProviderId, + WORKLOAD_IDENTITY_KEYLESS_CONFIGS, + workloadIdentityProviderFor, +} from '@/lib/credentials/workload-identity/audiences' +import { isWorkloadIdentityConfigured } from '@/lib/credentials/workload-identity/issuer' import { createIntegrationCredentialVisibility } from '@/lib/integrations/credential-visibility.server' import { allowedIntegrationTypes, @@ -18,7 +24,10 @@ import { import { GITHUB_INSTALLATION_PROVIDER_ID } from '@/lib/oauth/github-installation-types' import { ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID, + AWS_SERVICE_ACCOUNT_PROVIDER_ID, + DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, + MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, type OAuthServiceMetadata, OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID, ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID, @@ -72,6 +81,14 @@ export interface ServiceAccountCredentialProviderCatalogEntry helpText?: string requiresClientGeneratedCredentialId: boolean fields: CredentialProviderField[] + /** The provider's keyless mode; absent without one, or when this deployment cannot sign. */ + keyless?: CredentialProviderKeylessMode +} + +interface CredentialProviderKeylessMode { + authType: string + requiredConfigFields: string[] + optionalConfigFields: string[] } export type CredentialProviderCatalogEntry = @@ -117,6 +134,72 @@ function providerField( } } +interface KeylessCatalogMode { + keyless: CredentialProviderKeylessMode + /** Where keyless works, when narrower than the provider. */ + scope?: string +} + +function keylessModeFor(providerId: string): KeylessCatalogMode | undefined { + if (!isWorkloadIdentityServiceAccountProviderId(providerId)) return undefined + const { authType, requiredFields, optionalFields, scope } = + WORKLOAD_IDENTITY_KEYLESS_CONFIGS[workloadIdentityProviderFor(providerId)] + return { + keyless: { + authType, + requiredConfigFields: [...requiredFields], + optionalConfigFields: [...optionalFields], + }, + ...(scope ? { scope } : {}), + } +} + +/** + * Adds a provider's keyless mode to its key-based descriptor, the same way for + * every keyless provider: one description suffix, and a hint on the + * `serviceAccountJson` field — which a token provider such as Snowflake gains + * as an optional alternative to its token fields. + */ +function withKeylessMode( + descriptor: ServiceAccountDescriptor, + mode: KeylessCatalogMode | undefined +): ServiceAccountDescriptor & { keyless?: CredentialProviderKeylessMode } { + if (!mode) return descriptor + const { keyless, scope } = mode + const scopeNote = scope ? ` ${scope}` : '' + const hint = `Keyless: send the keylessConfig from Create workload identity subject with ${keyless.requiredConfigFields.join(', ')} added.${scopeNote}` + const configurationField = descriptor.fields.find((field) => field.id === 'serviceAccountJson') + const fields = configurationField + ? descriptor.fields.map((field) => + field === configurationField + ? { ...field, hint: field.hint ? `${field.hint} ${hint}` : hint } + : field + ) + : [ + ...descriptor.fields, + { + id: 'serviceAccountJson', + label: 'Keyless configuration', + placeholder: JSON.stringify({ + authType: keyless.authType, + ...Object.fromEntries(keyless.requiredConfigFields.map((field) => [field, '...'])), + subject: '...', + subjectProof: '...', + }), + required: false, + secret: true, + multiline: true, + hint: `Send instead of the other fields. ${hint}`, + }, + ] + return { + ...descriptor, + description: `${descriptor.description} Also supports a keyless credential that trusts Sim through workload identity federation.${scopeNote}`, + fields, + keyless, + } +} + function getServiceAccountDescriptor(providerId: string): ServiceAccountDescriptor { if (providerId === GITHUB_INSTALLATION_PROVIDER_ID) { return { @@ -128,6 +211,64 @@ function getServiceAccountDescriptor(providerId: string): ServiceAccountDescript fields: [], } } + if (providerId === DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID) { + return { + name: 'Databricks connection', + description: + 'Use the Databricks block with a personal access token or a service principal OAuth secret.', + docsUrl: 'https://docs.sim.ai/integrations/databricks-credentials', + fields: [ + { + id: 'serviceAccountJson', + label: 'Connection configuration', + placeholder: + '{"authType":"personal_access_token","host":"dbc-abc123.cloud.databricks.com","token":"dapi..."}', + required: true, + secret: true, + multiline: true, + hint: `JSON object with host and authType: "personal_access_token" (token) or "service_principal" (clientId, clientSecret).`, + }, + ], + } + } + if (providerId === MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID) { + return { + name: 'Microsoft app credential', + description: + 'Use Microsoft Entra ID, SharePoint, Dataverse, and Dynamics 365 as an app registration (app-only), with a client secret or a certificate.', + docsUrl: 'https://docs.sim.ai/integrations/microsoft-app-credentials', + fields: [ + { + id: 'serviceAccountJson', + label: 'Credential configuration', + placeholder: + '{"authType":"client_secret","tenantId":"...","clientId":"...","clientSecret":"..."}', + required: true, + secret: true, + multiline: true, + hint: `JSON object with tenantId, clientId, optional environmentUrl (Dataverse and Dynamics 365), and authType: "client_secret" (clientSecret) or "certificate" (privateKey, certificate as PEM).`, + }, + ], + } + } + if (providerId === AWS_SERVICE_ACCOUNT_PROVIDER_ID) { + return { + name: 'AWS credential', + description: 'Use AWS blocks with saved access keys, or access keys that assume an IAM role.', + docsUrl: 'https://docs.sim.ai/integrations/aws-credentials', + fields: [ + { + id: 'serviceAccountJson', + label: 'Credential configuration', + placeholder: '{"authType":"access_keys","accessKeyId":"AKIA...","secretAccessKey":"..."}', + required: true, + secret: true, + multiline: true, + hint: `JSON object with authType: "access_keys" (accessKeyId, secretAccessKey, optional stsRegion) or "assume_role" (accessKeyId, secretAccessKey, roleArn, optional externalId and stsRegion).`, + }, + ], + } + } if (providerId === ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID) { return { name: 'Oracle Database connection', @@ -334,7 +475,7 @@ export async function listCredentialProviderCatalog( const userId = principalUserId(principal) const organizationId = 'organizationId' in context ? context.organizationId : context.workspaceOrganizationId - const [allowedIntegrations, blockVisibility] = await Promise.all([ + const [allowedIntegrations, blockVisibility, keylessAvailable] = await Promise.all([ 'organizationId' in context ? allowedOrganizationIntegrationTypes(context.organizationId) : allowedIntegrationTypes(principal, context.workspaceId), @@ -342,6 +483,8 @@ export async function listCredentialProviderCatalog( ...(userId ? { userId } : {}), ...(organizationId ? { orgId: organizationId } : {}), }), + // Keyless subjects are issued for workspaces only, and only where Sim can sign. + 'organizationId' in context ? false : isWorkloadIdentityConfigured(), ]) const services = getAllOAuthServices() const oauthServices = services.filter((service) => service.authType === 'oauth') @@ -406,7 +549,10 @@ export async function listCredentialProviderCatalog( const serviceAccountEntries: ServiceAccountCredentialProviderCatalogEntry[] = [ ...serviceAccountOwners, ].map(([providerId, owner]) => { - const descriptor = getServiceAccountDescriptor(providerId) + const descriptor = withKeylessMode( + getServiceAccountDescriptor(providerId), + keylessAvailable ? keylessModeFor(providerId) : undefined + ) return { type: 'service_account', serviceId: providerId, @@ -419,6 +565,7 @@ export async function listCredentialProviderCatalog( ...(descriptor.helpText ? { helpText: descriptor.helpText } : {}), requiresClientGeneratedCredentialId: descriptor.requiresClientGeneratedCredentialId === true, fields: descriptor.fields, + ...(descriptor.keyless ? { keyless: descriptor.keyless } : {}), } }) diff --git a/apps/sim/lib/credentials/application/service-account.test.ts b/apps/sim/lib/credentials/application/service-account.test.ts index 0db61e5fff0..ee1eb8f047c 100644 --- a/apps/sim/lib/credentials/application/service-account.test.ts +++ b/apps/sim/lib/credentials/application/service-account.test.ts @@ -46,6 +46,7 @@ import { createServiceAccountCredentialUseCase, deleteCredentialUseCase, } from '@/lib/credentials/application/service-account' +import { KeylessCredentialRefusedError } from '@/lib/credentials/workload-identity/subject' import { executeCopilotCredentialUseCase } from '@/lib/mothership/application/execute-credential-use-case' const mocks = { @@ -126,6 +127,30 @@ describe('credential service-account application operations', () => { expect(mocks.create).not.toHaveBeenCalled() }) + it('rethrows the keyless refusal the create result carries, with its code intact', async () => { + const refusal = new KeylessCredentialRefusedError( + 'Another credential already uses this trust setup.', + 'WORKLOAD_IDENTITY_SUBJECT_IN_USE' + ) + mocks.create.mockResolvedValue({ + success: false, + error: refusal.message, + errorCode: 'conflict', + keylessRefusal: refusal, + }) + + await expect( + createServiceAccountCredentialUseCase.execute({ + principal, + input: { + workspaceId: WORKSPACE_ID, + providerId: 'aws-service-account', + serviceAccountJson: '{}', + }, + }) + ).rejects.toBe(refusal) + }) + const storedInput = { workspaceId: WORKSPACE_ID, displayName: 'Support bot', diff --git a/apps/sim/lib/credentials/application/service-account.ts b/apps/sim/lib/credentials/application/service-account.ts index 25f099c0302..94f272174b1 100644 --- a/apps/sim/lib/credentials/application/service-account.ts +++ b/apps/sim/lib/credentials/application/service-account.ts @@ -107,6 +107,7 @@ export const createServiceAccountCredentialUseCase = defineAuthorizedWorkspaceUs }) if (!result.success) { if (result.providerUnavailable) throw new CredentialProviderUnavailableError() + if (result.keylessRefusal) throw result.keylessRefusal switch (result.errorCode) { case 'validation': case 'not_found': diff --git a/apps/sim/lib/credentials/application/sharing.integration.ts b/apps/sim/lib/credentials/application/sharing.integration.ts index 7bbba0f27d4..2df8384675d 100644 --- a/apps/sim/lib/credentials/application/sharing.integration.ts +++ b/apps/sim/lib/credentials/application/sharing.integration.ts @@ -11,7 +11,12 @@ describe('Credential sharing through user-held API credentials in PostgreSQL', ( const workspaceId = generateId() const organizationId = generateId() const credentialId = generateId() - const principal = { kind: 'personal_api_key' as const, userId: actorId, keyId: generateId() } + const principal = { + kind: 'personal_api_key' as const, + userId: actorId, + keyId: generateId(), + access: 'write' as const, + } let runtime: Awaited> async function loadRuntime() { @@ -126,7 +131,7 @@ describe('Credential sharing through user-held API credentials in PostgreSQL', ( it('denies workspace keys before looking up a credential', async () => { await expect( runtime.upsertCredentialMemberUseCase.execute({ - principal: { kind: 'workspace_api_key', workspaceId, keyId: generateId() }, + principal: { kind: 'workspace_api_key', workspaceId, keyId: generateId(), access: 'write' }, input: { credentialId: generateId(), userId: readerId, role: 'admin' }, }) ).rejects.toMatchObject({ detailCode: 'WORKSPACE_KEY_OPERATION_NOT_PERMITTED' }) diff --git a/apps/sim/lib/credentials/aws-service-account.server.test.ts b/apps/sim/lib/credentials/aws-service-account.server.test.ts new file mode 100644 index 00000000000..97b4a329854 --- /dev/null +++ b/apps/sim/lib/credentials/aws-service-account.server.test.ts @@ -0,0 +1,157 @@ +/** + * @vitest-environment node + */ +import { queueTableRows, resetDbChainMock } from '@sim/testing' +import { encryptionMock, encryptionMockFns } from '@sim/testing/mocks/encryption.mock' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const hoisted = vi.hoisted(() => ({ + send: vi.fn(), + mint: vi.fn(async ({ subject }: { subject: string }) => `token-for:${subject}`), +})) + +vi.mock('@/lib/core/security/encryption', () => encryptionMock) +vi.mock('@/lib/credentials/workload-identity/issuer', () => ({ + isWorkloadIdentityConfigured: async () => true, + mintWorkloadIdentityToken: hoisted.mint, +})) +vi.mock('@aws-sdk/client-sts', async (importActual) => ({ + ...(await importActual()), + STSClient: class { + send = hoisted.send + }, +})) + +import { AssumeRoleWithWebIdentityCommand, GetCallerIdentityCommand } from '@aws-sdk/client-sts' +import { credential } from '@sim/db/schema' +import { + getAwsCredentialConfig, + verifyAndEncryptAwsCredential, +} from '@/lib/credentials/aws-service-account.server' +import { WorkloadIdentityNotConfiguredError } from '@/lib/credentials/workload-identity/subject' +import { AWS_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/oauth/types' + +const ROLE_ARN = 'arn:aws:iam::123456789012:role/sim-workflows' +const SUBJECT = 'sim:workspace:ws-1:credential:abcdef0123456789' +const session = { AccessKeyId: 'ASIATEMP', SecretAccessKey: 'secret', SessionToken: 'token' } + +function accessDenied() { + return Object.assign(new Error('Not authorized to perform sts:AssumeRoleWithWebIdentity'), { + name: 'AccessDenied', + }) +} + +/** A role whose trust policy pins `sub` to {@link SUBJECT}, or trusts any subject. */ +function roleTrusting(pinsSubject: boolean) { + hoisted.send.mockImplementation(async (command: unknown) => { + if (command instanceof AssumeRoleWithWebIdentityCommand) { + const trusted = !pinsSubject || command.input.WebIdentityToken === `token-for:${SUBJECT}` + if (!trusted) throw accessDenied() + return { Credentials: { ...session, Expiration: new Date() } } + } + if (command instanceof GetCallerIdentityCommand) { + return { Arn: 'arn:aws:sts::123456789012:assumed-role/sim-workflows/sim-verify' } + } + throw new Error('unexpected STS command') + }) +} + +/** Reconnecting a keyless credential that keeps its stored subject. */ +const keylessReconnect = { + serialized: JSON.stringify({ authType: 'oidc_role', roleArn: ROLE_ARN }), + owner: { + workspaceId: 'ws-1', + actorUserId: 'user-1', + storedSecret: { + type: 'aws_credential_v1', + config: { authType: 'oidc_role', roleArn: ROLE_ARN, subject: SUBJECT }, + }, + }, +} + +describe('saving a keyless AWS role', () => { + beforeEach(() => { + hoisted.send.mockReset() + encryptionMockFns.mockEncryptSecret.mockResolvedValue({ encrypted: 'ciphertext', iv: 'iv' }) + }) + + it('saves a role whose trust policy pins this credential’s subject', async () => { + roleTrusting(true) + + await expect( + verifyAndEncryptAwsCredential(keylessReconnect.serialized, keylessReconnect.owner) + ).resolves.toMatchObject({ encryptedServiceAccountKey: 'ciphertext' }) + }) + + it('refuses a role that trusts any Sim subject, so no other workspace can assume it', async () => { + roleTrusting(false) + + await expect( + verifyAndEncryptAwsCredential(keylessReconnect.serialized, keylessReconnect.owner) + ).rejects.toThrow(/sub/) + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + it('reads a provider message that ends in a period as one sentence', async () => { + hoisted.send.mockRejectedValue( + Object.assign(new Error('Not authorized to perform sts:AssumeRoleWithWebIdentity.'), { + name: 'AccessDenied', + }) + ) + + await expect( + verifyAndEncryptAwsCredential(keylessReconnect.serialized, keylessReconnect.owner) + ).rejects.toThrow( + 'Sim could not assume this role: Not authorized to perform sts:AssumeRoleWithWebIdentity. Check that' + ) + }) + + it('reconnects to a keyless refusal with its code when signing stops mid-save', async () => { + roleTrusting(true) + hoisted.mint.mockRejectedValueOnce(new WorkloadIdentityNotConfiguredError()) + + await expect( + verifyAndEncryptAwsCredential(keylessReconnect.serialized, keylessReconnect.owner) + ).rejects.toMatchObject({ detailCode: 'WORKLOAD_IDENTITY_NOT_CONFIGURED' }) + }) + + it('does not save when the any-subject check cannot be completed', async () => { + roleTrusting(true) + const pinned = hoisted.send.getMockImplementation() + hoisted.send.mockImplementation(async (command: unknown) => { + const probesUnboundSubject = + command instanceof AssumeRoleWithWebIdentityCommand && + command.input.WebIdentityToken !== `token-for:${SUBJECT}` + if (probesUnboundSubject) { + throw Object.assign(new Error('socket hang up'), { name: 'TimeoutError' }) + } + return pinned?.(command) + }) + + await expect( + verifyAndEncryptAwsCredential(keylessReconnect.serialized, keylessReconnect.owner) + ).rejects.toThrow() + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) +}) + +describe('loading a saved AWS credential for a run', () => { + beforeEach(() => resetDbChainMock()) + + it('refuses a credential owned by a different workspace than the run', async () => { + queueTableRows(credential, [ + { + workspaceId: 'ws-other', + organizationId: null, + type: 'service_account', + providerId: AWS_SERVICE_ACCOUNT_PROVIDER_ID, + revokedAt: null, + encryptedServiceAccountKey: 'ciphertext', + }, + ]) + + await expect(getAwsCredentialConfig('cred-1', { workspaceId: 'ws-1' })).rejects.toThrow( + 'AWS credential is unavailable' + ) + }) +}) diff --git a/apps/sim/lib/credentials/aws-service-account.server.ts b/apps/sim/lib/credentials/aws-service-account.server.ts new file mode 100644 index 00000000000..4995824060c --- /dev/null +++ b/apps/sim/lib/credentials/aws-service-account.server.ts @@ -0,0 +1,430 @@ +import { + AssumeRoleCommand, + AssumeRoleWithWebIdentityCommand, + type Credentials, + GetCallerIdentityCommand, + STSClient, +} from '@aws-sdk/client-sts' +import { db } from '@sim/db' +import { credential } from '@sim/db/schema' +import { and, eq, isNull, or } from 'drizzle-orm' +import { z } from 'zod' +import { awsRequestHandler } from '@/lib/core/network/proxy-agent.server' +import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' +import { failureClause, parseServiceAccountConfig } from '@/lib/credentials/service-account-config' +import { WORKLOAD_IDENTITY_AUDIENCES } from '@/lib/credentials/workload-identity/audiences' +import { + isWorkloadIdentityConfigured, + mintWorkloadIdentityToken, + type WorkloadIdentityOwner, +} from '@/lib/credentials/workload-identity/issuer' +import { + bindWorkloadIdentitySubject, + KeylessCredentialRefusedError, + newWorkloadIdentitySubject, + type WorkloadIdentityCredentialOwner, + WorkloadIdentityNotConfiguredError, +} from '@/lib/credentials/workload-identity/subject' +import { AWS_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/oauth/types' + +/** + * Saved AWS identities for every AWS block. + * + * Three modes, all resolved server-side at execution. The stored keys and the + * sessions resolved from them stay out of block outputs, logs, and model input, + * with two exceptions: an S3 presigned URL made with temporary credentials + * carries the temporary access key ID and session token (never the secret), and + * operations whose purpose is to issue AWS credentials (STS assume role and get + * session token, IAM create access key) return the credentials they issue, as + * the same operations do with inline keys. Using a credential therefore includes + * issuing whatever its IAM permissions allow. + * - `access_keys`: a long-lived IAM user key pair, used as-is, like inline keys. + * - `assume_role`: a key pair that assumes a role, optionally with an external ID. + * - `oidc_role`: no stored key at all — Sim signs a short-lived OIDC token for + * this credential's subject and exchanges it with STS for the customer's role. + */ + +const AWS_WORKLOAD_IDENTITY_AUDIENCE = WORKLOAD_IDENTITY_AUDIENCES.aws +const AWS_CREDENTIAL_SECRET_TYPE = 'aws_credential_v1' as const + +const ROLE_ARN_PATTERN = + /^arn:aws(?:-cn|-us-gov|-iso[a-z-]*)?:iam::\d{12}:role\/[\w+=,.@/-]{1,512}$/ +const REGION_PATTERN = /^[a-z]{2}(?:-[a-z]+)+-\d{1,2}$/ +const ACCESS_KEY_ID_PATTERN = /^[A-Z0-9]{16,128}$/ +const EXTERNAL_ID_PATTERN = /^[\w+=,.@:/-]{2,1224}$/ +const VERIFY_TIMEOUT_MS = 20_000 + +const keyPairShape = { + accessKeyId: z.string().trim().regex(ACCESS_KEY_ID_PATTERN, 'Invalid AWS access key ID'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required').max(256), +} + +const commonShape = { + stsRegion: z.string().trim().regex(REGION_PATTERN, 'Invalid AWS region').optional(), + sessionDurationSeconds: z.number().int().min(900).max(43_200).optional(), +} + +const roleArnSchema = z.string().trim().regex(ROLE_ARN_PATTERN, 'Invalid IAM role ARN') + +const accessKeysSchema = z + .object({ authType: z.literal('access_keys'), ...keyPairShape, ...commonShape }) + .strict() + +const assumeRoleSchema = z + .object({ + authType: z.literal('assume_role'), + ...keyPairShape, + roleArn: roleArnSchema, + externalId: z.string().regex(EXTERNAL_ID_PATTERN, 'Invalid external ID').optional(), + ...commonShape, + }) + .strict() + +const oidcRoleShape = { + authType: z.literal('oidc_role'), + roleArn: roleArnSchema, + subject: z.string().min(1).max(512), + ...commonShape, +} + +const storedConfigSchema = z.discriminatedUnion('authType', [ + accessKeysSchema, + assumeRoleSchema, + z.object(oidcRoleShape).strict(), +]) + +export type AwsCredentialConfig = z.infer + +/** + * What the connect modal submits. A new keyless role carries the subject Sim + * issued and its proof; a reconnect that keeps the configured trust omits both. + */ +const submittedConfigSchema = z.discriminatedUnion('authType', [ + accessKeysSchema, + assumeRoleSchema, + z + .object({ + ...oidcRoleShape, + subject: oidcRoleShape.subject.optional(), + subjectProof: z.string().min(1).max(512).optional(), + }) + .strict(), +]) + +const storedSecretSchema = z + .object({ type: z.literal(AWS_CREDENTIAL_SECRET_TYPE), config: storedConfigSchema }) + .strict() + +export class AwsCredentialError extends Error { + constructor(message: string) { + super(message) + this.name = 'AwsCredentialError' + } +} + +export interface AwsSessionCredentials { + accessKeyId: string + secretAccessKey: string + sessionToken?: string +} + +/** STS lives in every partition; a role's partition decides the default endpoint. */ +function defaultStsRegion(roleArn?: string): string { + if (roleArn?.startsWith('arn:aws-cn:')) return 'cn-north-1' + if (roleArn?.startsWith('arn:aws-us-gov:')) return 'us-gov-west-1' + return 'us-east-1' +} + +function stsRegionFor(config: AwsCredentialConfig): string { + return config.stsRegion ?? defaultStsRegion('roleArn' in config ? config.roleArn : undefined) +} + +/** RoleSessionName allows `[\w+=,.@-]{2,64}` and shows up in the customer's CloudTrail. */ +export function buildAwsRoleSessionName(label: string | undefined): string { + const cleaned = (label ?? '').replace(/[^\w+=,.@-]/g, '-').slice(0, 56) + return `sim-${cleaned || 'credential'}` +} + +function toSessionCredentials(credentials: Credentials | undefined): AwsSessionCredentials { + if (!credentials?.AccessKeyId || !credentials.SecretAccessKey || !credentials.SessionToken) { + throw new AwsCredentialError('AWS STS returned no credentials') + } + return { + accessKeyId: credentials.AccessKeyId, + secretAccessKey: credentials.SecretAccessKey, + sessionToken: credentials.SessionToken, + } +} + +/** + * Exchanges a saved AWS identity for credentials an SDK client can sign with. + * `access_keys` return the stored pair unchanged (no session token), so a + * credential behaves exactly like the inline keys it replaces. + */ +export async function resolveAwsSessionCredentials( + config: AwsCredentialConfig, + owner: WorkloadIdentityOwner, + options: { roleSessionName: string; signal?: AbortSignal } +): Promise { + if (config.authType === 'access_keys') { + return { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey } + } + const region = stsRegionFor(config) + const durationSeconds = config.sessionDurationSeconds ?? 3600 + if (config.authType === 'assume_role') { + const client = new STSClient({ + region, + credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey }, + requestHandler: awsRequestHandler(), + }) + const response = await client.send( + new AssumeRoleCommand({ + RoleArn: config.roleArn, + RoleSessionName: options.roleSessionName, + DurationSeconds: durationSeconds, + ...(config.externalId ? { ExternalId: config.externalId } : {}), + }), + { abortSignal: options.signal } + ) + return toSessionCredentials(response.Credentials) + } + const webIdentityToken = await mintWorkloadIdentityToken({ + subject: config.subject, + audience: AWS_WORKLOAD_IDENTITY_AUDIENCE, + owner, + }) + /** + * AWS does not check the request signature for AssumeRoleWithWebIdentity; + * placeholder credentials keep the SDK off the ambient provider chain so the + * request never signs with Sim's own task role. + */ + const client = new STSClient({ + region, + credentials: { accessKeyId: 'anonymous', secretAccessKey: 'anonymous' }, + requestHandler: awsRequestHandler(), + }) + const response = await client.send( + new AssumeRoleWithWebIdentityCommand({ + RoleArn: config.roleArn, + RoleSessionName: options.roleSessionName, + WebIdentityToken: webIdentityToken, + DurationSeconds: durationSeconds, + }), + { abortSignal: options.signal } + ) + return toSessionCredentials(response.Credentials) +} + +/** The auth mode a decrypted AWS secret holds, or undefined when it is not one. */ +export function storedAwsAuthType(decrypted: string): AwsCredentialConfig['authType'] | undefined { + try { + const parsed = storedSecretSchema.safeParse(JSON.parse(decrypted)) + return parsed.success ? parsed.data.config.authType : undefined + } catch { + return undefined + } +} + +function readStoredSubject(storedSecret: Record | null | undefined) { + const parsed = storedSecretSchema.safeParse(storedSecret) + return parsed.success && parsed.data.config.authType === 'oidc_role' + ? parsed.data.config.subject + : undefined +} + +function bindSubject( + subject: string | undefined, + subjectProof: string | undefined, + storedSubject: string | undefined, + owner: WorkloadIdentityCredentialOwner +): string { + return bindWorkloadIdentitySubject({ + provider: 'aws', + owner, + storedSubject, + subject, + subjectProof, + }) +} + +function verificationFailure(config: AwsCredentialConfig, error: unknown): Error { + // A keyless refusal already names its cause and code; wrapping it would lose both. + if (error instanceof KeylessCredentialRefusedError) return error + const detail = failureClause(error) + if (config.authType === 'oidc_role') { + return new AwsCredentialError( + `Sim could not assume this role: ${detail}. Check that the role trusts the Sim OIDC provider with this exact subject and audience ${AWS_WORKLOAD_IDENTITY_AUDIENCE}.` + ) + } + if (config.authType === 'assume_role') { + return new AwsCredentialError( + `Sim could not assume this role with the access keys: ${detail}. Check the role ARN, its trust policy, and the external ID.` + ) + } + return new AwsCredentialError(`AWS rejected these access keys: ${detail}`) +} + +/** + * Validates the submitted configuration, proves it works by resolving it and + * calling `sts:GetCallerIdentity`, and returns the encrypted blob, the caller + * ARN AWS reports, and the keyless subject it newly binds, if any (a + * reconnect keeping its stored subject binds none). + */ +export async function verifyAndEncryptAwsCredential( + serialized: string, + owner: WorkloadIdentityCredentialOwner +): Promise<{ encryptedServiceAccountKey: string; callerArn: string; newSubject?: string }> { + const parsed = parseServiceAccountConfig(serialized, submittedConfigSchema, 'AWS credential') + if (!parsed.success) throw new AwsCredentialError(parsed.message) + const submitted = parsed.data + let config: AwsCredentialConfig + if (submitted.authType === 'oidc_role') { + if (!(await isWorkloadIdentityConfigured())) { + throw new WorkloadIdentityNotConfiguredError('Use access keys.') + } + const { subject, subjectProof, ...rest } = submitted + config = { + ...rest, + subject: bindSubject(subject, subjectProof, readStoredSubject(owner.storedSecret), owner), + } + } else { + config = submitted + } + + // The trust-pin probe is independent of the verification, so both run at once; + // a verification failure still reports first. + const [verified, pinned] = await Promise.allSettled([ + verifyCallerArn(config, owner), + config.authType === 'oidc_role' ? assertRolePinsSubject(config, owner) : undefined, + ]) + if (verified.status === 'rejected') throw verificationFailure(config, verified.reason) + if (pinned.status === 'rejected') throw pinned.reason + const callerArn = verified.value + + const { encrypted } = await encryptSecret( + JSON.stringify({ type: AWS_CREDENTIAL_SECRET_TYPE, config }) + ) + return { + encryptedServiceAccountKey: encrypted, + callerArn, + ...(config.authType === 'oidc_role' && config.subject !== readStoredSubject(owner.storedSecret) + ? { newSubject: config.subject } + : {}), + } +} + +/** Resolves the configuration and returns the caller ARN AWS reports for it. */ +async function verifyCallerArn( + config: AwsCredentialConfig, + owner: WorkloadIdentityOwner +): Promise { + const signal = AbortSignal.timeout(VERIFY_TIMEOUT_MS) + const session = await resolveAwsSessionCredentials(config, owner, { + roleSessionName: buildAwsRoleSessionName('verify'), + signal, + }) + const identity = await new STSClient({ + region: stsRegionFor(config), + credentials: session, + requestHandler: awsRequestHandler(), + }).send(new GetCallerIdentityCommand({}), { abortSignal: signal }) + if (!identity.Arn) throw new Error('AWS returned no caller identity') + return identity.Arn +} + +/** + * Sim signs tokens for any role ARN a member types in, so a role that trusts + * Sim's issuer without pinning `sub` could be assumed from any Sim workspace. + * Retrying the role with a subject no credential holds must be denied before + * the credential is saved. + */ +async function assertRolePinsSubject( + config: Extract, + owner: WorkloadIdentityOwner +): Promise { + const unboundSubject = newWorkloadIdentitySubject(owner) + try { + await resolveAwsSessionCredentials({ ...config, subject: unboundSubject }, owner, { + roleSessionName: buildAwsRoleSessionName('verify'), + signal: AbortSignal.timeout(VERIFY_TIMEOUT_MS), + }) + } catch (error) { + if (error instanceof Error && error.name === 'AccessDenied') return + if (error instanceof KeylessCredentialRefusedError) throw error + throw new AwsCredentialError( + `Sim could not confirm the role's trust policy limits it to this credential: ${failureClause(error)}. Try again.` + ) + } + throw new AwsCredentialError( + `This role trusts every Sim subject. Use the trust policy from Sim (the connect dialog or \`sim credentials keyless-subject\`), whose sub condition pins ${config.subject}, so only this credential can assume the role.` + ) +} + +/** + * Loads an executor-authorized AWS credential bound to its trusted owner + * scope. The executor has already authorized the acting user's use of the + * credential; this re-checks that the row belongs to the run's own workspace + * or organization and is a live AWS credential before decrypting it. + */ +export async function getAwsCredentialConfig( + credentialId: string, + scope: WorkloadIdentityOwner +): Promise<{ config: AwsCredentialConfig; owner: WorkloadIdentityOwner }> { + const unavailable = () => new AwsCredentialError('AWS credential is unavailable') + if (!credentialId || (!scope.workspaceId && !scope.organizationId)) throw unavailable() + const [row] = await db + .select({ + workspaceId: credential.workspaceId, + organizationId: credential.organizationId, + type: credential.type, + providerId: credential.providerId, + revokedAt: credential.revokedAt, + encryptedServiceAccountKey: credential.encryptedServiceAccountKey, + }) + .from(credential) + .where( + and( + eq(credential.id, credentialId), + or( + scope.workspaceId + ? and(eq(credential.workspaceId, scope.workspaceId), isNull(credential.organizationId)) + : undefined, + scope.organizationId + ? and( + eq(credential.organizationId, scope.organizationId), + isNull(credential.workspaceId) + ) + : undefined + ) + ) + ) + .limit(1) + const ownerMatches = + row && + ((row.workspaceId === scope.workspaceId && + !!scope.workspaceId && + row.organizationId === null) || + (row.organizationId === scope.organizationId && + !!scope.organizationId && + row.workspaceId === null)) + if ( + !ownerMatches || + row.type !== 'service_account' || + row.providerId !== AWS_SERVICE_ACCOUNT_PROVIDER_ID || + row.revokedAt !== null || + !row.encryptedServiceAccountKey + ) + throw unavailable() + try { + const { decrypted } = await decryptSecret(row.encryptedServiceAccountKey) + const { config } = storedSecretSchema.parse(JSON.parse(decrypted)) + return { + config, + owner: row.workspaceId + ? { workspaceId: row.workspaceId } + : { organizationId: row.organizationId ?? undefined }, + } + } catch { + throw unavailable() + } +} diff --git a/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts b/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts index e57ce214e28..a9d365612cd 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts @@ -105,6 +105,16 @@ export interface ClientCredentialAccountDescriptor { defaultAuthMethod?: string /** Sim setup guide, docked bottom-left of the connect modal. */ docsUrl: string + /** + * Builds the provider's one-time consent URL from the values in the connect + * modal, for providers whose grant fails with `consent_required` until a user + * approves the app (DocuSign JWT Grant). `redirectUri` is the Sim origin the + * modal is open on. Returns `undefined` when the values can't form one. + */ + consentUrl?: ( + values: Partial>, + redirectUri: string + ) => string | undefined /** * Optional one-line caveat, rendered under the `orgId` field of the connect modal. A descriptor * without an `orgId` field puts its guidance in a field `hint` instead. @@ -125,6 +135,7 @@ export const ORACLE_FUSION_SERVICE_ACCOUNT_PROVIDER_ID = 'oracle-fusion-service- export const GREENHOUSE_SERVICE_ACCOUNT_PROVIDER_ID = 'greenhouse-service-account' as const export const JAMA_SERVICE_ACCOUNT_PROVIDER_ID = 'jama-service-account' as const export const HELPSCOUT_SERVICE_ACCOUNT_PROVIDER_ID = 'helpscout-service-account' as const +export const DOCUSIGN_SERVICE_ACCOUNT_PROVIDER_ID = 'docusign-service-account' as const /** Permissions requested when connecting a Manage Vanta application. */ export const VANTA_PERMISSION_OPTIONS = [ @@ -150,6 +161,7 @@ export type ClientCredentialAccountProviderId = | typeof GREENHOUSE_SERVICE_ACCOUNT_PROVIDER_ID | typeof HELPSCOUT_SERVICE_ACCOUNT_PROVIDER_ID | typeof JAMA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof DOCUSIGN_SERVICE_ACCOUNT_PROVIDER_ID /** * Exact account-specific SuiteTalk origin accepted by NetSuite's OAuth and @@ -385,6 +397,63 @@ export function resolveZohoDeskDataCenter(rawDataCenter?: string): { return { id, ...ZOHO_DESK_DATA_CENTERS[id] } } +/** + * DocuSign's two account systems. Each environment has its own authentication + * service and its own eSignature REST hosts, and a token from one is never + * accepted by the other, so the credential stores which one it belongs to and + * nothing a caller types can change it. + * @see https://developers.docusign.com/platform/auth/jwt/jwt-get-token/ + */ +export const DOCUSIGN_ENVIRONMENTS = { + demo: { authHost: 'account-d.docusign.com', label: 'Developer (demo)' }, + production: { authHost: 'account.docusign.com', label: 'Production' }, +} as const satisfies Record + +export type DocuSignEnvironment = keyof typeof DOCUSIGN_ENVIRONMENTS + +/** Resolves a stored environment value, or `undefined` when it is not exactly one of the two. */ +export function resolveDocuSignEnvironment( + value: string | undefined +): DocuSignEnvironment | undefined { + const trimmed = value?.trim() + return trimmed && Object.hasOwn(DOCUSIGN_ENVIRONMENTS, trimmed) + ? (trimmed as DocuSignEnvironment) + : undefined +} + +/** DocuSign integration keys, user IDs, and account IDs are all GUIDs. */ +export const DOCUSIGN_GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + +/** An unencrypted PEM RSA private key, in the PKCS#1 form DocuSign generates or PKCS#8. */ +const DOCUSIGN_PRIVATE_KEY_REGEX = + /^-----BEGIN (RSA )?PRIVATE KEY-----[\s\S]+-----END (RSA )?PRIVATE KEY-----$/ + +/** Scopes a JWT Grant credential requests; `impersonation` is what JWT consent covers. */ +export const DOCUSIGN_JWT_SCOPES = 'signature impersonation' + +/** + * The one-time consent URL the impersonated user opens to let the integration + * key act as them. DocuSign requires a `redirect_uri` registered on the key; + * the JWT flow ignores the `code` it receives. + * @see https://developers.docusign.com/platform/auth/jwt/jwt-get-token/ + */ +function buildDocuSignConsentUrl( + integrationKey: string, + environment: string | undefined, + redirectUri: string +): string | undefined { + const resolved = resolveDocuSignEnvironment(environment) + const key = integrationKey.trim() + if (!resolved || !DOCUSIGN_GUID_REGEX.test(key)) return undefined + const params = new URLSearchParams({ + response_type: 'code', + scope: DOCUSIGN_JWT_SCOPES, + client_id: key, + redirect_uri: redirectUri, + }) + return `https://${DOCUSIGN_ENVIRONMENTS[resolved].authHost}/oauth/auth?${params.toString()}` +} + export const CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS: Record< ClientCredentialAccountProviderId, ClientCredentialAccountDescriptor @@ -417,6 +486,69 @@ export const CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS: Record< helpText: 'Create API credentials in your Jama profile. Your account needs REST API access and a Named Creator license.', }, + [DOCUSIGN_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: DOCUSIGN_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'DocuSign', + connectNoun: 'JWT Grant app', + fields: [ + { + id: 'dataCenter', + label: 'Environment', + placeholder: 'Select an environment', + secret: false, + options: (Object.keys(DOCUSIGN_ENVIRONMENTS) as DocuSignEnvironment[]).map((id) => ({ + value: id, + label: DOCUSIGN_ENVIRONMENTS[id].label, + })), + hint: 'Where the integration key was created. Keys start in the developer environment until they go live.', + }, + { + id: 'clientId', + label: 'Integration key', + placeholder: '00000000-0000-0000-0000-000000000000', + secret: false, + hintPattern: DOCUSIGN_GUID_REGEX, + hintNormalize: (value) => value.trim(), + hintMessage: 'Expected the integration key GUID from Apps and Keys.', + }, + { + id: 'username', + label: 'Impersonated user ID', + placeholder: '00000000-0000-0000-0000-000000000000', + secret: false, + hintPattern: DOCUSIGN_GUID_REGEX, + hintNormalize: (value) => value.trim(), + hintMessage: 'Expected the User ID GUID from Apps and Keys, not an email address.', + hint: 'Every call runs as this user, with their DocuSign permissions.', + }, + { + id: 'privateKey', + label: 'RSA private key', + placeholder: '-----BEGIN RSA PRIVATE KEY-----', + secret: true, + multiline: true, + hintPattern: DOCUSIGN_PRIVATE_KEY_REGEX, + hintNormalize: (value) => value.trim(), + hintMessage: + 'Expected the full PEM private key from the RSA keypair, including its BEGIN and END lines.', + }, + { + id: 'orgId', + label: 'Account ID', + placeholder: "Defaults to the user's default account", + secret: false, + optional: true, + hintPattern: DOCUSIGN_GUID_REGEX, + hintNormalize: (value) => value.trim(), + hintMessage: 'Expected the API Account ID GUID from Apps and Keys.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/docusign-service-account', + helpText: + "Only needed when the user belongs to more than one account and the one to use isn't their default.", + consentUrl: (values, redirectUri) => + buildDocuSignConsentUrl(values.clientId ?? '', values.dataCenter, redirectUri), + }, [RAMP_SERVICE_ACCOUNT_PROVIDER_ID]: { providerId: RAMP_SERVICE_ACCOUNT_PROVIDER_ID, serviceLabel: 'Ramp', diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/docusign.test.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/docusign.test.ts new file mode 100644 index 00000000000..51912c73c5a --- /dev/null +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/docusign.test.ts @@ -0,0 +1,284 @@ +import { generateKeyPairSync } from 'crypto' +import { jsonResponse } from '@sim/testing/helpers/http' +import { decodeProtectedHeader, jwtVerify } from 'jose' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { mintDocuSignServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/docusign' + +const INTEGRATION_KEY = '11111111-2222-3333-4444-555555555555' +const USER_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' +const DEFAULT_ACCOUNT = '99999999-0000-0000-0000-000000000001' +const OTHER_ACCOUNT = '99999999-0000-0000-0000-000000000002' + +const { privateKey: RSA_PEM, publicKey: RSA_PUBLIC } = generateKeyPairSync('rsa', { + modulusLength: 2048, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, + publicKeyEncoding: { type: 'spki', format: 'pem' }, +}) +const EC_PEM = generateKeyPairSync('ec', { + namedCurve: 'P-256', + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + publicKeyEncoding: { type: 'spki', format: 'pem' }, +}).privateKey +const SHORT_RSA_PEM = generateKeyPairSync('rsa', { + modulusLength: 1024, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, + publicKeyEncoding: { type: 'spki', format: 'pem' }, +}).privateKey + +const FIELDS = { + clientId: INTEGRATION_KEY, + username: USER_ID, + privateKey: RSA_PEM, + dataCenter: 'demo', + orgId: '', +} + +const ACCESS_TOKEN = 'test-docusign-access-token' + +function userinfo(accounts: Array>) { + return jsonResponse({ + sub: USER_ID, + name: 'Integration User', + email: 'ops@example.com', + accounts, + }) +} + +const DEMO_ACCOUNTS = [ + { + account_id: OTHER_ACCOUNT, + is_default: false, + account_name: 'Other', + base_uri: 'https://demo.docusign.net', + }, + { + account_id: DEFAULT_ACCOUNT, + is_default: true, + account_name: 'Main', + base_uri: 'https://demo.docusign.net', + }, +] + +const mockFetch = vi.fn() + +function mockMint(accounts = DEMO_ACCOUNTS) { + mockFetch + .mockResolvedValueOnce( + jsonResponse({ access_token: ACCESS_TOKEN, token_type: 'Bearer', expires_in: 3600 }) + ) + .mockResolvedValueOnce(userinfo(accounts)) +} + +/** Every string a failure exposes: its message plus the log detail callers write to logs. */ +function exposedText(error: unknown): string { + const failure = error as { message?: string; logDetail?: unknown } + return `${failure.message} ${JSON.stringify(failure.logDetail)}` +} + +describe('mintDocuSignServiceAccountToken', () => { + beforeEach(() => { + vi.stubGlobal('fetch', mockFetch) + }) + + it('signs the documented assertion for the demo environment and binds the default account', async () => { + mockMint() + + const result = await mintDocuSignServiceAccountToken(FIELDS) + + const [tokenUrl, tokenInit] = mockFetch.mock.calls[0] + expect(tokenUrl).toBe('https://account-d.docusign.com/oauth/token') + const body = new URLSearchParams(tokenInit.body as string) + expect(body.get('grant_type')).toBe('urn:ietf:params:oauth:grant-type:jwt-bearer') + const assertion = body.get('assertion') ?? '' + expect(decodeProtectedHeader(assertion)).toMatchObject({ alg: 'RS256', typ: 'JWT' }) + const { createPublicKey } = await import('crypto') + const { payload } = await jwtVerify(assertion, createPublicKey(RSA_PUBLIC), { + issuer: INTEGRATION_KEY, + subject: USER_ID, + audience: 'account-d.docusign.com', + }) + expect(payload.scope).toBe('signature impersonation') + expect((payload.exp ?? 0) - (payload.iat ?? 0)).toBeLessThanOrEqual(3600) + + expect(mockFetch.mock.calls[1][0]).toBe('https://account-d.docusign.com/oauth/userinfo') + expect(mockFetch.mock.calls[1][1].headers.Authorization).toBe(`Bearer ${ACCESS_TOKEN}`) + expect(result).toMatchObject({ + accessToken: ACCESS_TOKEN, + expiresInSeconds: 3000, + instanceUrl: `https://demo.docusign.net/restapi/v2.1/accounts/${DEFAULT_ACCOUNT}`, + identity: { principal: { kind: 'user', id: USER_ID } }, + }) + }) + + it('mints and resolves accounts on the production host when the credential says production', async () => { + mockMint([ + { + account_id: DEFAULT_ACCOUNT, + is_default: true, + account_name: 'Production', + base_uri: 'https://na3.docusign.net', + }, + ]) + + const result = await mintDocuSignServiceAccountToken({ ...FIELDS, dataCenter: 'production' }) + + expect(mockFetch.mock.calls[0][0]).toBe('https://account.docusign.com/oauth/token') + expect(mockFetch.mock.calls[1][0]).toBe('https://account.docusign.com/oauth/userinfo') + expect(result.instanceUrl).toBe( + `https://na3.docusign.net/restapi/v2.1/accounts/${DEFAULT_ACCOUNT}` + ) + }) + + it('binds the pinned account rather than the default one', async () => { + mockMint() + + const result = await mintDocuSignServiceAccountToken({ ...FIELDS, orgId: OTHER_ACCOUNT }) + + expect(result.instanceUrl).toBe( + `https://demo.docusign.net/restapi/v2.1/accounts/${OTHER_ACCOUNT}` + ) + }) + + it('refuses a pinned account the user does not belong to', async () => { + mockMint() + + await expect( + mintDocuSignServiceAccountToken({ + ...FIELDS, + orgId: '99999999-0000-0000-0000-000000000003', + }) + ).rejects.toMatchObject({ code: 'account_not_found' }) + }) + + it.each([ + ['https://evil.example.com', 'demo'], + ['https://na3.docusign.net', 'demo'], + ['https://demo.docusign.net', 'production'], + ['http://na3.docusign.net', 'production'], + ])('never binds a %s base_uri to a %s credential', async (baseUri, environment) => { + mockMint([ + { + account_id: DEFAULT_ACCOUNT, + is_default: true, + account_name: 'Default', + base_uri: baseUri, + }, + ]) + + await expect( + mintDocuSignServiceAccountToken({ ...FIELDS, dataCenter: environment }) + ).rejects.toMatchObject({ code: 'provider_unavailable' }) + }) + + it.each([ + ['an absent environment', { dataCenter: undefined }], + ['an environment that is a host', { dataCenter: 'https://evil.example.com' }], + ['an unknown environment', { dataCenter: 'staging' }], + ['a non-GUID integration key', { clientId: 'my-app' }], + ['an email as the user ID', { username: 'ops@example.com' }], + ['a non-GUID account ID', { orgId: '12345' }], + ['a certificate instead of a key', { privateKey: RSA_PUBLIC }], + [ + 'an encrypted key', + { + privateKey: + '-----BEGIN ENCRYPTED PRIVATE KEY-----\nAAAA\n-----END ENCRYPTED PRIVATE KEY-----', + }, + ], + [ + 'an unreadable PEM', + { privateKey: '-----BEGIN RSA PRIVATE KEY-----\nAAAA\n-----END RSA PRIVATE KEY-----' }, + ], + ['a non-RSA key', { privateKey: EC_PEM }], + ])('rejects %s before any request', async (_label, overrides) => { + await expect( + mintDocuSignServiceAccountToken({ ...FIELDS, ...overrides }) + ).rejects.toMatchObject({ code: 'invalid_credentials' }) + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('reports consent_required as its own code', async () => { + mockFetch.mockResolvedValueOnce(jsonResponse({ error: 'consent_required' }, 400)) + + await expect(mintDocuSignServiceAccountToken(FIELDS)).rejects.toMatchObject({ + code: 'consent_required', + }) + }) + + it.each([ + ['issuer_not_found', 'integration key not found in this environment'], + ['user_not_found', 'impersonated user ID not found in this environment'], + ['no_valid_keys_or_signatures', expect.stringContaining('private key does not match')], + ])('maps invalid_grant %s to invalid_credentials', async (description, hint) => { + mockFetch.mockResolvedValueOnce( + jsonResponse({ error: 'invalid_grant', error_description: description }, 400) + ) + + await expect(mintDocuSignServiceAccountToken(FIELDS)).rejects.toMatchObject({ + code: 'invalid_credentials', + logDetail: expect.objectContaining({ hint }), + }) + }) + + it('refuses an RSA key under 2048 bits with copy that says so, before any request', async () => { + await expect( + mintDocuSignServiceAccountToken({ ...FIELDS, privateKey: SHORT_RSA_PEM }) + ).rejects.toMatchObject({ + code: 'invalid_credentials', + logDetail: expect.objectContaining({ reason: expect.stringContaining('1024-bit') }), + }) + expect(mockFetch).not.toHaveBeenCalled() + }) + + it.each([ + [900, 360], + [300, 240], + ])( + 'reports a short %ss token so the cache reuses it before, never past, its expiry', + async (expiresIn, reported) => { + mockFetch + .mockResolvedValueOnce(jsonResponse({ access_token: ACCESS_TOKEN, expires_in: expiresIn })) + .mockResolvedValueOnce(userinfo(DEMO_ACCOUNTS)) + + const result = await mintDocuSignServiceAccountToken(FIELDS) + + expect(result.expiresInSeconds).toBe(reported) + } + ) + + it.each([429, 503])('treats a %s from the token endpoint as an outage', async (status) => { + mockFetch.mockResolvedValueOnce(jsonResponse({ error: 'busy' }, status)) + + await expect(mintDocuSignServiceAccountToken(FIELDS)).rejects.toMatchObject({ + code: 'provider_unavailable', + }) + }) + + it('keeps the key, assertion, and access token out of every failure', async () => { + const failures: unknown[] = [] + mockFetch.mockResolvedValueOnce( + jsonResponse({ error: 'invalid_grant', error_description: 'user_not_found' }, 400) + ) + failures.push(await mintDocuSignServiceAccountToken(FIELDS).catch((error) => error)) + mockFetch + .mockResolvedValueOnce(jsonResponse({ access_token: ACCESS_TOKEN, expires_in: 3600 })) + .mockResolvedValueOnce(jsonResponse({ error: 'unauthorized' }, 401)) + failures.push(await mintDocuSignServiceAccountToken(FIELDS).catch((error) => error)) + mockMint([]) + failures.push(await mintDocuSignServiceAccountToken(FIELDS).catch((error) => error)) + failures.push( + await mintDocuSignServiceAccountToken({ ...FIELDS, clientId: 'bad' }).catch((error) => error) + ) + + const assertion = new URLSearchParams(mockFetch.mock.calls[0][1].body as string).get( + 'assertion' + ) + const keyBody = RSA_PEM.split('\n')[1] + for (const failure of failures) { + const text = exposedText(failure) + expect(text).not.toContain(keyBody) + expect(text).not.toContain(ACCESS_TOKEN) + expect(text).not.toContain(String(assertion)) + } + }) +}) diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/docusign.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/docusign.ts new file mode 100644 index 00000000000..2a0aee7ee39 --- /dev/null +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/docusign.ts @@ -0,0 +1,360 @@ +import { createPrivateKey, type KeyObject } from 'crypto' +import { SignJWT } from 'jose' +import { + DOCUSIGN_ENVIRONMENTS, + DOCUSIGN_GUID_REGEX, + DOCUSIGN_JWT_SCOPES, + type DocuSignEnvironment, + resolveDocuSignEnvironment, +} from '@/lib/credentials/client-credential-accounts/descriptors' +import type { + ClientCredentialAccountFields, + ClientCredentialAccountMintOptions, + ClientCredentialAccountMintResult, +} from '@/lib/credentials/client-credential-accounts/server' +import { userPrincipal } from '@/lib/credentials/principal' +import { + fetchProvider, + isTransientProviderStatus, + parseProviderJson, + readProviderErrorSnippet, + TokenServiceAccountValidationError, +} from '@/lib/credentials/token-service-accounts/errors' + +const TOKEN_STEP = 'docusign_token_mint' +const USERINFO_STEP = 'docusign_userinfo' +const VALIDATION_STEP = 'docusign_field_validation' + +const JWT_BEARER_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer' + +/** + * Lifetime of the signed assertion (not the access token). DocuSign defaults + * `exp` to an hour after `iat`; a short window bounds replay. + */ +const JWT_ASSERTION_LIFETIME_SECONDS = 300 + +/** + * DocuSign advises fetching a new token about 15 minutes before the old one + * expires. The resolver re-mints once less than 5 minutes remain, so reporting + * 10 minutes less than `expires_in` lands the re-mint at the advised point. + * @see https://developers.docusign.com/platform/auth/jwt/jwt-get-token/ + */ +const TOKEN_EXPIRY_MARGIN_SECONDS = 600 +const DEFAULT_TOKEN_LIFETIME_SECONDS = 3600 + +/** + * Floor for the reported lifetime when DocuSign ever returns a short + * `expires_in`: one minute above the resolver's 5-minute reuse threshold, so a + * token is reused briefly instead of re-minted on every call. Never more than + * a minute short of the real expiry. + */ +const SHORT_TOKEN_CACHE_SECONDS = 360 +const SHORT_TOKEN_EXPIRY_MARGIN_SECONDS = 60 + +/** Smallest RSA key DocuSign's RS256 signing accepts in `jose`. */ +const MIN_RSA_MODULUS_BITS = 2048 + +/** Lifetime to report to the token cache for a token that really lasts `lifetime` seconds. */ +function cacheLifetimeSeconds(lifetime: number): number { + const advised = lifetime - TOKEN_EXPIRY_MARGIN_SECONDS + const shortTokenFloor = Math.min( + lifetime - SHORT_TOKEN_EXPIRY_MARGIN_SECONDS, + SHORT_TOKEN_CACHE_SECONDS + ) + return Math.max(advised, shortTokenFloor, 0) +} + +/** + * eSignature REST hosts per environment. Demo accounts live only on + * `demo.docusign.net`; production accounts on a regional `*.docusign.net` + * host (`na3`, `eu`, …). A `base_uri` outside its environment's shape is never + * trusted with the token. + */ +const DEMO_BASE_URI_HOST = 'demo.docusign.net' +const PRODUCTION_BASE_URI_HOST_REGEX = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.docusign\.net$/ + +interface DocuSignTokenResponse { + access_token?: string + expires_in?: number +} + +interface DocuSignUserinfoAccount { + account_id?: unknown + is_default?: unknown + account_name?: unknown + base_uri?: unknown +} + +interface DocuSignUserinfoResponse { + sub?: unknown + name?: unknown + email?: unknown + accounts?: unknown +} + +function invalid(reason: string, step = VALIDATION_STEP): TokenServiceAccountValidationError { + return new TokenServiceAccountValidationError('invalid_credentials', 400, { step, reason }) +} + +/** + * Loads the pasted RSA key. DocuSign generates PKCS#1 (`BEGIN RSA PRIVATE + * KEY`); PKCS#8 is accepted too. Encrypted keys and certificates fail to load + * (Sim collects no passphrase). Keys under 2048 bits are refused because + * `jose` will not sign RS256 with them, and keys past 4096 bits because signing + * cost grows sharply with modulus size. + */ +function loadDocuSignPrivateKey(privateKeyPem: string): KeyObject { + let key: KeyObject + try { + key = createPrivateKey(privateKeyPem) + } catch { + throw invalid('private key is not a readable PEM key') + } + if (key.asymmetricKeyType !== 'rsa') { + throw invalid('DocuSign signs assertions with RS256, which needs an RSA key') + } + const modulusLength = key.asymmetricKeyDetails?.modulusLength ?? 0 + if (modulusLength < MIN_RSA_MODULUS_BITS) { + throw invalid( + `RSA key is ${modulusLength}-bit; generate a 2048-bit keypair on the integration key` + ) + } + if (modulusLength > 4096) { + throw invalid('RSA key is larger than 4096 bits') + } + return key +} + +interface ValidatedDocuSignFields { + environment: DocuSignEnvironment + integrationKey: string + userId: string + accountId?: string + key: KeyObject +} + +/** Rejects malformed input before anything is signed or sent. */ +function validateDocuSignFields(fields: ClientCredentialAccountFields): ValidatedDocuSignFields { + const environment = resolveDocuSignEnvironment(fields.dataCenter) + if (!environment) throw invalid('environment must be demo or production') + const integrationKey = fields.clientId.trim() + if (!DOCUSIGN_GUID_REGEX.test(integrationKey)) throw invalid('integration key is not a GUID') + const userId = fields.username?.trim() ?? '' + if (!DOCUSIGN_GUID_REGEX.test(userId)) throw invalid('impersonated user ID is not a GUID') + const accountId = fields.orgId.trim() || undefined + if (accountId && !DOCUSIGN_GUID_REGEX.test(accountId)) throw invalid('account ID is not a GUID') + const key = loadDocuSignPrivateKey(fields.privateKey?.trim() ?? '') + return { environment, integrationKey, userId, accountId, key } +} + +/** + * Maps a JWT Grant token error to an operator-facing hint for server logs. + * DocuSign reports every failure as HTTP 400 `{ error, error_description }`. + */ +function docuSignErrorHint(error: string, description: string): string | undefined { + if (error === 'consent_required') { + return 'the impersonated user has not granted (or revoked) consent for this integration key' + } + switch (description) { + case 'issuer_not_found': + return 'integration key not found in this environment' + case 'user_not_found': + return 'impersonated user ID not found in this environment' + case 'no_valid_keys_or_signatures': + return 'private key does not match an RSA keypair on this integration key, or the clock is skewed' + default: + return undefined + } +} + +function parseErrorBody(body: string): { error: string; description: string } { + try { + const parsed = JSON.parse(body) as { error?: unknown; error_description?: unknown } + return { + error: typeof parsed.error === 'string' ? parsed.error : '', + description: typeof parsed.error_description === 'string' ? parsed.error_description : '', + } + } catch { + return { error: '', description: '' } + } +} + +/** + * The eSignature REST base for one account, or `undefined` when its + * `base_uri` is not an https host of the credential's environment. + */ +function accountApiBase( + account: DocuSignUserinfoAccount, + environment: DocuSignEnvironment +): string | undefined { + if (typeof account.base_uri !== 'string' || typeof account.account_id !== 'string') return + if (!DOCUSIGN_GUID_REGEX.test(account.account_id)) return + let url: URL + try { + url = new URL(account.base_uri) + } catch { + return + } + if (url.protocol !== 'https:' || url.port || url.username || url.password) return + const host = url.hostname.toLowerCase() + const allowed = + environment === 'demo' + ? host === DEMO_BASE_URI_HOST + : host !== DEMO_BASE_URI_HOST && PRODUCTION_BASE_URI_HOST_REGEX.test(host) + if (!allowed) return + return `https://${host}/restapi/v2.1/accounts/${account.account_id.toLowerCase()}` +} + +/** + * Mints a DocuSign access token with the JWT Grant: an RS256 assertion + * (`iss` = integration key, `sub` = impersonated user, `aud` = the + * environment's authentication host, scope `signature impersonation`) posted + * to `https://{aud}/oauth/token`. Tokens last an hour and carry no refresh + * token, so they are re-minted. + * + * Every mint also reads `/oauth/userinfo` to pick the account (the pinned + * Account ID, else the user's default) and returns its eSignature REST base as + * `instanceUrl`, so tools never resolve the account against the deployment's + * OAuth host. DocuSign rate-limits userinfo per user per hour; one call per + * mint stays well inside it. + * + * Both hosts come only from the stored environment, never from input. + * @see https://developers.docusign.com/platform/auth/jwt/jwt-get-token/ + * @see https://developers.docusign.com/platform/auth/reference/user-info/ + */ +export async function mintDocuSignServiceAccountToken( + fields: ClientCredentialAccountFields, + options?: ClientCredentialAccountMintOptions +): Promise { + const { environment, integrationKey, userId, accountId, key } = validateDocuSignFields(fields) + const authHost = DOCUSIGN_ENVIRONMENTS[environment].authHost + + const now = Math.floor(Date.now() / 1000) + const assertion = await new SignJWT({ scope: DOCUSIGN_JWT_SCOPES }) + .setProtectedHeader({ alg: 'RS256', typ: 'JWT' }) + .setIssuer(integrationKey) + .setSubject(userId) + // DocuSign: "Do not include https:// in the aud value!" + .setAudience(authHost) + .setIssuedAt(now) + .setExpirationTime(now + JWT_ASSERTION_LIFETIME_SECONDS) + .sign(key) + + const tokenRes = await fetchProvider( + `https://${authHost}/oauth/token`, + { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ grant_type: JWT_BEARER_GRANT_TYPE, assertion }).toString(), + signal: options?.signal, + }, + TOKEN_STEP + ) + + if (!tokenRes.ok) { + const body = await readProviderErrorSnippet(tokenRes) + if ( + tokenRes.status >= 400 && + tokenRes.status < 500 && + !isTransientProviderStatus(tokenRes.status) + ) { + const { error, description } = parseErrorBody(body) + const hint = docuSignErrorHint(error, description) + throw new TokenServiceAccountValidationError( + error === 'consent_required' ? 'consent_required' : 'invalid_credentials', + tokenRes.status, + { step: TOKEN_STEP, environment, body, ...(hint ? { hint } : {}) } + ) + } + throw new TokenServiceAccountValidationError('provider_unavailable', tokenRes.status, { + step: TOKEN_STEP, + environment, + body, + }) + } + + const payload = await parseProviderJson(tokenRes, TOKEN_STEP) + if (typeof payload.access_token !== 'string' || !payload.access_token) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: TOKEN_STEP, + reason: 'token response missing access_token', + }) + } + const accessToken = payload.access_token + const lifetime = + typeof payload.expires_in === 'number' && payload.expires_in > 0 + ? payload.expires_in + : DEFAULT_TOKEN_LIFETIME_SECONDS + const expiresInSeconds = cacheLifetimeSeconds(lifetime) + + const userinfoRes = await fetchProvider( + `https://${authHost}/oauth/userinfo`, + { + headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' }, + signal: options?.signal, + }, + USERINFO_STEP + ) + if (!userinfoRes.ok) { + const body = await readProviderErrorSnippet(userinfoRes) + throw new TokenServiceAccountValidationError('provider_unavailable', userinfoRes.status, { + step: USERINFO_STEP, + environment, + body, + }) + } + const userinfo = await parseProviderJson(userinfoRes, USERINFO_STEP) + const accounts = (Array.isArray(userinfo.accounts) ? userinfo.accounts : []).filter( + (account): account is DocuSignUserinfoAccount => Boolean(account) && typeof account === 'object' + ) + const account = accountId + ? accounts.find( + (candidate) => + typeof candidate.account_id === 'string' && + candidate.account_id.toLowerCase() === accountId.toLowerCase() + ) + : (accounts.find((candidate) => candidate.is_default === true) ?? accounts[0]) + if (!account) { + throw new TokenServiceAccountValidationError('account_not_found', 400, { + step: USERINFO_STEP, + environment, + reason: accountId + ? 'the impersonated user is not a member of the pinned account' + : 'the impersonated user has no DocuSign accounts', + }) + } + const instanceUrl = accountApiBase(account, environment) + if (!instanceUrl) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: USERINFO_STEP, + environment, + reason: 'account base_uri is not an eSignature host of this environment', + }) + } + + if (options?.skipIdentity) { + return { accessToken, expiresInSeconds, instanceUrl } + } + + const resolvedAccountId = String(account.account_id).toLowerCase() + const accountName = + typeof account.account_name === 'string' && account.account_name + ? account.account_name + : undefined + const userName = typeof userinfo.name === 'string' && userinfo.name ? userinfo.name : undefined + const email = typeof userinfo.email === 'string' && userinfo.email ? userinfo.email : undefined + const sub = typeof userinfo.sub === 'string' && userinfo.sub ? userinfo.sub : userId + return { + accessToken, + expiresInSeconds, + instanceUrl, + identity: { + displayName: + [userName ?? email, accountName].filter(Boolean).join(' · ') || + `DocuSign ${resolvedAccountId}`, + principal: userPrincipal(sub, email), + auditMetadata: { docusignAccountId: resolvedAccountId, docusignEnvironment: environment }, + storedMetadata: { accountId: resolvedAccountId, environment, apiBase: instanceUrl }, + }, + } +} diff --git a/apps/sim/lib/credentials/client-credential-accounts/server.ts b/apps/sim/lib/credentials/client-credential-accounts/server.ts index c4bba3af9d1..ae13062cac4 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/server.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/server.ts @@ -2,6 +2,7 @@ import { BOX_SERVICE_ACCOUNT_PROVIDER_ID, CLIENT_CREDENTIAL_ACCOUNT_SECRET_TYPE, type ClientCredentialAccountProviderId, + DOCUSIGN_SERVICE_ACCOUNT_PROVIDER_ID, FIVETRAN_SERVICE_ACCOUNT_PROVIDER_ID, GREENHOUSE_SERVICE_ACCOUNT_PROVIDER_ID, getClientCredentialAccountDescriptor, @@ -19,6 +20,7 @@ import { ZOOM_SERVICE_ACCOUNT_PROVIDER_ID, } from '@/lib/credentials/client-credential-accounts/descriptors' import { mintBoxServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/box' +import { mintDocuSignServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/docusign' import { mintFivetranServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/fivetran' import { mintGreenhouseServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/greenhouse' import { mintHelpScoutServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/helpscout' @@ -69,7 +71,7 @@ export interface ClientCredentialAccountFields { * {@link clientSecret} in practice. */ privateKey?: string - /** Username a key-based grant authenticates as (Salesforce JWT `sub`). */ + /** User a key-based grant authenticates as (Salesforce JWT username, DocuSign user ID). */ username?: string } @@ -102,7 +104,8 @@ export interface ClientCredentialAccountMintResult { expiresInSeconds: number /** * Provider API destination the minted token must be used against (Salesforce, - * NetSuite, or Oracle EPM), forwarded to tools alongside the token. + * NetSuite, Oracle EPM, or the DocuSign account's eSignature REST base), + * forwarded to tools alongside the token. */ instanceUrl?: string /** @@ -157,6 +160,7 @@ const CLIENT_CREDENTIAL_ACCOUNT_MINTERS: Record< [ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID]: mintOracleEpmServiceAccountToken, [GREENHOUSE_SERVICE_ACCOUNT_PROVIDER_ID]: mintGreenhouseServiceAccountToken, [HELPSCOUT_SERVICE_ACCOUNT_PROVIDER_ID]: mintHelpScoutServiceAccountToken, + [DOCUSIGN_SERVICE_ACCOUNT_PROVIDER_ID]: mintDocuSignServiceAccountToken, } export function getClientCredentialAccountMinter( diff --git a/apps/sim/lib/credentials/databricks-service-account.server.test.ts b/apps/sim/lib/credentials/databricks-service-account.server.test.ts new file mode 100644 index 00000000000..26b610ba9e5 --- /dev/null +++ b/apps/sim/lib/credentials/databricks-service-account.server.test.ts @@ -0,0 +1,241 @@ +/** + * @vitest-environment node + */ + +import { queueTableRows, resetDbChainMock } from '@sim/testing' +import { jsonResponse } from '@sim/testing/helpers/http' +import { encryptionMock, encryptionMockFns } from '@sim/testing/mocks/encryption.mock' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const hoisted = vi.hoisted(() => ({ + mint: vi.fn(async ({ subject }: { subject: string }) => `token-for:${subject}`), +})) + +vi.mock('@/lib/core/security/encryption', () => encryptionMock) +vi.mock('@/lib/credentials/workload-identity/issuer', () => ({ + isWorkloadIdentityConfigured: async () => true, + MAX_WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS: 3600, + mintWorkloadIdentityToken: hoisted.mint, +})) + +import { credential } from '@sim/db/schema' +import { + resolveDatabricksCredentialToken, + verifyAndEncryptDatabricksCredential, +} from '@/lib/credentials/databricks-service-account.server' +import { WorkloadIdentityNotConfiguredError } from '@/lib/credentials/workload-identity/subject' +import { DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/oauth/types' + +const owner = { workspaceId: 'ws-1', actorUserId: 'user-1' } + +function patConfig(host: string) { + return JSON.stringify({ authType: 'personal_access_token', host, token: 'dapi-secret' }) +} + +describe('saving a Databricks connection', () => { + const fetch = vi.fn() + + beforeEach(() => { + fetch.mockReset().mockResolvedValue(jsonResponse({ userName: 'me@example.com' })) + vi.stubGlobal('fetch', fetch) + encryptionMockFns.mockEncryptSecret.mockResolvedValue({ encrypted: 'ciphertext', iv: 'iv' }) + }) + + it('verifies a Databricks workspace host by calling only that host', async () => { + await expect( + verifyAndEncryptDatabricksCredential( + patConfig('dbc-1234abcd-5678.cloud.databricks.com'), + owner + ) + ).resolves.toMatchObject({ identity: 'me@example.com' }) + expect(fetch).toHaveBeenCalledWith( + 'https://dbc-1234abcd-5678.cloud.databricks.com/api/2.0/preview/scim/v2/Me', + expect.objectContaining({ redirect: 'error' }) + ) + }) + + it('refuses an identity reply over the size cap instead of saving the host as the identity', async () => { + fetch.mockResolvedValue( + jsonResponse({ userName: 'me@example.com', groups: 'g'.repeat(1024 * 1024) }) + ) + + await expect( + verifyAndEncryptDatabricksCredential( + patConfig('dbc-1234abcd-5678.cloud.databricks.com'), + owner + ) + ).rejects.toThrow(/Could not verify/) + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + it.each([ + 'attacker.example.com', + 'https://attacker.example.com', + 'dbc-1234abcd-5678.cloud.databricks.com.attacker.example.com', + '127.0.0.1', + ])('refuses %s without sending the token anywhere', async (host) => { + await expect(verifyAndEncryptDatabricksCredential(patConfig(host), owner)).rejects.toThrow() + expect(fetch).not.toHaveBeenCalled() + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) +}) + +describe('saving a keyless Databricks connection', () => { + const HOST = 'dbc-1234abcd-5678.cloud.databricks.com' + const CLIENT_ID = '00000000-0000-0000-0000-000000000000' + const SUBJECT = 'sim:workspace:ws-1:credential:abcdef0123456789' + const fetch = vi.fn() + const config = { authType: 'oidc_federation', host: HOST, clientId: CLIENT_ID } + const reconnectOwner = { + ...owner, + storedSecret: { type: 'databricks_credential_v1', config: { ...config, subject: SUBJECT } }, + } + + /** A service principal whose federation policy pins `subject` to {@link SUBJECT}, or trusts any subject. */ + function principalTrusting(pinsSubject: boolean, probeStatus = 401) { + fetch.mockImplementation(async (url: string, init: RequestInit) => { + if (url.endsWith('/oidc/v1/token')) { + const body = new URLSearchParams(String(init.body)) + const trusted = !pinsSubject || body.get('subject_token') === `token-for:${SUBJECT}` + return trusted + ? jsonResponse({ access_token: 'dbx-token' }) + : jsonResponse({ error: 'invalid_grant' }, probeStatus) + } + return jsonResponse({ userName: 'sim-workflows' }) + }) + } + + beforeEach(() => { + fetch.mockReset() + vi.stubGlobal('fetch', fetch) + encryptionMockFns.mockEncryptSecret.mockReset() + encryptionMockFns.mockEncryptSecret.mockResolvedValue({ encrypted: 'ciphertext', iv: 'iv' }) + }) + + it('exchanges this credential’s token as the service principal and saves', async () => { + principalTrusting(true) + + await expect( + verifyAndEncryptDatabricksCredential(JSON.stringify(config), reconnectOwner) + ).resolves.toMatchObject({ identity: 'sim-workflows' }) + const exchange = fetch.mock.calls + .map(([url, init]) => ({ url, body: new URLSearchParams(String(init?.body)) })) + .find(({ body }) => body.get('subject_token') === `token-for:${SUBJECT}`) + expect(exchange?.url).toBe(`https://${HOST}/oidc/v1/token`) + expect(Object.fromEntries(exchange?.body ?? [])).toEqual({ + client_id: CLIENT_ID, + subject_token: `token-for:${SUBJECT}`, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + scope: 'all-apis', + }) + }) + + it('reconnects to a keyless refusal with its code when signing stops mid-save', async () => { + principalTrusting(true) + hoisted.mint.mockRejectedValueOnce(new WorkloadIdentityNotConfiguredError()) + + await expect( + verifyAndEncryptDatabricksCredential(JSON.stringify(config), reconnectOwner) + ).rejects.toMatchObject({ detailCode: 'WORKLOAD_IDENTITY_NOT_CONFIGURED' }) + }) + + it('refuses a federation policy that trusts any Sim subject, so no other workspace can use it', async () => { + principalTrusting(false) + + await expect( + verifyAndEncryptDatabricksCredential(JSON.stringify(config), reconnectOwner) + ).rejects.toThrow(/also trusts other Sim credentials/) + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + it('does not save when the any-subject check cannot be completed', async () => { + principalTrusting(true, 503) + + await expect( + verifyAndEncryptDatabricksCredential(JSON.stringify(config), reconnectOwner) + ).rejects.toThrow(/could not confirm/) + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) +}) + +describe('getting a Databricks token for a run', () => { + const fetch = vi.fn() + + beforeEach(() => { + resetDbChainMock() + queueTableRows(credential, [ + { + workspaceId: 'ws-1', + organizationId: null, + type: 'service_account', + providerId: DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, + revokedAt: null, + encryptedServiceAccountKey: 'ciphertext', + }, + ]) + encryptionMockFns.mockDecryptSecret.mockResolvedValueOnce({ + decrypted: JSON.stringify({ + type: 'databricks_credential_v1', + config: { + authType: 'service_principal', + host: 'dbc-1234abcd-5678.cloud.databricks.com', + clientId: '00000000-0000-0000-0000-000000000000', + clientSecret: 'dose-secret', + }, + }), + }) + vi.stubGlobal('fetch', fetch) + }) + + it('reports a refused client secret as a credential to reconnect', async () => { + fetch.mockResolvedValue(jsonResponse({ error: 'invalid_client' }, 401)) + + await expect(resolveDatabricksCredentialToken('cred-1')).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) + + it('reports a Databricks outage as the provider being unavailable, not a bad credential', async () => { + fetch.mockResolvedValue(jsonResponse({ error: 'temporarily_unavailable' }, 503)) + + await expect(resolveDatabricksCredentialToken('cred-1')).rejects.toMatchObject({ + code: 'provider_unavailable', + }) + }) + + it('reports a token reply over the size cap as oversized, not as a refused request', async () => { + fetch.mockResolvedValue(jsonResponse({ access_token: 't'.repeat(1024 * 1024) }, 200)) + + await expect(resolveDatabricksCredentialToken('cred-1')).rejects.toMatchObject({ + code: 'provider_unavailable', + status: 502, + logDetail: { reason: 'Databricks token response exceeded the size limit' }, + }) + }) + + it('reports a token reply cut off mid-body as the provider being unavailable, not a bad credential', async () => { + const body = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('{"error":"invalid_cl')) + controller.error(new Error('socket hang up')) + }, + }) + fetch.mockResolvedValue( + new Response(body, { status: 401, headers: { 'Content-Type': 'application/json' } }) + ) + + await expect(resolveDatabricksCredentialToken('cred-1')).rejects.toMatchObject({ + code: 'provider_unavailable', + logDetail: { reason: 'Databricks token response could not be read' }, + }) + }) + + it('reports a token reply without a token as the provider misbehaving, not a bad credential', async () => { + fetch.mockResolvedValue(jsonResponse({}, 200)) + + await expect(resolveDatabricksCredentialToken('cred-1')).rejects.toMatchObject({ + code: 'provider_unavailable', + }) + }) +}) diff --git a/apps/sim/lib/credentials/databricks-service-account.server.ts b/apps/sim/lib/credentials/databricks-service-account.server.ts new file mode 100644 index 00000000000..287dd4ff731 --- /dev/null +++ b/apps/sim/lib/credentials/databricks-service-account.server.ts @@ -0,0 +1,428 @@ +import { db } from '@sim/db' +import { credential } from '@sim/db/schema' +import { getErrorMessage } from '@sim/utils/errors' +import { eq } from 'drizzle-orm' +import { z } from 'zod' +import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' +import { validateDatabricksWorkspaceHost } from '@/lib/core/security/input-validation' +import { isPayloadSizeLimitError, readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' +import { failureClause, parseServiceAccountConfig } from '@/lib/credentials/service-account-config' +import { + providerFailureCode, + TokenServiceAccountValidationError, +} from '@/lib/credentials/token-service-accounts/errors' +import { WORKLOAD_IDENTITY_AUDIENCES } from '@/lib/credentials/workload-identity/audiences' +import { + isWorkloadIdentityConfigured, + MAX_WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS, + mintWorkloadIdentityToken, + type WorkloadIdentityOwner, +} from '@/lib/credentials/workload-identity/issuer' +import { + bindWorkloadIdentitySubject, + KeylessCredentialRefusedError, + newWorkloadIdentitySubject, + type WorkloadIdentityCredentialOwner, + WorkloadIdentityNotConfiguredError, +} from '@/lib/credentials/workload-identity/subject' +import { DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/oauth/types' + +/** + * Saved Databricks connections for the Databricks block. + * + * - `personal_access_token`: a PAT used as-is, like an inline token. + * - `service_principal`: OAuth machine-to-machine — the service principal's + * client ID and secret mint a workspace token per run. + * - `oidc_federation`: no stored secret — Sim signs a short-lived OIDC token + * for this credential's subject and exchanges it (RFC 8693) for a token of + * the service principal whose federation policy trusts it. + * + * Every mode resolves server-side to a bearer token plus the workspace host + * (`accessToken`, `domain`), which fill the tools' `apiKey` and `host`. + */ + +const DATABRICKS_CREDENTIAL_SECRET_TYPE = 'databricks_credential_v1' as const +const DATABRICKS_WORKLOAD_IDENTITY_AUDIENCE = WORKLOAD_IDENTITY_AUDIENCES.databricks + +const REQUEST_TIMEOUT_MS = 15_000 +/** Token and identity responses are small; the cap keeps a misbehaving host from exhausting memory. */ +const MAX_RESPONSE_BYTES = 1024 * 1024 +const CLIENT_ID_PATTERN = /^[A-Za-z0-9-]{8,128}$/ + +/** Workspace origin with Databricks' own host allowlist as the SSRF guard. */ +function workspaceOrigin(host: string): string { + const result = validateDatabricksWorkspaceHost(host.trim().replace(/^http:\/\//i, ''), 'host') + if (!result.isValid || !result.sanitized) { + throw new DatabricksCredentialError(result.error || 'Invalid Databricks workspace host') + } + return result.sanitized +} + +const hostSchema = z + .string() + .trim() + .min(1, 'Databricks workspace host is required') + .max(255) + .transform((value, context) => { + try { + return new URL(workspaceOrigin(value)).host + } catch (error) { + context.addIssue({ code: 'custom', message: getErrorMessage(error, 'Invalid host') }) + return z.NEVER + } + }) + +const clientIdSchema = z.string().trim().regex(CLIENT_ID_PATTERN, 'Invalid client ID') + +const oidcShape = { + authType: z.literal('oidc_federation'), + host: hostSchema, + clientId: clientIdSchema, + subject: z.string().min(1).max(512), +} + +const patSchema = z + .object({ + authType: z.literal('personal_access_token'), + host: hostSchema, + token: z.string().trim().min(1, 'Personal access token is required').max(4096), + }) + .strict() + +const servicePrincipalSchema = z + .object({ + authType: z.literal('service_principal'), + host: hostSchema, + clientId: clientIdSchema, + clientSecret: z.string().min(1, 'OAuth secret is required').max(4096), + }) + .strict() + +const storedConfigSchema = z.discriminatedUnion('authType', [ + patSchema, + servicePrincipalSchema, + z.object(oidcShape).strict(), +]) + +type DatabricksCredentialConfig = z.infer + +const submittedConfigSchema = z.discriminatedUnion('authType', [ + patSchema, + servicePrincipalSchema, + z + .object({ + ...oidcShape, + subject: oidcShape.subject.optional(), + subjectProof: z.string().min(1).max(512).optional(), + }) + .strict(), +]) + +const storedSecretSchema = z + .object({ type: z.literal(DATABRICKS_CREDENTIAL_SECRET_TYPE), config: storedConfigSchema }) + .strict() + +export class DatabricksCredentialError extends Error { + /** @param status Databricks' HTTP status, when Databricks itself refused the request. */ + constructor( + message: string, + readonly status?: number + ) { + super(message) + this.name = 'DatabricksCredentialError' + } +} + +async function requestWorkspaceToken( + host: string, + body: URLSearchParams, + signal: AbortSignal | undefined, + headers: Record = {} +): Promise { + const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS) + const response = await fetch(`${workspaceOrigin(host)}/oidc/v1/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...headers }, + body, + // A secret or subject token must never follow a redirect off the workspace host. + redirect: 'error', + signal: signal ? AbortSignal.any([signal, timeout]) : timeout, + }) + const payload = (await readResponseJsonWithLimit(response, { + maxBytes: MAX_RESPONSE_BYTES, + label: 'Databricks token response', + }).catch((error: unknown) => { + if (error instanceof SyntaxError) return {} + signal?.throwIfAborted() + if (isPayloadSizeLimitError(error)) { + // A refusal's status still holds; an oversized success is the provider misbehaving. + throw new DatabricksCredentialError( + 'Databricks token response exceeded the size limit', + response.ok ? 502 : response.status + ) + } + // An interrupted body says nothing about the credential, whatever the status line was. + throw new DatabricksCredentialError('Databricks token response could not be read', 502) + })) as { + access_token?: unknown + error_description?: unknown + error?: unknown + } + if (!response.ok || typeof payload.access_token !== 'string' || !payload.access_token) { + const reason = + typeof payload.error_description === 'string' + ? payload.error_description + : typeof payload.error === 'string' + ? payload.error + : `HTTP ${response.status}` + throw new DatabricksCredentialError( + `Databricks rejected the token request: ${reason}`, + response.status + ) + } + return payload.access_token +} + +/** A bearer token for the configured workspace. */ +async function resolveDatabricksAccessToken( + config: DatabricksCredentialConfig, + owner: WorkloadIdentityOwner, + signal?: AbortSignal +): Promise { + if (config.authType === 'personal_access_token') return config.token + if (config.authType === 'service_principal') { + const basic = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64') + return requestWorkspaceToken( + config.host, + new URLSearchParams({ grant_type: 'client_credentials', scope: 'all-apis' }), + signal, + { Authorization: `Basic ${basic}` } + ) + } + const subjectToken = await mintWorkloadIdentityToken({ + subject: config.subject, + audience: DATABRICKS_WORKLOAD_IDENTITY_AUDIENCE, + owner, + // Databricks' token expires with this one, and Genie polls for up to five minutes. + ttlSeconds: MAX_WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS, + }) + return requestWorkspaceToken( + config.host, + new URLSearchParams({ + client_id: config.clientId, + subject_token: subjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + scope: 'all-apis', + }), + signal + ) +} + +/** The auth mode a decrypted Databricks secret holds, or undefined when it is not one. */ +export function storedDatabricksAuthType( + decrypted: string +): DatabricksCredentialConfig['authType'] | undefined { + try { + const parsed = storedSecretSchema.safeParse(JSON.parse(decrypted)) + return parsed.success ? parsed.data.config.authType : undefined + } catch { + return undefined + } +} + +function readStoredSubject(storedSecret: Record | null | undefined) { + const parsed = storedSecretSchema.safeParse(storedSecret) + return parsed.success && parsed.data.config.authType === 'oidc_federation' + ? parsed.data.config.subject + : undefined +} + +function bindSubject( + subject: string | undefined, + subjectProof: string | undefined, + owner: WorkloadIdentityCredentialOwner +): string { + return bindWorkloadIdentitySubject({ + provider: 'databricks', + owner, + storedSubject: readStoredSubject(owner.storedSecret), + subject, + subjectProof, + }) +} + +/** Proves a configuration by calling the workspace's SCIM `Me` with the resolved token. */ +async function verifyWorkspaceIdentity( + config: DatabricksCredentialConfig, + owner: WorkloadIdentityOwner +): Promise { + try { + const token = await resolveDatabricksAccessToken(config, owner) + const response = await fetch(`${workspaceOrigin(config.host)}/api/2.0/preview/scim/v2/Me`, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + redirect: 'error', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + if (!response.ok) throw new Error(`workspace returned HTTP ${response.status}`) + // A malformed body only loses the display name; an oversized one must fail + // verification rather than save the host as the identity. + const me = (await readResponseJsonWithLimit(response, { + maxBytes: MAX_RESPONSE_BYTES, + label: 'Databricks identity response', + }).catch((error: unknown) => { + if (error instanceof SyntaxError) return {} + throw error + })) as { + userName?: unknown + displayName?: unknown + } + return ( + (typeof me.userName === 'string' && me.userName) || + (typeof me.displayName === 'string' && me.displayName) || + config.host + ) + } catch (error) { + // A keyless refusal already names its cause and code; wrapping it would lose both. + if (error instanceof KeylessCredentialRefusedError) throw error + throw new DatabricksCredentialError( + `Could not verify the Databricks credential: ${getErrorMessage(error, 'Unknown error')}` + ) + } +} + +/** + * Sim signs tokens for any service principal a member names, and every Sim + * tenant shares the issuer and audience, so a federation policy that matches on + * a claim other than `sub` (a `subject_claim` of `sim_workspace_id`, say) would + * let other Sim credentials act as the principal. Exchanging a subject no + * credential holds must be refused before the credential is saved. + */ +async function assertPolicyPinsSubject( + config: Extract, + owner: WorkloadIdentityOwner +): Promise { + try { + await resolveDatabricksAccessToken( + { ...config, subject: newWorkloadIdentitySubject(owner) }, + owner, + AbortSignal.timeout(REQUEST_TIMEOUT_MS) + ) + } catch (error) { + const refused = + error instanceof DatabricksCredentialError && + error.status !== undefined && + providerFailureCode(error.status) === 'invalid_credentials' + if (refused) return + if (error instanceof KeylessCredentialRefusedError) throw error + throw new DatabricksCredentialError( + `Sim could not confirm the federation policy limits this service principal to this credential: ${failureClause(error)}. Try again.` + ) + } + throw new DatabricksCredentialError( + `This service principal's federation policy also trusts other Sim credentials. Use the federation policy from Sim (the connect dialog or \`sim credentials keyless-subject\`), which matches the sub claim to ${config.subject}, so only this credential can use it.` + ) +} + +/** + * Validates the submitted configuration, proves it by calling the workspace's + * SCIM `Me` endpoint with the resolved token, and returns the encrypted blob, + * the identity Databricks reports, and the keyless subject it newly binds, if + * any (a reconnect keeping its stored subject binds none). + */ +export async function verifyAndEncryptDatabricksCredential( + serialized: string, + owner: WorkloadIdentityCredentialOwner +): Promise<{ encryptedServiceAccountKey: string; identity: string; newSubject?: string }> { + const parsed = parseServiceAccountConfig( + serialized, + submittedConfigSchema, + 'Databricks credential' + ) + if (!parsed.success) throw new DatabricksCredentialError(parsed.message) + const submitted = parsed.data + let config: DatabricksCredentialConfig + if (submitted.authType === 'oidc_federation') { + if (!(await isWorkloadIdentityConfigured())) { + throw new WorkloadIdentityNotConfiguredError( + 'Use a personal access token or a service principal OAuth secret.' + ) + } + const { subject, subjectProof, ...rest } = submitted + config = { ...rest, subject: bindSubject(subject, subjectProof, owner) } + } else { + config = submitted + } + + // The subject-pin probe is independent of the verification, so both run at + // once; a verification failure still reports first. + const [verified, pinned] = await Promise.allSettled([ + verifyWorkspaceIdentity(config, owner), + config.authType === 'oidc_federation' ? assertPolicyPinsSubject(config, owner) : undefined, + ]) + if (verified.status === 'rejected') throw verified.reason + if (pinned.status === 'rejected') throw pinned.reason + const identity = verified.value + + const { encrypted } = await encryptSecret( + JSON.stringify({ type: DATABRICKS_CREDENTIAL_SECRET_TYPE, config }) + ) + return { + encryptedServiceAccountKey: encrypted, + identity, + ...(config.authType === 'oidc_federation' && + config.subject !== readStoredSubject(owner.storedSecret) + ? { newSubject: config.subject } + : {}), + } +} + +/** + * Resolves an executor-authorized Databricks credential (the executor already + * checked the caller may use it) to a token and its workspace host. + */ +export async function resolveDatabricksCredentialToken( + credentialId: string, + signal?: AbortSignal +): Promise<{ accessToken: string; domain: string }> { + const [row] = await db + .select({ + workspaceId: credential.workspaceId, + organizationId: credential.organizationId, + type: credential.type, + providerId: credential.providerId, + revokedAt: credential.revokedAt, + encryptedServiceAccountKey: credential.encryptedServiceAccountKey, + }) + .from(credential) + .where(eq(credential.id, credentialId)) + .limit(1) + if ( + !row || + row.type !== 'service_account' || + row.providerId !== DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID || + row.revokedAt !== null || + !row.encryptedServiceAccountKey + ) { + throw new DatabricksCredentialError('Databricks credential is unavailable') + } + const { decrypted } = await decryptSecret(row.encryptedServiceAccountKey) + const { config } = storedSecretSchema.parse(JSON.parse(decrypted)) + const owner: WorkloadIdentityOwner = row.workspaceId + ? { workspaceId: row.workspaceId } + : { organizationId: row.organizationId ?? undefined } + try { + return { + accessToken: await resolveDatabricksAccessToken(config, owner, signal), + domain: config.host, + } + } catch (error) { + if (!(error instanceof DatabricksCredentialError)) throw error + // Report in the shared service-account codes: an outage is not a bad credential. + throw new TokenServiceAccountValidationError( + error.status === undefined ? 'invalid_credentials' : providerFailureCode(error.status), + error.status ?? 400, + { step: 'token', reason: error.message } + ) + } +} diff --git a/apps/sim/lib/credentials/gitlab-personal-token.test.ts b/apps/sim/lib/credentials/gitlab-personal-token.test.ts index 689c31e76ea..825d9851022 100644 --- a/apps/sim/lib/credentials/gitlab-personal-token.test.ts +++ b/apps/sim/lib/credentials/gitlab-personal-token.test.ts @@ -8,6 +8,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) vi.mock('@/lib/core/security/encryption', () => encryptionMock) +import { EgressRefusedError } from '@/lib/core/network/transport.server' import { decryptPersonalToken, encryptPersonalToken, @@ -96,6 +97,28 @@ describe('GitLab personal token verification', () => { await expect(verifyGitLabPersonalToken('secret')).rejects.toThrow('GitLab rejected this token') expect(mocks.fetch).toHaveBeenCalledTimes(1) }) + it('reports a host the egress policy refuses as the user input', async () => { + mocks.fetch + .mockReset() + .mockRejectedValue(new EgressRefusedError('url resolves to a private address')) + await expect(verifyGitLabPersonalToken('secret', 'gitlab.internal')).rejects.toMatchObject({ + code: 'validation', + message: expect.stringContaining("can't connect to that address"), + }) + }) + it('reports a GitLab host that does not resolve as the user input', async () => { + mocks.fetch.mockReset().mockRejectedValue( + Object.assign(new Error('getaddrinfo ENOTFOUND gitlab.example.invalid'), { + code: 'ENOTFOUND', + }) + ) + await expect( + verifyGitLabPersonalToken('secret', 'gitlab.example.invalid') + ).rejects.toMatchObject({ + code: 'validation', + message: expect.stringContaining("couldn't find that GitLab host"), + }) + }) it('binds organization tokens independently of the execution workspace', async () => { mocks.encrypt.mockResolvedValue({ encrypted: 'ciphertext' }) const { workspaceId: _workspaceId, ...identity } = envelope diff --git a/apps/sim/lib/credentials/gitlab-personal-token.ts b/apps/sim/lib/credentials/gitlab-personal-token.ts index e1253723839..a0ee529c7b0 100644 --- a/apps/sim/lib/credentials/gitlab-personal-token.ts +++ b/apps/sim/lib/credentials/gitlab-personal-token.ts @@ -1,8 +1,10 @@ import { z } from 'zod' +import { EgressRefusedError } from '@/lib/core/network/transport.server' import { OrchestrationError } from '@/lib/core/orchestration/types' import { resourceScopeFromOwner, sameResourceScope } from '@/lib/core/resource-scope' import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' +import { isHostNotFound } from '@/lib/credentials/token-service-accounts/errors' import { normalizeGitLabHost } from '@/tools/gitlab/utils' const gitLabUserSchema = z.object({ @@ -55,6 +57,20 @@ export async function verifyGitLabPersonalToken(accessToken: string, rawHost?: s timeout: 10_000, maxResponseBytes: 64 * 1024, maxRedirects: 0, + }).catch((error: unknown) => { + if (error instanceof EgressRefusedError) { + throw new OrchestrationError( + 'validation', + "Sim can't connect to that address. Use your GitLab instance's public https URL." + ) + } + if (isHostNotFound(error)) { + throw new OrchestrationError( + 'validation', + "Sim couldn't find that GitLab host. Check the address and try again." + ) + } + throw error }) if (!response.ok) { throw new OrchestrationError( diff --git a/apps/sim/lib/credentials/google-workload-identity.server.test.ts b/apps/sim/lib/credentials/google-workload-identity.server.test.ts new file mode 100644 index 00000000000..88f8b8d8ca4 --- /dev/null +++ b/apps/sim/lib/credentials/google-workload-identity.server.test.ts @@ -0,0 +1,403 @@ +/** + * @vitest-environment node + */ + +import { jsonResponse } from '@sim/testing/helpers/http' +import { encryptionMock, encryptionMockFns } from '@sim/testing/mocks/encryption.mock' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { workloadIdentityConfigured } = vi.hoisted(() => ({ + workloadIdentityConfigured: { value: true }, +})) + +vi.mock('@/lib/core/security/encryption', () => encryptionMock) +vi.mock('@/lib/credentials/workload-identity/issuer', () => ({ + isWorkloadIdentityConfigured: async () => workloadIdentityConfigured.value, + mintWorkloadIdentityToken: async ({ subject, audience }: { subject: string; audience: string }) => + `sim-oidc|${audience}|${subject}`, +})) + +import { + resolveGoogleWorkloadIdentityToken, + verifyAndEncryptGoogleWorkloadIdentityCredential, +} from '@/lib/credentials/google-workload-identity.server' +import { issueWorkloadIdentitySubject } from '@/lib/credentials/workload-identity/subject' + +const PROJECT_NUMBER = '123456789012' +const POOL = 'sim-pool' +const PROVIDER = 'sim-oidc' +const SA_EMAIL = 'bq-runner@analytics-prod.iam.gserviceaccount.com' +const PROVIDER_NAME = `projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL}/providers/${PROVIDER}` +const SUBJECT = 'sim:workspace:ws-1:credential:abcdef0123456789' +const IMPERSONATE_URL = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${encodeURIComponent(SA_EMAIL)}:generateAccessToken` +const CLOUD_PLATFORM = 'https://www.googleapis.com/auth/cloud-platform' +const BIGQUERY = 'https://www.googleapis.com/auth/bigquery' + +const owner = { workspaceId: 'ws-1', actorUserId: 'user-1' } +const storedConfig = { + projectNumber: PROJECT_NUMBER, + poolId: POOL, + providerId: PROVIDER, + serviceAccountEmail: SA_EMAIL, + subject: SUBJECT, +} +const reconnectOwner = { + ...owner, + storedSecret: { type: 'google_workload_identity_v1', config: storedConfig }, +} + +interface RecordedRequest { + url: string + headers: Record + body: Record + redirect: RequestRedirect | undefined +} + +/** + * A fake Google STS and IAM Credentials. STS accepts a Sim token + * only for the provider's own audience and for subjects `acceptSubject` + * allows, and returns a federated token naming the subject; IAM lets the + * subjects `canImpersonate` allows impersonate the service account. + */ +function fakeGoogle( + fetch: ReturnType, + options: { + acceptSubject?: (subject: string) => boolean + canImpersonate?: (subject: string) => boolean + stsError?: Response + iamError?: Response + } = {} +) { + const sts: RecordedRequest[] = [] + const iam: RecordedRequest[] = [] + fetch.mockImplementation(async (url: string, init: RequestInit = {}) => { + const record = () => ({ + url, + headers: Object.fromEntries(new Headers(init.headers).entries()), + body: init.body ? JSON.parse(String(init.body)) : {}, + redirect: init.redirect, + }) + if (url === 'https://sts.googleapis.com/v1/token') { + const request = record() + sts.push(request) + if (options.stsError) return options.stsError.clone() + const [, audience, subject] = String(request.body.subjectToken).split('|') + if ( + audience !== `https://iam.googleapis.com/${PROVIDER_NAME}` || + options.acceptSubject?.(subject) === false + ) { + return jsonResponse( + { error: 'invalid_grant', error_description: 'The audience in ID Token does not match' }, + 400 + ) + } + return jsonResponse({ + access_token: `fed:${subject}`, + issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', + token_type: 'Bearer', + expires_in: 3600, + }) + } + if (url.startsWith('https://iamcredentials.googleapis.com/')) { + const request = record() + iam.push(request) + if (options.iamError) return options.iamError.clone() + const subject = (request.headers.authorization ?? '').replace('Bearer fed:', '') + if (options.canImpersonate && !options.canImpersonate(subject)) { + return jsonResponse( + { error: { code: 403, status: 'PERMISSION_DENIED', message: 'denied' } }, + 403 + ) + } + return jsonResponse({ + accessToken: `ya29.sa-token-for-${(request.body.scope as string[]).join(',')}`, + expireTime: new Date(Date.now() + 3600_000).toISOString(), + }) + } + return jsonResponse({}, 404) + }) + return { sts, iam } +} + +function save( + config: Record, + saver: Parameters[1] = reconnectOwner +) { + return verifyAndEncryptGoogleWorkloadIdentityCredential( + JSON.stringify({ + authType: 'workload_identity', + projectNumber: PROJECT_NUMBER, + poolId: POOL, + providerId: PROVIDER, + serviceAccountEmail: SA_EMAIL, + ...config, + }), + saver + ) +} + +describe('saving a keyless Google Cloud credential', () => { + const fetch = vi.fn() + + beforeEach(() => { + fetch.mockReset() + vi.stubGlobal('fetch', fetch) + workloadIdentityConfigured.value = true + encryptionMockFns.mockEncryptSecret.mockReset() + encryptionMockFns.mockEncryptSecret.mockResolvedValue({ encrypted: 'ciphertext', iv: 'iv' }) + }) + + it('exchanges a Sim token for the provider’s audience at STS, then impersonates the service account', async () => { + const google = fakeGoogle(fetch, { canImpersonate: (subject) => subject === SUBJECT }) + + await expect(save({})).resolves.toEqual({ + encryptedServiceAccountKey: 'ciphertext', + serviceAccountEmail: SA_EMAIL, + workloadIdentityProvider: PROVIDER_NAME, + }) + const exchange = google.sts.find((request) => + String(request.body.subjectToken).endsWith(`|${SUBJECT}`) + ) + expect(exchange).toEqual({ + url: 'https://sts.googleapis.com/v1/token', + redirect: 'error', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + body: { + grantType: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: `//iam.googleapis.com/${PROVIDER_NAME}`, + scope: CLOUD_PLATFORM, + requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token', + subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt', + subjectToken: `sim-oidc|https://iam.googleapis.com/${PROVIDER_NAME}|${SUBJECT}`, + }, + }) + expect(google.iam).toContainEqual({ + url: IMPERSONATE_URL, + redirect: 'error', + headers: { + 'content-type': 'application/json', + accept: 'application/json', + authorization: `Bearer fed:${SUBJECT}`, + }, + body: { scope: [CLOUD_PLATFORM], lifetime: '3600s' }, + }) + expect(encryptionMockFns.mockEncryptSecret).toHaveBeenCalledWith( + JSON.stringify({ type: 'google_workload_identity_v1', config: storedConfig }) + ) + }) + + it('names the provider settings to check when STS refuses the token', async () => { + fakeGoogle(fetch, { acceptSubject: () => false }) + + await expect(save({})).rejects.toThrow( + /refused Sim’s token for this workload identity provider.*\(invalid_grant\)/ + ) + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + it('names the binding to add when the subject may not impersonate the service account', async () => { + fakeGoogle(fetch, { canImpersonate: () => false }) + + await expect(save({})).rejects.toThrow( + `Grant roles/iam.workloadIdentityUser on the service account to principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL}/subject/${SUBJECT}` + ) + }) + + it('refuses a service account that any Sim subject can impersonate', async () => { + fakeGoogle(fetch) + + await expect(save({})).rejects.toThrow(/also trusts other Sim credentials/) + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + it.each([ + { + label: 'STS is unavailable for the stray subject', + fail: (url: string, body: string) => + url.startsWith('https://sts.') && !body.includes(SUBJECT) + ? jsonResponse({ error: 'temporarily_unavailable' }, 503) + : undefined, + }, + { + label: 'IAM is unreachable for the stray subject', + fail: (url: string, _body: string, auth: string) => { + if (url.startsWith('https://iamcredentials.') && auth !== `Bearer fed:${SUBJECT}`) { + throw new TypeError('fetch failed') + } + return undefined + }, + }, + ])('refuses to save when it cannot confirm the subject is pinned: $label', async ({ fail }) => { + fakeGoogle(fetch, { canImpersonate: (subject) => subject === SUBJECT }) + const google = fetch.getMockImplementation() + fetch.mockImplementation(async (url: string, init: RequestInit = {}) => { + const auth = new Headers(init.headers).get('authorization') ?? '' + return fail(url, String(init.body ?? ''), auth) ?? google?.(url, init) + }) + + await expect(save({})).rejects.toThrow(/could not confirm this service account trusts only/) + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + it('saves when the provider’s attribute condition refuses every other subject', async () => { + const google = fakeGoogle(fetch, { acceptSubject: (subject) => subject === SUBJECT }) + + await expect(save({})).resolves.toMatchObject({ serviceAccountEmail: SA_EMAIL }) + expect(google.sts.some((request) => !String(request.body.subjectToken).endsWith(SUBJECT))).toBe( + true + ) + }) + + it('refuses a subject issued to another member, without contacting Google', async () => { + fakeGoogle(fetch) + const grant = issueWorkloadIdentitySubject('google', { workspaceId: 'ws-1' }, 'user-2') + + await expect( + save({ subject: grant.subject, subjectProof: grant.subjectProof }, owner) + ).rejects.toThrow(/belongs to someone else/) + expect(fetch).not.toHaveBeenCalled() + }) + + it('accepts a subject issued to the saving member', async () => { + const grant = issueWorkloadIdentitySubject('google', { workspaceId: 'ws-1' }, 'user-1') + fakeGoogle(fetch, { canImpersonate: (subject) => subject === grant.subject }) + + await expect( + save({ subject: grant.subject, subjectProof: grant.subjectProof }, owner) + ).resolves.toMatchObject({ serviceAccountEmail: SA_EMAIL, newSubject: grant.subject }) + }) + + it.each([ + { projectNumber: 'my-project' }, + { projectNumber: '123/locations/global/workloadIdentityPools/other' }, + { poolId: '../other-pool' }, + { poolId: 'pool/providers/other' }, + { poolId: 'gcp-reserved' }, + { poolId: 'Sim-Pool' }, + { providerId: 'p' }, + { providerId: 'prov?audience=x' }, + { serviceAccountEmail: 'someone@gmail.com' }, + { serviceAccountEmail: 'sa@evil.example.com/x.iam.gserviceaccount.com' }, + { serviceAccountEmail: 'sa@proj.iam.gserviceaccount.com:generateIdToken' }, + { serviceAccountEmail: `a@b@${SA_EMAIL}` }, + ])('refuses %o without contacting Google', async (field) => { + fakeGoogle(fetch) + + await expect(save(field)).rejects.toThrow(/Enter the/) + expect(fetch).not.toHaveBeenCalled() + }) + + it('refuses keyless setup on a deployment that cannot sign tokens', async () => { + workloadIdentityConfigured.value = false + fakeGoogle(fetch) + + await expect(save({})).rejects.toThrow(/not available on this deployment/) + expect(fetch).not.toHaveBeenCalled() + }) + + it('never repeats a token or Google’s description in a refusal', async () => { + const leaked = 'ya29.LEAKED-ACCESS-TOKEN' + for (const google of [ + { + stsError: jsonResponse( + { + error: 'invalid_grant', + error_description: `bad token sim-oidc|x|${SUBJECT} ${leaked}`, + }, + 400 + ), + }, + { iamError: jsonResponse({ error: { code: 403, message: `denied ${leaked}` } }, 403) }, + ]) { + fakeGoogle(fetch, google) + const error: Error = await save({}).catch((caught) => caught) + expect(error.message).toMatch(/Google Cloud|service account/) + expect(error.message).not.toContain(leaked) + expect(error.message).not.toContain('sim-oidc') + expect(error.message).not.toContain('fed:') + } + }) +}) + +describe('getting a keyless Google Cloud token for a run', () => { + const fetch = vi.fn() + const secret = JSON.stringify({ type: 'google_workload_identity_v1', config: storedConfig }) + + function resolve(overrides: Partial[0]>) { + return resolveGoogleWorkloadIdentityToken({ + credentialId: 'cred-1', + decrypted: secret, + secretVersion: 'v1', + owner: { workspaceId: 'ws-1' }, + scopes: [BIGQUERY], + service: 'google-bigquery', + ...overrides, + }) + } + + beforeEach(() => { + fetch.mockReset() + vi.stubGlobal('fetch', fetch) + }) + + it('mints a service account token for the tool’s scopes', async () => { + const google = fakeGoogle(fetch) + + await expect(resolve({ credentialId: 'cred-scopes' })).resolves.toBe( + `ya29.sa-token-for-${BIGQUERY}` + ) + expect(google.iam[0]?.body).toEqual({ scope: [BIGQUERY], lifetime: '3600s' }) + }) + + it('refuses domain-wide delegation before contacting Google', async () => { + fakeGoogle(fetch) + + await expect( + resolve({ credentialId: 'cred-dwd', impersonateEmail: 'alice@example.com' }) + ).rejects.toMatchObject({ code: 'unsupported_use' }) + expect(fetch).not.toHaveBeenCalled() + }) + + it.each(['google-drive', 'gmail', 'google-sheets', undefined])( + 'refuses the Workspace service %s before contacting Google', + async (service) => { + fakeGoogle(fetch) + + await expect(resolve({ credentialId: 'cred-workspace', service })).rejects.toMatchObject({ + code: 'unsupported_use', + }) + expect(fetch).not.toHaveBeenCalled() + } + ) + + it('refuses a request with no scopes in the shared validation shape', async () => { + fakeGoogle(fetch) + + await expect(resolve({ credentialId: 'cred-no-scopes', scopes: [] })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + expect(fetch).not.toHaveBeenCalled() + }) + + it('reuses a minted token until the stored secret changes', async () => { + const google = fakeGoogle(fetch) + await resolve({ credentialId: 'cred-cache' }) + await resolve({ credentialId: 'cred-cache', service: 'vertex-ai' }) + expect(google.iam).toHaveLength(1) + + await resolve({ credentialId: 'cred-cache', secretVersion: 'v2' }) + expect(google.iam).toHaveLength(2) + }) + + it('reports a refused impersonation as a bad credential and an outage as unavailable', async () => { + fakeGoogle(fetch, { canImpersonate: () => false }) + await expect(resolve({ credentialId: 'cred-refused' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + + fakeGoogle(fetch, { stsError: jsonResponse({ error: 'quota_exceeded' }, 429) }) + await expect(resolve({ credentialId: 'cred-outage' })).rejects.toMatchObject({ + code: 'provider_unavailable', + }) + }) +}) diff --git a/apps/sim/lib/credentials/google-workload-identity.server.ts b/apps/sim/lib/credentials/google-workload-identity.server.ts new file mode 100644 index 00000000000..c3ee029d898 --- /dev/null +++ b/apps/sim/lib/credentials/google-workload-identity.server.ts @@ -0,0 +1,478 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { LRUCache } from 'lru-cache' +import { z } from 'zod' +import { coalesceLocally } from '@/lib/concurrency/singleflight' +import { encryptSecret } from '@/lib/core/security/encryption' +import { + GOOGLE_KEYLESS_SERVICES, + GOOGLE_WORKLOAD_IDENTITY_SECRET_TYPE, + googleStsAudience, + googleWorkloadIdentityAudience, + googleWorkloadIdentityPrincipal, + googleWorkloadIdentityProviderName, + isGoogleProjectNumber, + isGoogleServiceAccountEmail, + isGoogleWorkloadIdentityId, +} from '@/lib/credentials/google-workload-identity' +import { parseServiceAccountConfig } from '@/lib/credentials/service-account-config' +import { + providerFailureCode, + TokenServiceAccountValidationError, +} from '@/lib/credentials/token-service-accounts/errors' +import { + isWorkloadIdentityConfigured, + mintWorkloadIdentityToken, + type WorkloadIdentityOwner, +} from '@/lib/credentials/workload-identity/issuer' +import { + bindWorkloadIdentitySubject, + newWorkloadIdentitySubject, + type WorkloadIdentityCredentialOwner, + WorkloadIdentityNotConfiguredError, +} from '@/lib/credentials/workload-identity/subject' + +/** + * Keyless Google Cloud credentials: Workload Identity Federation with service + * account impersonation. Sim signs a short-lived token for the credential's own + * subject, Google STS exchanges it for a federated token on the customer's + * workload identity provider, and the federated token calls + * `generateAccessToken` for the service account. + * + * Impersonation rather than direct federated access: some Google Cloud APIs + * don't accept federated tokens, and impersonation gives the same identity + * (the service account and its IAM roles) that JSON-key credentials use, so + * BigQuery and Vertex AI behave exactly as they do with a key. + * + * @see https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-other-providers + * @see https://docs.cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token + * @see https://docs.cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken + */ + +const SUBMITTED_AUTH_TYPE = 'workload_identity' as const + +const STS_TOKEN_URL = 'https://sts.googleapis.com/v1/token' +const IAM_CREDENTIALS_ORIGIN = 'https://iamcredentials.googleapis.com' +const CLOUD_PLATFORM_SCOPE = 'https://www.googleapis.com/auth/cloud-platform' +const TOKEN_EXCHANGE_GRANT = 'urn:ietf:params:oauth:grant-type:token-exchange' +const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token' +const JWT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:jwt' +/** generateAccessToken's default and, without an organization policy change, its maximum. */ +const SERVICE_ACCOUNT_TOKEN_LIFETIME = '3600s' +const REQUEST_TIMEOUT_MS = 15_000 +/** `google.subject` "Cannot exceed 127 bytes". */ +const MAX_GOOGLE_SUBJECT_BYTES = 127 + +/** Re-mint once less than this much of a token's lifetime remains. */ +const TOKEN_REUSE_MARGIN_MS = 5 * 60 * 1000 +const TOKEN_CACHE_MAX = 10_000 + +/** STS error codes safe to show; descriptions stay out of the message. */ +const STS_DIAGNOSTIC_ERRORS = new Set([ + 'invalid_grant', + 'invalid_request', + 'invalid_target', + 'unauthorized_client', + 'access_denied', +]) + +const refShape = { + projectNumber: z + .string() + .trim() + .refine( + isGoogleProjectNumber, + 'Enter the project number (digits only) of the project that holds the workload identity pool' + ), + poolId: z + .string() + .trim() + .refine( + isGoogleWorkloadIdentityId, + 'Enter the workload identity pool ID: 4-32 lowercase letters, digits, or hyphens' + ), + providerId: z + .string() + .trim() + .refine( + isGoogleWorkloadIdentityId, + 'Enter the workload identity provider ID: 4-32 lowercase letters, digits, or hyphens' + ), + serviceAccountEmail: z + .string() + .trim() + .toLowerCase() + .refine( + isGoogleServiceAccountEmail, + 'Enter the service account email, like name@project-id.iam.gserviceaccount.com' + ), +} + +const subjectSchema = z + .string() + .min(1) + .refine((value) => Buffer.byteLength(value, 'utf8') <= MAX_GOOGLE_SUBJECT_BYTES) + +const storedConfigSchema = z.object({ ...refShape, subject: subjectSchema }).strict() + +type GoogleWorkloadIdentityConfig = z.infer + +const submittedConfigSchema = z + .object({ + authType: z.literal(SUBMITTED_AUTH_TYPE), + ...refShape, + subject: z.string().min(1).max(512).optional(), + subjectProof: z.string().min(1).max(512).optional(), + }) + .strict() + +const storedSecretSchema = z + .object({ type: z.literal(GOOGLE_WORKLOAD_IDENTITY_SECRET_TYPE), config: storedConfigSchema }) + .strict() + +/** A configuration problem Sim can name the fix for; its message is shown to the person saving. */ +export class GoogleWorkloadIdentityError extends Error { + /** @param status The HTTP status Google answered with, when Google itself refused. */ + constructor( + message: string, + readonly status?: number + ) { + super(message) + this.name = 'GoogleWorkloadIdentityError' + } +} + +/** Whether a submitted Google service-account configuration is the keyless mode. */ +export function isGoogleWorkloadIdentitySubmission(serialized: string): boolean { + try { + const parsed: unknown = JSON.parse(serialized) + return ( + typeof parsed === 'object' && + parsed !== null && + (parsed as { authType?: unknown }).authType === SUBMITTED_AUTH_TYPE + ) + } catch { + return false + } +} + +async function postJson( + url: string, + body: Record, + headers: Record +): Promise<{ status: number; payload: Record }> { + let response: Response + try { + response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...headers }, + body: JSON.stringify(body), + // A token must never follow a redirect off the Google host it was meant for. + redirect: 'error', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + } catch { + throw new GoogleWorkloadIdentityError('Could not reach Google Cloud. Try again.') + } + const payload = (await response.json().catch(() => ({}))) as Record + return { status: response.status, payload: payload ?? {} } +} + +function isProviderOutage(status: number): boolean { + return providerFailureCode(status) === 'provider_unavailable' +} + +const STS_REFUSED_MESSAGE = + 'Google Cloud refused Sim’s token for this workload identity provider. Check the project number, pool ID, and provider ID; that the provider is enabled and is an OIDC provider for the issuer from Sim (the connect dialog or `sim credentials keyless-subject`), with no allowed audiences and the attribute mapping google.subject=assertion.sub; and that its attribute condition allows this credential’s subject.' + +/** Copy for an STS refusal. Only Google's documented issuer-connection error is matched by text. */ +function stsFailureMessage(status: number, payload: Record): string { + if (isProviderOutage(status)) return 'Google Cloud STS is unavailable. Try again.' + const description = typeof payload.error_description === 'string' ? payload.error_description : '' + if (description.includes('Error connecting to the given credential')) { + return 'Google Cloud could not fetch the signing keys from Sim’s issuer. The issuer must be reachable over public HTTPS.' + } + const code = typeof payload.error === 'string' ? payload.error : '' + return STS_DIAGNOSTIC_ERRORS.has(code) ? `${STS_REFUSED_MESSAGE} (${code})` : STS_REFUSED_MESSAGE +} + +/** + * Exchanges a Sim token for this subject at Google STS. The audience is the + * credential's own provider; the subject token's `aud` is that provider's + * resource name, which a provider with no allowed audiences accepts. + */ +async function exchangeSubjectToken( + config: GoogleWorkloadIdentityConfig, + owner: WorkloadIdentityOwner +): Promise { + const subjectToken = await mintWorkloadIdentityToken({ + subject: config.subject, + audience: googleWorkloadIdentityAudience(config), + owner, + }) + // Google asks that this request carry no Authorization header. + const { status, payload } = await postJson( + STS_TOKEN_URL, + { + grantType: TOKEN_EXCHANGE_GRANT, + audience: googleStsAudience(config), + scope: CLOUD_PLATFORM_SCOPE, + requestedTokenType: ACCESS_TOKEN_TYPE, + subjectTokenType: JWT_TOKEN_TYPE, + subjectToken, + }, + {} + ) + if (status !== 200) { + throw new GoogleWorkloadIdentityError(stsFailureMessage(status, payload), status) + } + if (typeof payload.access_token !== 'string' || !payload.access_token) { + throw new GoogleWorkloadIdentityError('Google Cloud STS answered without a token.', 502) + } + return payload.access_token +} + +function impersonationFailureMessage(status: number, config: GoogleWorkloadIdentityConfig) { + if (isProviderOutage(status)) return 'Google Cloud IAM is unavailable. Try again.' + if (status === 403) { + return `The service account ${config.serviceAccountEmail} does not let this credential impersonate it. Grant roles/iam.workloadIdentityUser on the service account to ${googleWorkloadIdentityPrincipal(config, config.subject)}, and enable the IAM Service Account Credentials API. A new binding can take a few minutes to apply.` + } + if (status === 404) { + return `Google Cloud could not find the service account ${config.serviceAccountEmail}. Check the email.` + } + return `Google Cloud refused to impersonate ${config.serviceAccountEmail} (HTTP ${status}).` +} + +interface ServiceAccountAccessToken { + accessToken: string + expiresAtMs: number +} + +/** Impersonates the service account with the federated token, for the given scopes. */ +async function impersonateServiceAccount( + config: GoogleWorkloadIdentityConfig, + federatedToken: string, + scopes: string[] +): Promise { + // The `-` project wildcard is required; the email was validated when the credential was saved. + const url = `${IAM_CREDENTIALS_ORIGIN}/v1/projects/-/serviceAccounts/${encodeURIComponent(config.serviceAccountEmail)}:generateAccessToken` + const { status, payload } = await postJson( + url, + { scope: scopes, lifetime: SERVICE_ACCOUNT_TOKEN_LIFETIME }, + { Authorization: `Bearer ${federatedToken}` } + ) + if (status !== 200) { + throw new GoogleWorkloadIdentityError(impersonationFailureMessage(status, config), status) + } + if (typeof payload.accessToken !== 'string' || !payload.accessToken) { + throw new GoogleWorkloadIdentityError('Google Cloud IAM answered without a token.', 502) + } + const expiresAtMs = + typeof payload.expireTime === 'string' ? Date.parse(payload.expireTime) : Number.NaN + return { + accessToken: payload.accessToken, + expiresAtMs: Number.isFinite(expiresAtMs) ? expiresAtMs : Date.now() + 3600_000, + } +} + +async function mintServiceAccountToken( + config: GoogleWorkloadIdentityConfig, + owner: WorkloadIdentityOwner, + scopes: string[] +): Promise { + const federatedToken = await exchangeSubjectToken(config, owner) + return impersonateServiceAccount(config, federatedToken, scopes) +} + +function isRefusal(error: unknown): boolean { + return ( + error instanceof GoogleWorkloadIdentityError && + error.status !== undefined && + providerFailureCode(error.status) === 'invalid_credentials' + ) +} + +/** + * Google maps every valid token from Sim's issuer to a principal in the pool + * unless the provider's attribute condition says otherwise, so a subject no + * credential holds must not be able to impersonate the service account. If it + * can (a `principalSet` binding for the whole pool, say), every Sim credential + * could act as this service account, and saving is refused. + */ +async function assertSubjectPinned( + config: GoogleWorkloadIdentityConfig, + owner: WorkloadIdentityOwner +): Promise { + const stray = { ...config, subject: newWorkloadIdentitySubject(owner) } + try { + await mintServiceAccountToken(stray, owner, [CLOUD_PLATFORM_SCOPE]) + } catch (error) { + if (isRefusal(error)) return + throw new GoogleWorkloadIdentityError( + 'Sim could not confirm this service account trusts only this credential. Try again.' + ) + } + throw new GoogleWorkloadIdentityError( + 'This service account also trusts other Sim credentials. Grant roles/iam.workloadIdentityUser to this credential’s principal:// subject only (not a principalSet for the pool), and add the attribute condition from Sim (the connect dialog or `sim credentials keyless-subject`) so the provider accepts only this subject.' + ) +} + +function readStoredSubject(storedSecret: Record | null | undefined) { + const parsed = storedSecretSchema.safeParse(storedSecret) + return parsed.success ? parsed.data.config.subject : undefined +} + +function bindSubject( + subject: string | undefined, + subjectProof: string | undefined, + owner: WorkloadIdentityCredentialOwner +): string { + return bindWorkloadIdentitySubject({ + provider: 'google', + owner, + storedSubject: readStoredSubject(owner.storedSecret), + subject, + subjectProof, + }) +} + +export interface GoogleWorkloadIdentitySaveResult { + encryptedServiceAccountKey: string + serviceAccountEmail: string + /** The workload identity provider's resource name. */ + workloadIdentityProvider: string + /** A subject this save binds that the credential did not already hold. */ + newSubject?: string +} + +/** + * Validates the submitted configuration, then proves it end to end: Sim's + * token is exchanged at STS, the federated token impersonates the service + * account. Alongside, a subject no credential holds must be refused. Returns the encrypted blob. + */ +export async function verifyAndEncryptGoogleWorkloadIdentityCredential( + serialized: string, + owner: WorkloadIdentityCredentialOwner +): Promise { + const parsed = parseServiceAccountConfig( + serialized, + submittedConfigSchema, + 'Google Cloud keyless credential' + ) + if (!parsed.success) throw new GoogleWorkloadIdentityError(parsed.message) + if (!(await isWorkloadIdentityConfigured())) { + throw new WorkloadIdentityNotConfiguredError('Use a JSON key.') + } + const submitted = parsed.data + const config: GoogleWorkloadIdentityConfig = { + projectNumber: submitted.projectNumber, + poolId: submitted.poolId, + providerId: submitted.providerId, + serviceAccountEmail: submitted.serviceAccountEmail, + subject: bindSubject(submitted.subject, submitted.subjectProof, owner), + } + if (!subjectSchema.safeParse(config.subject).success) { + throw new GoogleWorkloadIdentityError('This credential’s subject is too long for Google Cloud.') + } + + // A token from generateAccessToken proves the provider accepted Sim's token and the + // service account lets this subject impersonate it. + const verify = () => mintServiceAccountToken(config, owner, [CLOUD_PLATFORM_SCOPE]) + // The pin probe is independent of the verification, so both run at once; a + // verification failure still reports first. + const [verified, pinned] = await Promise.allSettled([ + verify(), + assertSubjectPinned(config, owner), + ]) + if (verified.status === 'rejected') { + if (verified.reason instanceof GoogleWorkloadIdentityError) throw verified.reason + throw new GoogleWorkloadIdentityError( + `Could not verify the Google Cloud keyless credential: ${getErrorMessage(verified.reason, 'Unknown error')}` + ) + } + if (pinned.status === 'rejected') throw pinned.reason + + const { encrypted } = await encryptSecret( + JSON.stringify({ type: GOOGLE_WORKLOAD_IDENTITY_SECRET_TYPE, config }) + ) + return { + encryptedServiceAccountKey: encrypted, + serviceAccountEmail: config.serviceAccountEmail, + workloadIdentityProvider: googleWorkloadIdentityProviderName(config), + ...(config.subject !== readStoredSubject(owner.storedSecret) + ? { newSubject: config.subject } + : {}), + } +} + +/** + * Minted tokens per credential, stored-secret version, and scopes: a reconnect + * re-encrypts the secret and so never reuses the old configuration's token. + */ +const tokenCache = new LRUCache({ max: TOKEN_CACHE_MAX }) + +export interface ResolveGoogleWorkloadIdentityTokenInput { + credentialId: string + /** The credential's decrypted secret. */ + decrypted: string + /** Changes whenever the stored secret is re-encrypted. */ + secretVersion: string + owner: WorkloadIdentityOwner + scopes: string[] + /** A domain-wide-delegation subject, which a keyless credential refuses. */ + impersonateEmail?: string + /** The Google service the token is for, from server-side policy. */ + service?: string +} + +/** + * Resolves an authorized keyless Google credential to a service account token. + * It refuses domain-wide delegation and any service that is not a Google Cloud + * API before doing anything else: Workspace data needs a delegated user, which + * this mode cannot prove. + */ +export async function resolveGoogleWorkloadIdentityToken( + input: ResolveGoogleWorkloadIdentityTokenInput +): Promise { + if (input.impersonateEmail) { + throw new TokenServiceAccountValidationError('unsupported_use', 400, { + step: 'google_keyless_delegation', + reason: 'keyless Google Cloud credentials cannot impersonate a Google Workspace user', + }) + } + if (!input.service || !GOOGLE_KEYLESS_SERVICES.has(input.service)) { + throw new TokenServiceAccountValidationError('unsupported_use', 400, { + step: 'google_keyless_service', + reason: 'keyless Google Cloud credentials work only with Google Cloud APIs', + }) + } + if (input.scopes.length === 0) { + throw new TokenServiceAccountValidationError('invalid_credentials', 400, { + step: 'google_keyless_scopes', + reason: 'scopes are required for service account credentials', + }) + } + const scopeKey = [...new Set(input.scopes)].sort().join(' ') + const cacheKey = `${input.credentialId}:${input.secretVersion}:${scopeKey}` + const cached = tokenCache.get(cacheKey) + if (cached) return cached + + // Concurrent block runs share one mint, so it is not tied to any one caller's signal. + return coalesceLocally(`google-keyless:${cacheKey}`, async () => { + const { config } = storedSecretSchema.parse(JSON.parse(input.decrypted)) + let token: ServiceAccountAccessToken + try { + token = await mintServiceAccountToken(config, input.owner, scopeKey.split(' ')) + } catch (error) { + if (!(error instanceof GoogleWorkloadIdentityError)) throw error + // Report in the shared service-account codes: an outage is not a bad credential. + throw new TokenServiceAccountValidationError( + error.status === undefined ? 'provider_unavailable' : providerFailureCode(error.status), + error.status ?? 502, + { step: 'google_keyless_token' } + ) + } + const ttl = token.expiresAtMs - Date.now() - TOKEN_REUSE_MARGIN_MS + if (ttl > 0) tokenCache.set(cacheKey, token.accessToken, { ttl }) + return token.accessToken + }) +} diff --git a/apps/sim/lib/credentials/google-workload-identity.test.ts b/apps/sim/lib/credentials/google-workload-identity.test.ts new file mode 100644 index 00000000000..04e70bdccb3 --- /dev/null +++ b/apps/sim/lib/credentials/google-workload-identity.test.ts @@ -0,0 +1,50 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { googleKeylessOffer } from '@/lib/credentials/google-workload-identity' + +describe('keyless setup in the Google service account dialog', () => { + it.each(['google-drive', 'gmail', 'google-sheets'])( + 'never offers the keyless form when reconnecting a keyless credential from %s', + (oauthServiceId) => { + expect(googleKeylessOffer({ oauthServiceId, available: true, reconnectsKeyless: true })).toBe( + 'cloud_only' + ) + } + ) + + it('offers JSON keys only for a new connection from a Workspace service', () => { + expect( + googleKeylessOffer({ oauthServiceId: 'gmail', available: true, reconnectsKeyless: false }) + ).toBe('none') + }) + + it.each([true, false])( + 'offers keyless for BigQuery whether or not the reconnected credential is keyless (%s)', + (reconnectsKeyless) => { + expect( + googleKeylessOffer({ + oauthServiceId: 'google-bigquery', + available: true, + reconnectsKeyless, + }) + ).toBe('offer') + } + ) + + it('keeps a keyless credential keyless when reconnected from its own page', () => { + expect(googleKeylessOffer({ available: true, reconnectsKeyless: true })).toBe('offer') + expect(googleKeylessOffer({ available: true, reconnectsKeyless: false })).toBe('none') + }) + + it('offers nothing keyless where this deployment cannot sign tokens', () => { + expect( + googleKeylessOffer({ + oauthServiceId: 'google-bigquery', + available: false, + reconnectsKeyless: true, + }) + ).toBe('none') + }) +}) diff --git a/apps/sim/lib/credentials/google-workload-identity.ts b/apps/sim/lib/credentials/google-workload-identity.ts new file mode 100644 index 00000000000..42eeab2d59b --- /dev/null +++ b/apps/sim/lib/credentials/google-workload-identity.ts @@ -0,0 +1,186 @@ +/** + * Keyless Google Cloud credentials (Workload Identity Federation): the parts + * the connect dialog and the server share. A keyless credential is a mode of + * the `google-service-account` credential that stores no key: Sim's signed + * token is exchanged at Google STS against the customer's workload identity + * provider, and the federated token impersonates the service account. + * + * Every value here ends up in a Google URL or audience, so each one is checked + * against Google's documented format and nothing else can reach the request. + * + * @see https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-other-providers + */ + +/** + * The Google services a keyless credential may mint for: Google Cloud APIs the + * service account calls as itself. Google Workspace services need domain-wide + * delegation, which keyless credentials do not support. + */ +export const GOOGLE_KEYLESS_SERVICES: ReadonlySet = new Set([ + 'google-bigquery', + 'vertex-ai', +]) + +/** + * What a Google service account connect dialog offers for keyless setup. + * + * - `offer`: a Google Cloud service (new or reconnect), or a keyless credential + * reconnected from a context that names no service (its own detail page). + * - `cloud_only`: a keyless credential reconnected from a Google Workspace + * service, which can never use it; the dialog explains instead of saving. + * - `none`: JSON key only. + */ +export function googleKeylessOffer(input: { + /** The OAuth service the dialog was opened for, when it was opened for one. */ + oauthServiceId?: string + /** This deployment can sign workload-identity tokens for this owner. */ + available: boolean + /** The credential being reconnected is keyless. */ + reconnectsKeyless: boolean +}): 'offer' | 'cloud_only' | 'none' { + if (!input.available) return 'none' + if (input.oauthServiceId && GOOGLE_KEYLESS_SERVICES.has(input.oauthServiceId)) return 'offer' + if (!input.reconnectsKeyless) return 'none' + return input.oauthServiceId ? 'cloud_only' : 'offer' +} + +/** The `type` of a keyless credential's stored secret; a JSON key's is `service_account`. */ +export const GOOGLE_WORKLOAD_IDENTITY_SECRET_TYPE = 'google_workload_identity_v1' as const + +/** Whether a decrypted `google-service-account` secret is a keyless one. */ +export function isGoogleWorkloadIdentitySecret(decrypted: string): boolean { + try { + const parsed: unknown = JSON.parse(decrypted) + return ( + typeof parsed === 'object' && + parsed !== null && + (parsed as { type?: unknown }).type === GOOGLE_WORKLOAD_IDENTITY_SECRET_TYPE + ) + } catch { + return false + } +} + +/** A project number is Google's numeric project identifier, not the project ID. */ +const PROJECT_NUMBER_PATTERN = /^[1-9][0-9]{0,19}$/ + +/** + * Pool and provider IDs: "4-32 characters, and may contain the characters + * [a-z0-9-]. The prefix `gcp-` is reserved for use by Google". + * @see https://docs.cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools/create + */ +const POOL_OR_PROVIDER_ID_PATTERN = /^(?!gcp-)[a-z0-9-]{4,32}$/ + +/** + * A user-managed service account: an account ID of 6-30 characters matching + * `[a-z]([-a-z0-9]*[a-z0-9])`, in a project whose ID follows the same rule. + * @see https://docs.cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts/create + */ +const SERVICE_ACCOUNT_EMAIL_PATTERN = + /^[a-z][-a-z0-9]{4,28}[a-z0-9]@[a-z][-a-z0-9]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$/ + +/** + * A Google Cloud project number: digits only, no leading zero, up to 20 digits + * (`123456789012`). The project ID (`my-project`) is not accepted. + */ +export function isGoogleProjectNumber(value: string): boolean { + return PROJECT_NUMBER_PATTERN.test(value) +} + +/** + * A workload identity pool or provider ID: 4-32 lowercase letters, digits, or + * hyphens, not starting with the reserved `gcp-` prefix. + */ +export function isGoogleWorkloadIdentityId(value: string): boolean { + return POOL_OR_PROVIDER_ID_PATTERN.test(value) +} + +/** + * A user-managed service account email, `@.iam.gserviceaccount.com`, + * where both IDs are 6-30 characters of lowercase letters, digits, and hyphens, + * start with a letter, and end with a letter or digit. + */ +export function isGoogleServiceAccountEmail(value: string): boolean { + return SERVICE_ACCOUNT_EMAIL_PATTERN.test(value) +} + +/** The customer's workload identity provider, by the IDs that name it. */ +export interface GoogleWorkloadIdentityProviderRef { + projectNumber: string + poolId: string + providerId: string +} + +/** `projects//locations/global/workloadIdentityPools//providers/`. */ +export function googleWorkloadIdentityProviderName(ref: GoogleWorkloadIdentityProviderRef): string { + if ( + !isGoogleProjectNumber(ref.projectNumber) || + !isGoogleWorkloadIdentityId(ref.poolId) || + !isGoogleWorkloadIdentityId(ref.providerId) + ) { + throw new Error('Invalid Google workload identity provider') + } + return `projects/${ref.projectNumber}/locations/global/workloadIdentityPools/${ref.poolId}/providers/${ref.providerId}` +} + +/** + * The `aud` of Sim's token. A provider with no allowed audiences accepts "the + * full canonical resource name of the WorkloadIdentityPoolProvider, with or + * without the HTTPS prefix"; Google's setup guide shows the `https://` form. + * @see https://docs.cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools.providers + */ +export function googleWorkloadIdentityAudience(ref: GoogleWorkloadIdentityProviderRef): string { + return `https://iam.googleapis.com/${googleWorkloadIdentityProviderName(ref)}` +} + +/** + * The STS `audience` parameter, in the form the token-exchange reference gives + * for workload identity pools. + * @see https://docs.cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token + */ +export function googleStsAudience(ref: GoogleWorkloadIdentityProviderRef): string { + return `//iam.googleapis.com/${googleWorkloadIdentityProviderName(ref)}` +} + +/** The federated principal Google maps a Sim token with this subject to. */ +export function googleWorkloadIdentityPrincipal( + ref: GoogleWorkloadIdentityProviderRef, + subject: string +): string { + googleWorkloadIdentityProviderName(ref) + return `principal://iam.googleapis.com/projects/${ref.projectNumber}/locations/global/workloadIdentityPools/${ref.poolId}/subject/${subject}` +} + +/** + * The gcloud commands that set up trust for one credential: the pool, an OIDC + * provider for Sim's issuer that accepts only this credential's subject, and + * the binding that lets that subject impersonate the service account. The + * provider sets no allowed audiences, so it accepts its own resource name. + */ +export function googleWorkloadIdentitySetupCommands(input: { + ref: GoogleWorkloadIdentityProviderRef + serviceAccountEmail: string + issuer: string + subject: string +}): string { + const { ref, serviceAccountEmail, issuer, subject } = input + return [ + `gcloud iam workload-identity-pools create ${ref.poolId} \\`, + ` --project="${ref.projectNumber}" \\`, + ' --location="global" \\', + ' --display-name="Sim"', + '', + `gcloud iam workload-identity-pools providers create-oidc ${ref.providerId} \\`, + ` --project="${ref.projectNumber}" \\`, + ' --location="global" \\', + ` --workload-identity-pool="${ref.poolId}" \\`, + ` --issuer-uri="${issuer}" \\`, + ' --attribute-mapping="google.subject=assertion.sub" \\', + ` --attribute-condition="assertion.sub=='${subject}'"`, + '', + `gcloud iam service-accounts add-iam-policy-binding ${serviceAccountEmail} \\`, + ` --project="${ref.projectNumber}" \\`, + ' --role="roles/iam.workloadIdentityUser" \\', + ` --member="${googleWorkloadIdentityPrincipal(ref, subject)}"`, + ].join('\n') +} diff --git a/apps/sim/lib/credentials/microsoft-app-service-account.server.test.ts b/apps/sim/lib/credentials/microsoft-app-service-account.server.test.ts new file mode 100644 index 00000000000..7dc2a6a40fe --- /dev/null +++ b/apps/sim/lib/credentials/microsoft-app-service-account.server.test.ts @@ -0,0 +1,461 @@ +/** + * @vitest-environment node + */ + +import { + createHash, + createPublicKey, + createSign, + generateKeyPairSync, + X509Certificate, +} from 'node:crypto' +import { queueTableRows, resetDbChainMock } from '@sim/testing' +import { jsonResponse } from '@sim/testing/helpers/http' +import { encryptionMock, encryptionMockFns } from '@sim/testing/mocks/encryption.mock' +import { decodeProtectedHeader, jwtVerify } from 'jose' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/encryption', () => encryptionMock) +vi.mock('@/lib/credentials/workload-identity/issuer', () => ({ + isWorkloadIdentityConfigured: async () => true, + mintWorkloadIdentityToken: async ({ subject, audience }: { subject: string; audience: string }) => + `sim-oidc|${audience}|${subject}`, +})) + +import { credential } from '@sim/db/schema' +import { + resolveMicrosoftAppCredentialToken, + verifyAndEncryptMicrosoftAppCredential, +} from '@/lib/credentials/microsoft-app-service-account.server' +import { issueWorkloadIdentitySubject } from '@/lib/credentials/workload-identity/subject' +import { MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/oauth/types' + +/** DER type-length-value, for the one self-signed certificate below. */ +function der(tag: number, ...parts: Buffer[]): Buffer { + const content = Buffer.concat(parts) + const length = + content.length < 0x80 + ? Buffer.from([content.length]) + : content.length < 0x100 + ? Buffer.from([0x81, content.length]) + : Buffer.from([0x82, content.length >> 8, content.length & 0xff]) + return Buffer.concat([Buffer.from([tag]), length, content]) +} + +function derOid(oid: string): Buffer { + const [first, second, ...rest] = oid.split('.').map(Number) + const bytes = [first * 40 + second] + for (const arc of rest) { + const septets = [arc & 0x7f] + for (let value = arc >> 7; value > 0; value >>= 7) septets.unshift((value & 0x7f) | 0x80) + bytes.push(...septets) + } + return der(0x06, Buffer.from(bytes)) +} + +function derUtcTime(date: Date): Buffer { + const text = date.toISOString().replace(/[-:T]/g, '').slice(2, 14) + return der(0x17, Buffer.from(`${text}Z`)) +} + +/** + * A self-signed RSA certificate (CN=sim-test-app) generated for this test run, + * so no key material is committed. + */ +function generateTestCertificate(): { certificate: string; privateKey: string } { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }) + const sha256WithRsa = der(0x30, derOid('1.2.840.113549.1.1.11'), der(0x05)) + const name = der( + 0x30, + der(0x31, der(0x30, derOid('2.5.4.3'), der(0x0c, Buffer.from('sim-test-app')))) + ) + const now = Date.now() + const tbs = der( + 0x30, + der(0xa0, der(0x02, Buffer.from([0x02]))), + der(0x02, Buffer.from([0x01])), + sha256WithRsa, + name, + der(0x30, derUtcTime(new Date(now - 86_400_000)), derUtcTime(new Date(now + 365 * 86_400_000))), + name, + publicKey.export({ type: 'spki', format: 'der' }) + ) + const signature = createSign('sha256').update(tbs).sign(privateKey) + const certificate = der(0x30, tbs, sha256WithRsa, der(0x03, Buffer.from([0x00]), signature)) + const pem = (label: string, body: Buffer) => + `-----BEGIN ${label}-----\n${body + .toString('base64') + .match(/.{1,64}/g) + ?.join('\n')}\n-----END ${label}-----` + return { + certificate: pem('CERTIFICATE', certificate), + privateKey: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(), + } +} + +const { certificate: TEST_CERTIFICATE, privateKey: TEST_PRIVATE_KEY } = generateTestCertificate() + +const TENANT = '11111111-2222-3333-4444-555555555555' +const CLIENT_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' +const SECRET = 'entra-secret-value~123' +const TOKEN_URL = `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/token` +const ENVIRONMENT = 'https://contoso.api.crm.dynamics.com' +const owner = { workspaceId: 'ws-1', actorUserId: 'user-1' } + +interface TokenRequest { + url: string + form: Record + redirect: RequestRedirect | undefined +} + +/** + * A fake Entra token endpoint plus Graph and Dataverse. `acceptAssertion` + * decides whether a client assertion is trusted; every other request succeeds. + */ +function fakeMicrosoft( + fetch: ReturnType, + options: { acceptAssertion?: (assertion: string) => boolean; tokenError?: Response } = {} +) { + const tokenRequests: TokenRequest[] = [] + fetch.mockImplementation(async (url: string, init: RequestInit = {}) => { + if (url.startsWith('https://login.microsoftonline.com/')) { + const form = Object.fromEntries(new URLSearchParams(String(init.body))) + tokenRequests.push({ url, form, redirect: init.redirect }) + if (options.tokenError) return options.tokenError.clone() + if (form.client_assertion && options.acceptAssertion?.(form.client_assertion) === false) { + return jsonResponse( + { error: 'invalid_client', error_codes: [700213], error_description: 'AADSTS700213: …' }, + 400 + ) + } + return jsonResponse({ + token_type: 'Bearer', + expires_in: 3599, + access_token: `at:${form.scope}`, + }) + } + if (url.startsWith('https://graph.microsoft.com/')) { + return jsonResponse({ id: 'sp-object-id', appId: CLIENT_ID, displayName: 'Sim workflows' }) + } + if (url.endsWith('/api/data/v9.2/WhoAmI')) return jsonResponse({ UserId: 'app-user' }) + return jsonResponse({}, 404) + }) + return tokenRequests +} + +function save(config: Record, saver = owner) { + return verifyAndEncryptMicrosoftAppCredential( + JSON.stringify({ tenantId: TENANT, clientId: CLIENT_ID, ...config }), + saver + ) +} + +describe('saving a Microsoft app credential', () => { + const fetch = vi.fn() + + beforeEach(() => { + fetch.mockReset() + vi.stubGlobal('fetch', fetch) + encryptionMockFns.mockEncryptSecret.mockReset() + encryptionMockFns.mockEncryptSecret.mockResolvedValue({ encrypted: 'ciphertext', iv: 'iv' }) + }) + + it('exchanges a client secret at the tenant’s token endpoint and reads the app’s own service principal', async () => { + const tokens = fakeMicrosoft(fetch) + + await expect(save({ authType: 'client_secret', clientSecret: SECRET })).resolves.toMatchObject({ + displayName: 'Sim workflows', + servicePrincipalId: 'sp-object-id', + }) + expect(tokens).toEqual([ + { + url: TOKEN_URL, + redirect: 'error', + form: { + client_id: CLIENT_ID, + scope: 'https://graph.microsoft.com/.default', + grant_type: 'client_credentials', + client_secret: SECRET, + }, + }, + ]) + expect(fetch).toHaveBeenCalledWith( + `https://graph.microsoft.com/v1.0/servicePrincipals(appId='${CLIENT_ID}')?$select=id,appId,displayName`, + expect.objectContaining({ redirect: 'error' }) + ) + }) + + it('signs a PS256 certificate assertion Entra can verify with the uploaded certificate', async () => { + const tokens = fakeMicrosoft(fetch) + + await save({ + authType: 'certificate', + certificate: TEST_CERTIFICATE, + privateKey: TEST_PRIVATE_KEY, + }) + + const assertion = tokens[0]?.form.client_assertion ?? '' + expect(tokens[0]?.form).toMatchObject({ + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + }) + expect(tokens[0]?.form.client_secret).toBeUndefined() + const certificate = new X509Certificate(TEST_CERTIFICATE) + expect(decodeProtectedHeader(assertion)).toEqual({ + alg: 'PS256', + typ: 'JWT', + 'x5t#S256': createHash('sha256').update(certificate.raw).digest('base64url'), + }) + const { payload } = await jwtVerify(assertion, createPublicKey(TEST_CERTIFICATE), { + algorithms: ['PS256'], + audience: TOKEN_URL, + issuer: CLIENT_ID, + subject: CLIENT_ID, + }) + expect(payload.jti).toEqual(expect.any(String)) + expect((payload.exp ?? 0) - (payload.nbf ?? 0)).toBeLessThanOrEqual(600) + }) + + it('refuses a private key that does not belong to the certificate before contacting Entra', async () => { + fakeMicrosoft(fetch) + const otherKey = generateKeyPairSync('rsa', { modulusLength: 2048 }) + .privateKey.export({ type: 'pkcs8', format: 'pem' }) + .toString() + + await expect( + save({ authType: 'certificate', certificate: TEST_CERTIFICATE, privateKey: otherKey }) + ).rejects.toThrow(/does not belong to this certificate/) + expect(fetch).not.toHaveBeenCalled() + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + describe('keyless', () => { + const SUBJECT = 'sim:workspace:ws-1:credential:abcdef0123456789' + const reconnectOwner = { + ...owner, + storedSecret: { + type: 'microsoft_app_credential_v1', + config: { authType: 'federated', tenantId: TENANT, clientId: CLIENT_ID, subject: SUBJECT }, + }, + } + + it('presents a Sim token for this credential’s subject with the Entra exchange audience', async () => { + const tokens = fakeMicrosoft(fetch, { + acceptAssertion: (assertion) => assertion.endsWith(`|${SUBJECT}`), + }) + + await expect(save({ authType: 'federated' }, reconnectOwner)).resolves.toMatchObject({ + displayName: 'Sim workflows', + }) + const used = tokens.map((request) => request.form.client_assertion) + expect(used).toContain(`sim-oidc|api://AzureADTokenExchange|${SUBJECT}`) + }) + + it('reports a newly issued subject so the save can check no other credential holds it', async () => { + const grant = issueWorkloadIdentitySubject('microsoft', { workspaceId: 'ws-1' }, 'user-1') + fakeMicrosoft(fetch, { + acceptAssertion: (assertion) => + assertion.endsWith(`|${grant.subject}`) || assertion.endsWith(`|${SUBJECT}`), + }) + + await expect( + save({ authType: 'federated', subject: grant.subject, subjectProof: grant.subjectProof }) + ).resolves.toMatchObject({ newSubject: grant.subject }) + await expect(save({ authType: 'federated' }, reconnectOwner)).resolves.not.toHaveProperty( + 'newSubject' + ) + }) + + it('refuses an app that also accepts a subject no credential holds', async () => { + fakeMicrosoft(fetch, { acceptAssertion: () => true }) + + await expect(save({ authType: 'federated' }, reconnectOwner)).rejects.toThrow( + /also trusts other Sim credentials/ + ) + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + it('refuses a subject issued to another member, without contacting Entra', async () => { + fakeMicrosoft(fetch) + const grant = issueWorkloadIdentitySubject('microsoft', { workspaceId: 'ws-1' }, 'user-2') + + await expect( + save({ authType: 'federated', subject: grant.subject, subjectProof: grant.subjectProof }) + ).rejects.toThrow(/belongs to someone else/) + expect(fetch).not.toHaveBeenCalled() + }) + + it('names the federated credential mismatch when Entra rejects the subject', async () => { + fakeMicrosoft(fetch, { acceptAssertion: () => false }) + + await expect(save({ authType: 'federated' }, reconnectOwner)).rejects.toThrow( + /No federated credential on this app matches/ + ) + }) + }) + + it('verifies the Dataverse environment with its own token and WhoAmI', async () => { + const tokens = fakeMicrosoft(fetch) + + await save({ + authType: 'client_secret', + clientSecret: SECRET, + environmentUrl: 'https://contoso.crm.dynamics.com/', + }) + + expect(tokens.map((request) => request.form.scope)).toEqual([ + 'https://graph.microsoft.com/.default', + `${ENVIRONMENT}/.default`, + ]) + expect(fetch).toHaveBeenCalledWith( + `${ENVIRONMENT}/api/data/v9.2/WhoAmI`, + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: `Bearer at:${ENVIRONMENT}/.default` }), + }) + ) + }) + + it.each([ + 'https://evil.example.com', + 'https://contoso.crm.dynamics.com.evil.example.com', + 'https://contoso.crm.dynamics.com/api/data', + 'http://contoso.crm.dynamics.com', + 'https://user@contoso.crm.dynamics.com', + ])('refuses the environment %s without sending a token anywhere', async (environmentUrl) => { + fakeMicrosoft(fetch) + + await expect( + save({ authType: 'client_secret', clientSecret: SECRET, environmentUrl }) + ).rejects.toThrow(/Dataverse environment URL/) + expect(fetch).not.toHaveBeenCalled() + }) + + it.each(['common', 'organizations', '../evil', 'contoso.onmicrosoft.com/x'])( + 'refuses the tenant %s', + async (tenantId) => { + await expect( + verifyAndEncryptMicrosoftAppCredential( + JSON.stringify({ + tenantId, + clientId: CLIENT_ID, + authType: 'client_secret', + clientSecret: SECRET, + }), + owner + ) + ).rejects.toThrow(/tenant/) + expect(fetch).not.toHaveBeenCalled() + } + ) + + it('never repeats the secret, key, or assertion in a refusal', async () => { + fakeMicrosoft(fetch, { + tokenError: jsonResponse( + { + error: 'invalid_client', + error_codes: [7000215], + error_description: `AADSTS7000215: bad secret ${SECRET} ${TEST_PRIVATE_KEY}`, + }, + 401 + ), + }) + + for (const config of [ + { authType: 'client_secret', clientSecret: SECRET }, + { authType: 'certificate', certificate: TEST_CERTIFICATE, privateKey: TEST_PRIVATE_KEY }, + ]) { + const error: Error = await save(config).catch((caught) => caught) + expect(error.message).toMatch(/Microsoft Entra/) + expect(error.message).not.toContain(SECRET) + expect(error.message).not.toContain('PRIVATE KEY') + expect(error.message).not.toContain('eyJ') + } + }) +}) + +describe('getting a Microsoft app token for a run', () => { + const fetch = vi.fn() + + function storeCredential(config: Record, encrypted = 'ciphertext-a') { + resetDbChainMock() + queueTableRows(credential, [ + { + workspaceId: 'ws-1', + organizationId: null, + type: 'service_account', + providerId: MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, + revokedAt: null, + encryptedServiceAccountKey: encrypted, + }, + ]) + encryptionMockFns.mockDecryptSecret.mockResolvedValueOnce({ + decrypted: JSON.stringify({ + type: 'microsoft_app_credential_v1', + config: { tenantId: TENANT, clientId: CLIENT_ID, ...config }, + }), + }) + } + + beforeEach(() => { + fetch.mockReset() + vi.stubGlobal('fetch', fetch) + encryptionMockFns.mockDecryptSecret.mockReset() + }) + + it.each(['outlook', 'onedrive', 'microsoft-teams', undefined])( + 'refuses to mint for %s before reading the credential', + async (service) => { + await expect(resolveMicrosoftAppCredentialToken('cred-1', service)).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + expect(encryptionMockFns.mockDecryptSecret).not.toHaveBeenCalled() + expect(fetch).not.toHaveBeenCalled() + } + ) + + it('mints a Dataverse token for the stored environment and returns that environment', async () => { + const tokens = fakeMicrosoft(fetch) + storeCredential( + { authType: 'client_secret', clientSecret: SECRET, environmentUrl: ENVIRONMENT }, + 'ciphertext-dataverse' + ) + + await expect( + resolveMicrosoftAppCredentialToken('cred-dataverse', 'microsoft-dataverse') + ).resolves.toEqual({ accessToken: `at:${ENVIRONMENT}/.default`, instanceUrl: ENVIRONMENT }) + expect(tokens.map((request) => request.form.scope)).toEqual([`${ENVIRONMENT}/.default`]) + }) + + it('refuses Dataverse for a credential saved without an environment', async () => { + fakeMicrosoft(fetch) + storeCredential({ authType: 'client_secret', clientSecret: SECRET }, 'ciphertext-graph-only') + + await expect( + resolveMicrosoftAppCredentialToken('cred-graph-only', 'microsoft-dataverse') + ).rejects.toMatchObject({ code: 'invalid_credentials' }) + expect(fetch).not.toHaveBeenCalled() + }) + + it('reuses a minted Graph token until the stored secret changes', async () => { + const tokens = fakeMicrosoft(fetch) + storeCredential({ authType: 'client_secret', clientSecret: SECRET }, 'ciphertext-v1') + await resolveMicrosoftAppCredentialToken('cred-cache', 'microsoft-ad') + storeCredential({ authType: 'client_secret', clientSecret: SECRET }, 'ciphertext-v1') + await resolveMicrosoftAppCredentialToken('cred-cache', 'sharepoint') + expect(tokens).toHaveLength(1) + encryptionMockFns.mockDecryptSecret.mockReset() + + storeCredential({ authType: 'client_secret', clientSecret: 'rotated' }, 'ciphertext-v2') + await resolveMicrosoftAppCredentialToken('cred-cache', 'sharepoint') + expect(tokens).toHaveLength(2) + expect(tokens[1]?.form.client_secret).toBe('rotated') + }) + + it('reports an Entra outage as the provider being unavailable, not a bad credential', async () => { + fakeMicrosoft(fetch, { tokenError: jsonResponse({ error: 'temporarily_unavailable' }, 503) }) + storeCredential({ authType: 'client_secret', clientSecret: SECRET }, 'ciphertext-outage') + + await expect( + resolveMicrosoftAppCredentialToken('cred-outage', 'microsoft-ad') + ).rejects.toMatchObject({ code: 'provider_unavailable' }) + }) +}) diff --git a/apps/sim/lib/credentials/microsoft-app-service-account.server.ts b/apps/sim/lib/credentials/microsoft-app-service-account.server.ts new file mode 100644 index 00000000000..d98e505bfd2 --- /dev/null +++ b/apps/sim/lib/credentials/microsoft-app-service-account.server.ts @@ -0,0 +1,701 @@ +import { createHash, createPrivateKey, type KeyObject, X509Certificate } from 'node:crypto' +import { db } from '@sim/db' +import { credential } from '@sim/db/schema' +import { getErrorMessage } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { eq } from 'drizzle-orm' +import { SignJWT } from 'jose' +import { LRUCache } from 'lru-cache' +import { z } from 'zod' +import { coalesceLocally } from '@/lib/concurrency/singleflight' +import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' +import { parseServiceAccountConfig } from '@/lib/credentials/service-account-config' +import { + providerFailureCode, + TokenServiceAccountValidationError, +} from '@/lib/credentials/token-service-accounts/errors' +import { WORKLOAD_IDENTITY_AUDIENCES } from '@/lib/credentials/workload-identity/audiences' +import { + isWorkloadIdentityConfigured, + mintWorkloadIdentityToken, + type WorkloadIdentityOwner, +} from '@/lib/credentials/workload-identity/issuer' +import { + bindWorkloadIdentitySubject, + newWorkloadIdentitySubject, + type WorkloadIdentityCredentialOwner, + WorkloadIdentityNotConfiguredError, +} from '@/lib/credentials/workload-identity/subject' +import { normalizeMicrosoftDataverseEnvironmentUrl } from '@/lib/oauth/microsoft-dataverse' +import { MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/oauth/types' + +/** + * Saved Microsoft Entra app-only credentials (OAuth 2.0 client credentials). + * + * One app registration serves every Microsoft service that runs without a + * signed-in user: Microsoft Graph for Entra ID and SharePoint, and the + * credential's own Dataverse environment for Dataverse and Dynamics 365. The + * app's application permissions and the environment's application user decide + * what it can do; Sim stores no permission list of its own. + * + * - `client_secret`: the app's client secret. + * - `certificate`: a PEM private key and its certificate. Sim signs a PS256 + * client assertion carrying the certificate's `x5t#S256` thumbprint. + * - `federated`: no stored secret. Sim signs a short-lived token for this + * credential's own subject and presents it as the client assertion, which a + * federated identity credential on the app trusts. + * + * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow + * @see https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials + */ + +const SECRET_TYPE = 'microsoft_app_credential_v1' as const +const LOGIN_ORIGIN = 'https://login.microsoftonline.com' +const GRAPH_RESOURCE = 'https://graph.microsoft.com' +const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' +const REQUEST_TIMEOUT_MS = 15_000 +/** Microsoft asks for an assertion that expires "5-10 minutes after nbf at most". */ +const CERTIFICATE_ASSERTION_TTL_SECONDS = 300 +const MIN_RSA_MODULUS_BITS = 2048 +const MAX_RSA_MODULUS_BITS = 4096 +const MAX_PEM_LENGTH = 16_384 + +/** Re-mint once less than this much of a token's lifetime remains. */ +const TOKEN_REUSE_MARGIN_MS = 5 * 60 * 1000 +const TOKEN_CACHE_MAX = 10_000 + +const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i +/** A verified domain (`contoso.onmicrosoft.com`); `common` and friends have no dot. */ +const TENANT_DOMAIN_PATTERN = + /^(?=.{4,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/ + +/** The Microsoft services that may use this credential, and the resource each one calls. */ +type MicrosoftAppService = 'microsoft-ad' | 'sharepoint' | 'microsoft-dataverse' + +const tenantSchema = z + .string() + .trim() + .toLowerCase() + .refine( + (value) => GUID_PATTERN.test(value) || TENANT_DOMAIN_PATTERN.test(value), + 'Enter the Directory (tenant) ID, a GUID like 00000000-0000-0000-0000-000000000000' + ) + +const clientIdSchema = z + .string() + .trim() + .toLowerCase() + .regex(GUID_PATTERN, 'Enter the Application (client) ID, a GUID') + +const environmentUrlSchema = z + .string() + .trim() + .max(255) + .transform((value, context) => { + try { + return normalizeMicrosoftDataverseEnvironmentUrl(value) + } catch { + context.addIssue({ + code: 'custom', + message: + 'Enter the Dataverse environment URL, like https://contoso.crm.dynamics.com, with no path', + }) + return z.NEVER + } + }) + +const baseShape = { + tenantId: tenantSchema, + clientId: clientIdSchema, + environmentUrl: environmentUrlSchema.optional(), +} + +const pemSchema = (label: string) => + z.string().trim().min(1, `${label} is required`).max(MAX_PEM_LENGTH, `${label} is too large`) + +const clientSecretSchema = z + .object({ + ...baseShape, + authType: z.literal('client_secret'), + clientSecret: z + .string() + .min(1, 'Client secret is required') + .max(1024) + .refine((value) => value.trim().length > 0, 'Client secret is required'), + }) + .strict() + +const certificateSchema = z + .object({ + ...baseShape, + authType: z.literal('certificate'), + privateKey: pemSchema('Private key'), + certificate: pemSchema('Certificate'), + }) + .strict() + +const federatedShape = { + ...baseShape, + authType: z.literal('federated'), + subject: z.string().min(1).max(512), +} + +const storedConfigSchema = z.discriminatedUnion('authType', [ + clientSecretSchema, + certificateSchema, + z.object(federatedShape).strict(), +]) + +type MicrosoftAppConfig = z.infer + +const submittedConfigSchema = z.discriminatedUnion('authType', [ + clientSecretSchema, + certificateSchema, + z + .object({ + ...federatedShape, + subject: federatedShape.subject.optional(), + subjectProof: z.string().min(1).max(512).optional(), + }) + .strict(), +]) + +const storedSecretSchema = z + .object({ type: z.literal(SECRET_TYPE), config: storedConfigSchema }) + .strict() + +/** A configuration problem Sim can name the fix for; its message is shown to the person saving. */ +export class MicrosoftAppCredentialError extends Error { + /** + * @param status The HTTP status Microsoft answered with, when Microsoft itself refused. + * @param aadsts The Entra `AADSTS` error code, when Entra reported one. + */ + constructor( + message: string, + readonly status?: number, + readonly aadsts?: number + ) { + super(message) + this.name = 'MicrosoftAppCredentialError' + } +} + +const FEDERATION_MISMATCH_MESSAGE = + 'No federated credential on this app matches the issuer, subject, and audience Sim presents. Add it with the exact values from Sim (the connect dialog or `sim credentials keyless-subject`) (matching is case-sensitive). A new federated credential can take a few minutes to take effect.' + +/** + * Actionable copy for the Entra errors a person saving the credential can fix. + * AADSTS70021 (no matching federated identity record) is documented in the + * workload identity federation considerations, and AADSTS700213 (no matching + * record for the presented subject, issuer, or audience) in the August 2024 + * breaking-changes entry; any other keyless refusal still names the mismatch. + * @see https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes + * @see https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-considerations + * @see https://learn.microsoft.com/en-us/entra/identity-platform/reference-breaking-changes + */ +function entraFailureMessage(aadsts: number | undefined, config: MicrosoftAppConfig): string { + switch (aadsts) { + case 90002: + return 'Microsoft Entra could not find this tenant. Check the Directory (tenant) ID.' + case 700016: + return 'No app with this Application (client) ID exists in the tenant. Check both IDs on the app registration’s Overview page.' + case 7000215: + return 'Microsoft Entra rejected the client secret. Copy the secret’s Value (not its Secret ID) from Certificates & secrets.' + case 7000222: + return 'This client secret has expired. Create a new one under Certificates & secrets.' + case 700027: + return 'Microsoft Entra could not verify the signed assertion. Upload this certificate (the .cer or .pem public part) to the app registration’s Certificates tab.' + case 7000112: + return 'This app registration is disabled in the tenant.' + case 500011: + return config.environmentUrl + ? 'Microsoft Entra does not recognize this Dataverse environment in the tenant. Check the environment URL.' + : 'Microsoft Entra does not recognize the requested resource in this tenant.' + case 70021: + case 700213: + return FEDERATION_MISMATCH_MESSAGE + default: + return config.authType === 'federated' + ? `Microsoft Entra refused the keyless sign-in${aadsts ? ` (AADSTS${aadsts})` : ''}. ${FEDERATION_MISMATCH_MESSAGE}` + : `Microsoft Entra refused the token request${aadsts ? ` (AADSTS${aadsts})` : ''}. Check the tenant ID, client ID, and credential.` + } +} + +function readAadstsCode(payload: { error_codes?: unknown; error_description?: unknown }) { + if (Array.isArray(payload.error_codes) && typeof payload.error_codes[0] === 'number') { + return payload.error_codes[0] + } + const match = + typeof payload.error_description === 'string' + ? /AADSTS(\d+)/.exec(payload.error_description) + : null + return match ? Number(match[1]) : undefined +} + +interface CertificateMaterial { + key: KeyObject + thumbprintS256: string +} + +/** + * Loads the certificate and its private key, refusing a pair Entra would + * reject anyway so the person sees what to fix before anything is sent. + */ +function loadCertificateMaterial( + privateKeyPem: string, + certificatePem: string +): CertificateMaterial { + let certificate: X509Certificate + try { + certificate = new X509Certificate(certificatePem) + } catch { + throw new MicrosoftAppCredentialError( + 'The certificate is not a readable PEM certificate (-----BEGIN CERTIFICATE-----).' + ) + } + let key: KeyObject + try { + key = createPrivateKey(privateKeyPem) + } catch { + throw new MicrosoftAppCredentialError( + 'The private key is not a readable, unencrypted PEM private key.' + ) + } + if (key.asymmetricKeyType !== 'rsa') { + throw new MicrosoftAppCredentialError( + 'Microsoft Entra certificate credentials need an RSA key.' + ) + } + const bits = key.asymmetricKeyDetails?.modulusLength ?? 0 + if (bits < MIN_RSA_MODULUS_BITS || bits > MAX_RSA_MODULUS_BITS) { + throw new MicrosoftAppCredentialError( + `The RSA key is ${bits}-bit; use a 2048- to 4096-bit key.` + ) + } + if (!certificate.checkPrivateKey(key)) { + throw new MicrosoftAppCredentialError('The private key does not belong to this certificate.') + } + if (Date.parse(certificate.validTo) <= Date.now()) { + throw new MicrosoftAppCredentialError( + 'This certificate has expired. Upload a new one to the app registration and save it here.' + ) + } + return { + key, + thumbprintS256: createHash('sha256').update(certificate.raw).digest('base64url'), + } +} + +function tokenEndpoint(tenantId: string): string { + return `${LOGIN_ORIGIN}/${encodeURIComponent(tenantId)}/oauth2/v2.0/token` +} + +/** The PS256 client assertion Microsoft documents for certificate credentials. */ +async function signCertificateAssertion( + config: Extract +): Promise { + const { key, thumbprintS256 } = loadCertificateMaterial(config.privateKey, config.certificate) + const now = Math.floor(Date.now() / 1000) + return new SignJWT({}) + .setProtectedHeader({ alg: 'PS256', typ: 'JWT', 'x5t#S256': thumbprintS256 }) + .setIssuer(config.clientId) + .setSubject(config.clientId) + .setAudience(tokenEndpoint(config.tenantId)) + .setJti(generateId()) + .setIssuedAt(now) + .setNotBefore(now) + .setExpirationTime(now + CERTIFICATE_ASSERTION_TTL_SECONDS) + .sign(key) +} + +function mintFederatedAssertion(subject: string, owner: WorkloadIdentityOwner): Promise { + return mintWorkloadIdentityToken({ + subject, + audience: WORKLOAD_IDENTITY_AUDIENCES.microsoft, + owner, + }) +} + +async function clientAuthentication( + config: MicrosoftAppConfig, + owner: WorkloadIdentityOwner +): Promise> { + if (config.authType === 'client_secret') return { client_secret: config.clientSecret } + const assertion = + config.authType === 'certificate' + ? await signCertificateAssertion(config) + : await mintFederatedAssertion(config.subject, owner) + return { client_assertion_type: CLIENT_ASSERTION_TYPE, client_assertion: assertion } +} + +interface EntraToken { + accessToken: string + expiresInSeconds: number +} + +/** + * Requests an app-only token for one resource (`/.default`) from the + * credential's own tenant. The token endpoint is fixed; only the tenant segment + * comes from the credential, and it was validated when the credential was saved. + */ +async function requestEntraToken( + config: MicrosoftAppConfig, + resource: string, + owner: WorkloadIdentityOwner, + signal?: AbortSignal +): Promise { + const body = new URLSearchParams({ + client_id: config.clientId, + scope: `${resource}/.default`, + grant_type: 'client_credentials', + ...(await clientAuthentication(config, owner)), + }) + const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS) + let response: Response + try { + response = await fetch(tokenEndpoint(config.tenantId), { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }, + body, + // A secret or assertion must never follow a redirect off the Entra host. + redirect: 'error', + signal: signal ? AbortSignal.any([signal, timeout]) : timeout, + }) + } catch { + signal?.throwIfAborted() + throw new MicrosoftAppCredentialError('Could not reach Microsoft Entra. Try again.') + } + const payload = (await response.json().catch(() => ({}))) as { + access_token?: unknown + expires_in?: unknown + error_codes?: unknown + error_description?: unknown + } + if (!response.ok) { + const aadsts = readAadstsCode(payload) + throw new MicrosoftAppCredentialError( + providerFailureCode(response.status) === 'invalid_credentials' + ? entraFailureMessage(aadsts, config) + : 'Microsoft Entra is unavailable. Try again.', + response.status, + aadsts + ) + } + if (typeof payload.access_token !== 'string' || !payload.access_token) { + throw new MicrosoftAppCredentialError('Microsoft Entra answered without an access token.', 502) + } + const expiresIn = Number(payload.expires_in) + return { + accessToken: payload.access_token, + expiresInSeconds: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600, + } +} + +async function getJson(url: string, token: string): Promise<{ status: number; body: unknown }> { + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'OData-MaxVersion': '4.0', + 'OData-Version': '4.0', + }, + redirect: 'error', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + return { status: response.status, body: await response.json().catch(() => ({})) } +} + +/** + * Reads the app's own service principal. Microsoft documents that "a service + * principal can retrieve its own application and service principal details + * without being granted any application permissions", so this proves the + * token without asking for a permission the tools may not need (a SharePoint + * app on Sites.Selected, say). + * @see https://learn.microsoft.com/en-us/graph/api/serviceprincipal-get + */ +async function readOwnServicePrincipal(config: MicrosoftAppConfig, token: string) { + const { status, body } = await getJson( + `${GRAPH_RESOURCE}/v1.0/servicePrincipals(appId='${config.clientId}')?$select=id,appId,displayName`, + token + ) + if (status !== 200) { + throw new MicrosoftAppCredentialError( + status >= 500 + ? 'Microsoft Graph is unavailable. Try again.' + : `Microsoft Graph did not accept the app’s token (HTTP ${status}).`, + status + ) + } + const principal = body as { id?: unknown; displayName?: unknown } + return { + id: typeof principal.id === 'string' ? principal.id : config.clientId, + displayName: typeof principal.displayName === 'string' ? principal.displayName : undefined, + } +} + +/** + * Calls Dataverse `WhoAmI` as the app. A 401/403 means the environment has no + * application user for this app (or it lacks a security role). + * @see https://learn.microsoft.com/en-us/power-platform/admin/manage-application-users + */ +async function verifyDataverseApplicationUser(environmentUrl: string, token: string) { + const { status } = await getJson(`${environmentUrl}/api/data/v9.2/WhoAmI`, token) + if (status === 200) return + if (status === 401 || status === 403) { + throw new MicrosoftAppCredentialError( + 'The app signed in, but the Dataverse environment did not accept it. In the Power Platform admin center, add this app as an application user of the environment and assign it a security role.', + status + ) + } + throw new MicrosoftAppCredentialError( + status >= 500 + ? 'The Dataverse environment is unavailable. Try again.' + : `The Dataverse environment refused the request (HTTP ${status}). Check the environment URL.`, + status + ) +} + +/** The auth mode a decrypted Microsoft app credential secret holds, or undefined when it is not one. */ +export function storedMicrosoftAppAuthType( + decrypted: string +): 'federated' | 'client_secret' | 'certificate' | undefined { + try { + const parsed = storedSecretSchema.safeParse(JSON.parse(decrypted)) + return parsed.success ? parsed.data.config.authType : undefined + } catch { + return undefined + } +} + +function readStoredSubject(storedSecret: Record | null | undefined) { + const parsed = storedSecretSchema.safeParse(storedSecret) + return parsed.success && parsed.data.config.authType === 'federated' + ? parsed.data.config.subject + : undefined +} + +/** + * Entra matches a federated credential on issuer, subject, and audience, so a + * subject no credential holds must be refused. If it is accepted, the app + * trusts every Sim credential (a flexible federated credential matching on a + * looser claim, say), and saving would let other Sim tenants act as the app. + */ +async function assertFederationPinsSubject( + config: Extract, + owner: WorkloadIdentityOwner +): Promise { + try { + await requestEntraToken( + { ...config, subject: newWorkloadIdentitySubject(owner) }, + GRAPH_RESOURCE, + owner + ) + } catch (error) { + const refused = + error instanceof MicrosoftAppCredentialError && + error.status !== undefined && + providerFailureCode(error.status) === 'invalid_credentials' + if (refused) return + throw new MicrosoftAppCredentialError( + 'Sim could not confirm this app trusts only this credential. Try again.' + ) + } + throw new MicrosoftAppCredentialError( + 'This app also trusts other Sim credentials. Use a federated credential whose subject is exactly the one from Sim (the connect dialog or `sim credentials keyless-subject`), so only this credential can use it.' + ) +} + +function bindSubject( + subject: string | undefined, + subjectProof: string | undefined, + owner: WorkloadIdentityCredentialOwner +): string { + return bindWorkloadIdentitySubject({ + provider: 'microsoft', + owner, + storedSubject: readStoredSubject(owner.storedSecret), + subject, + subjectProof, + }) +} + +export interface MicrosoftAppCredentialSaveResult { + encryptedServiceAccountKey: string + /** The app's service principal display name, or its client ID. */ + displayName: string + /** The app's service principal object ID in the tenant. */ + servicePrincipalId: string + tenantId: string + /** A subject this save binds that the credential did not already hold. */ + newSubject?: string +} + +/** + * Validates the submitted configuration, mints a Microsoft Graph token and + * reads the app's own service principal with it, and, when an environment is + * set, mints a Dataverse token and calls `WhoAmI`. Returns the encrypted blob. + */ +export async function verifyAndEncryptMicrosoftAppCredential( + serialized: string, + owner: WorkloadIdentityCredentialOwner +): Promise { + const parsed = parseServiceAccountConfig( + serialized, + submittedConfigSchema, + 'Microsoft app credential' + ) + if (!parsed.success) throw new MicrosoftAppCredentialError(parsed.message) + const submitted = parsed.data + let config: MicrosoftAppConfig + if (submitted.authType === 'federated') { + if (!(await isWorkloadIdentityConfigured())) { + throw new WorkloadIdentityNotConfiguredError('Use a client secret or certificate.') + } + const { subject, subjectProof, ...rest } = submitted + config = { ...rest, subject: bindSubject(subject, subjectProof, owner) } + } else { + config = submitted + } + if (config.authType === 'certificate') { + loadCertificateMaterial(config.privateKey, config.certificate) + } + + const verify = async () => { + const graphToken = await requestEntraToken(config, GRAPH_RESOURCE, owner) + const principal = await readOwnServicePrincipal(config, graphToken.accessToken) + if (config.environmentUrl) { + const dataverseToken = await requestEntraToken(config, config.environmentUrl, owner) + await verifyDataverseApplicationUser(config.environmentUrl, dataverseToken.accessToken) + } + return principal + } + // The subject-pin probe is independent of the verification, so both run at + // once; a verification failure still reports first. + const [verified, pinned] = await Promise.allSettled([ + verify(), + config.authType === 'federated' ? assertFederationPinsSubject(config, owner) : undefined, + ]) + if (verified.status === 'rejected') { + if (verified.reason instanceof MicrosoftAppCredentialError) throw verified.reason + throw new MicrosoftAppCredentialError( + `Could not verify the Microsoft app credential: ${getErrorMessage(verified.reason, 'Unknown error')}` + ) + } + if (pinned.status === 'rejected') throw pinned.reason + + const { encrypted } = await encryptSecret(JSON.stringify({ type: SECRET_TYPE, config })) + return { + encryptedServiceAccountKey: encrypted, + displayName: verified.value.displayName ?? config.clientId, + servicePrincipalId: verified.value.id, + tenantId: config.tenantId, + ...(config.authType === 'federated' && config.subject !== readStoredSubject(owner.storedSecret) + ? { newSubject: config.subject } + : {}), + } +} + +interface CachedToken { + accessToken: string + instanceUrl?: string +} + +/** + * Minted tokens per credential, resource, and stored-secret version: a + * reconnect re-encrypts the secret and so never reuses the old app's token. + */ +const tokenCache = new LRUCache({ max: TOKEN_CACHE_MAX }) + +export interface MicrosoftAppCredentialToken { + accessToken: string + /** The credential's Dataverse environment, for Dataverse and Dynamics 365 tools. */ + instanceUrl?: string +} + +function isMicrosoftAppService(value: string | undefined): value is MicrosoftAppService { + return value === 'microsoft-ad' || value === 'sharepoint' || value === 'microsoft-dataverse' +} + +/** + * Resolves an executor-authorized Microsoft app credential (the executor + * already checked the caller may use it) to a token for the service the tool + * belongs to. The resource is decided by the tool's service, never by caller + * params: Graph for Entra ID and SharePoint, the stored environment for + * Dataverse. Any other service is refused, so a Microsoft block that needs a + * signed-in user can never receive an app-only token. + */ +export async function resolveMicrosoftAppCredentialToken( + credentialId: string, + service: string | undefined, + signal?: AbortSignal +): Promise { + if (!isMicrosoftAppService(service)) { + throw new TokenServiceAccountValidationError('invalid_credentials', 400, { + step: 'microsoft_app_service', + reason: 'this Microsoft service needs a signed-in user, not an app-only credential', + }) + } + const [row] = await db + .select({ + workspaceId: credential.workspaceId, + organizationId: credential.organizationId, + type: credential.type, + providerId: credential.providerId, + revokedAt: credential.revokedAt, + encryptedServiceAccountKey: credential.encryptedServiceAccountKey, + }) + .from(credential) + .where(eq(credential.id, credentialId)) + .limit(1) + if ( + !row || + row.type !== 'service_account' || + row.providerId !== MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID || + row.revokedAt !== null || + !row.encryptedServiceAccountKey + ) { + throw new MicrosoftAppCredentialError('Microsoft app credential is unavailable') + } + const encryptedKey = row.encryptedServiceAccountKey + const cacheKey = `${credentialId}:${service === 'microsoft-dataverse' ? 'dataverse' : 'graph'}:${encryptedKey.slice(0, 32)}` + const cached = tokenCache.get(cacheKey) + if (cached) return cached + signal?.throwIfAborted() + + // Concurrent block runs share one mint, so it is not tied to any one caller's signal. + return coalesceLocally(`microsoft-app:${cacheKey}`, async () => { + const { decrypted } = await decryptSecret(encryptedKey) + const { config } = storedSecretSchema.parse(JSON.parse(decrypted)) + let resource = GRAPH_RESOURCE + let instanceUrl: string | undefined + if (service === 'microsoft-dataverse') { + if (!config.environmentUrl) { + throw new TokenServiceAccountValidationError('invalid_credentials', 400, { + step: 'microsoft_app_environment', + reason: 'credential has no Dataverse environment; reconnect it with one', + }) + } + resource = config.environmentUrl + instanceUrl = config.environmentUrl + } + const owner: WorkloadIdentityOwner = row.workspaceId + ? { workspaceId: row.workspaceId } + : { organizationId: row.organizationId ?? undefined } + let token: EntraToken + try { + token = await requestEntraToken(config, resource, owner) + } catch (error) { + if (!(error instanceof MicrosoftAppCredentialError)) throw error + // Report in the shared service-account codes: an outage is not a bad credential. + throw new TokenServiceAccountValidationError( + error.status === undefined ? 'provider_unavailable' : providerFailureCode(error.status), + error.status ?? 502, + { step: 'microsoft_app_token', reason: error.aadsts ? `AADSTS${error.aadsts}` : undefined } + ) + } + const value: CachedToken = { accessToken: token.accessToken, instanceUrl } + const ttl = token.expiresInSeconds * 1000 - TOKEN_REUSE_MARGIN_MS + if (ttl > 0) tokenCache.set(cacheKey, value, { ttl }) + return value + }) +} diff --git a/apps/sim/lib/credentials/orchestration/credential-create.ts b/apps/sim/lib/credentials/orchestration/credential-create.ts index 41a1af00008..3efb824c52d 100644 --- a/apps/sim/lib/credentials/orchestration/credential-create.ts +++ b/apps/sim/lib/credentials/orchestration/credential-create.ts @@ -29,6 +29,10 @@ import { } from '@/lib/credentials/service-account-secret' import { isTokenServiceAccountProviderId } from '@/lib/credentials/token-service-accounts/descriptors' import { TokenServiceAccountValidationError } from '@/lib/credentials/token-service-accounts/errors' +import { + assertWorkloadIdentitySubjectUnclaimed, + KeylessCredentialRefusedError, +} from '@/lib/credentials/workload-identity/subject' import { getServiceConfigByProviderId } from '@/lib/oauth' import { SLACK_CUSTOM_BOT_PROVIDER_ID } from '@/lib/oauth/types' import { captureServerEvent } from '@/lib/posthog/server' @@ -117,6 +121,8 @@ export interface PerformCreateCredentialResult { created?: boolean /** Verified provider identity metadata for the application audit projection. */ auditMetadata?: Record + /** A refused keyless credential, carried unchanged so callers rethrow it with its detail code. */ + keylessRefusal?: KeylessCredentialRefusedError } interface ExistingCredentialSourceParams extends ResourceOwner { @@ -224,6 +230,16 @@ function failure( return { success: false, error, errorCode, ...extra } } +/** The failure result for a refused keyless credential: its class and code travel with it. */ +export function keylessFailure(refusal: KeylessCredentialRefusedError) { + return { + success: false as const, + error: refusal.message, + errorCode: refusal.code === 'conflict' ? ('conflict' as const) : ('validation' as const), + keylessRefusal: refusal, + } +} + export type CreateCredentialRecordParams = Omit & ResourceOwner @@ -269,6 +285,7 @@ export async function createCredentialRecord( : null let resolvedEnvOwnerUserId: string | null = null let resolvedEncryptedServiceAccountKey: string | null = null + let resolvedWorkloadIdentitySubject: string | undefined const extraAuditMetadata: Record = {} if (type === 'oauth') { @@ -323,14 +340,22 @@ export async function createCredentialRecord( fingerprint: params.fingerprint, privateKeyPassphrase: params.privateKeyPassphrase, region: params.region, + owner: { + ...(scope.kind === 'workspace' + ? { workspaceId: scope.workspaceId } + : { organizationId: scope.organizationId }), + actorUserId: userId, + }, }) resolvedProviderId = secret.providerId resolvedAccountId = null resolvedEnvOwnerUserId = null if (!resolvedDisplayName) resolvedDisplayName = secret.displayName resolvedEncryptedServiceAccountKey = secret.encryptedServiceAccountKey + resolvedWorkloadIdentitySubject = secret.newWorkloadIdentitySubject Object.assign(extraAuditMetadata, secret.auditMetadata) } catch (error) { + if (error instanceof KeylessCredentialRefusedError) return keylessFailure(error) if (error instanceof ServiceAccountSecretError) { return failure(error.message, 'validation') } @@ -511,6 +536,18 @@ export async function createCredentialRecord( if (innerExisting) throw new DuplicateCredentialError() } + if (resolvedWorkloadIdentitySubject) { + try { + await assertWorkloadIdentitySubjectUnclaimed(tx, { + subject: resolvedWorkloadIdentitySubject, + scope, + }) + } catch (error) { + if (error instanceof KeylessCredentialRefusedError) return keylessFailure(error) + throw error + } + } + await tx.insert(credential).values({ id: credentialId, ...resourceScopeColumns(scope), diff --git a/apps/sim/lib/credentials/orchestration/index.test.ts b/apps/sim/lib/credentials/orchestration/index.test.ts index fb875fd5a68..96d24024500 100644 --- a/apps/sim/lib/credentials/orchestration/index.test.ts +++ b/apps/sim/lib/credentials/orchestration/index.test.ts @@ -24,7 +24,9 @@ const { mockGetClientCredentialAccountDescriptor, mockDeleteConnectionCredential, mockDeleteOrphanedOAuthAccount, + mockAssertReconnectedSubjectUnclaimed, } = vi.hoisted(() => ({ + mockAssertReconnectedSubjectUnclaimed: vi.fn(), mockVerifyAndBuildServiceAccountSecret: vi.fn(), mockIsClientCredentialAccountProviderId: vi.fn(() => false), // Only a descriptor carrying `defaultAuthMethod` is multi-grant; single-grant @@ -51,6 +53,12 @@ vi.mock('@/lib/credentials/service-account-secret', () => ({ verifyAndBuildServiceAccountSecret: mockVerifyAndBuildServiceAccountSecret, ServiceAccountSecretError: class ServiceAccountSecretError extends Error {}, })) +vi.mock('@/lib/credentials/workload-identity/subject', async (importOriginal) => { + const actual = + await importOriginal() + mockAssertReconnectedSubjectUnclaimed.mockImplementation(actual.assertReconnectedSubjectUnclaimed) + return { ...actual, assertReconnectedSubjectUnclaimed: mockAssertReconnectedSubjectUnclaimed } +}) vi.mock('@/lib/credentials/client-credential-accounts/descriptors', () => ({ CLIENT_CREDENTIAL_ACCOUNT_REQUIRED_FIELDS: {}, isClientCredentialAccountProviderId: mockIsClientCredentialAccountProviderId, @@ -406,6 +414,62 @@ describe('performUpdateCredential — service-account secret rotation', () => { expect(mockRecordAudit).not.toHaveBeenCalled() }) + it('reports a refused keyless rebuild with its stable code', async () => { + mockCredential() + mockStoredBlob({ type: 'service_account', client_email: OLD_EMAIL }) + const { WorkloadIdentityNotConfiguredError } = await import( + '@/lib/credentials/workload-identity/subject' + ) + const refusal = new WorkloadIdentityNotConfiguredError() + mockVerifyAndBuildServiceAccountSecret.mockRejectedValue(refusal) + + const result = await performUpdateCredential({ + credentialId: 'cred-1', + userId: 'user-1', + serviceAccountJson: '{}', + }) + + expect(result).toMatchObject({ + success: false, + errorCode: 'validation', + keylessRefusal: refusal, + }) + expect(dbChainMockFns.update).not.toHaveBeenCalled() + }) + + it('reports a reconnect onto a subject another credential holds with the create code', async () => { + mockCredential() + mockStoredBlob({ type: 'google_workload_identity_v1', config: { subject: 'sim:old' } }) + mockVerifyAndBuildServiceAccountSecret.mockResolvedValue({ + providerId: 'google-service-account', + encryptedServiceAccountKey: 'rotated-cipher', + displayName: OLD_EMAIL, + auditMetadata: {}, + principal: null, + }) + const { WorkloadIdentitySubjectError } = await import( + '@/lib/credentials/workload-identity/subject' + ) + const refusal = new WorkloadIdentitySubjectError( + 'Another credential already uses this trust setup.', + 'WORKLOAD_IDENTITY_SUBJECT_IN_USE' + ) + mockAssertReconnectedSubjectUnclaimed.mockRejectedValueOnce(refusal) + + const result = await performUpdateCredential({ + credentialId: 'cred-1', + userId: 'user-1', + serviceAccountJson: '{"authType":"workload_identity"}', + }) + + expect(result).toMatchObject({ + success: false, + errorCode: 'conflict', + keylessRefusal: refusal, + }) + expect(dbChainMockFns.update).not.toHaveBeenCalled() + }) + it('conceals managed OAuth credentials from the ordinary update path', async () => { mockCredential({ type: 'managed_oauth' }) @@ -579,6 +643,32 @@ describe('createServiceAccountCredential', () => { expect(mockGetCredentialActorContext).toHaveBeenCalledWith('credential-1', 'user-1', {}) }) + it('carries a refused keyless credential’s code into the create result', async () => { + const { WorkloadIdentitySubjectError } = await import( + '@/lib/credentials/workload-identity/subject' + ) + const refusal = new WorkloadIdentitySubjectError( + 'This trust setup has expired.', + 'WORKLOAD_IDENTITY_PROOF_EXPIRED' + ) + mockVerifyAndBuildServiceAccountSecret.mockRejectedValue(refusal) + + const result = await createServiceAccountCredential({ + workspaceId: 'workspace-1', + userId: 'user-1', + providerId: 'aws-service-account', + displayName: 'AWS (keyless)', + serviceAccountJson: '{"authType":"oidc_role"}', + }) + + expect(result).toMatchObject({ + success: false, + errorCode: 'validation', + keylessRefusal: refusal, + }) + expect(dbChainMockFns.insert).not.toHaveBeenCalled() + }) + it('returns an accessible credential for an exact non-token secret replay', async () => { const existingCredential = { id: 'credential-1', diff --git a/apps/sim/lib/credentials/orchestration/index.ts b/apps/sim/lib/credentials/orchestration/index.ts index 73aab4fab51..497b7165009 100644 --- a/apps/sim/lib/credentials/orchestration/index.ts +++ b/apps/sim/lib/credentials/orchestration/index.ts @@ -37,7 +37,9 @@ import { deletePersonalEnvCredentialForUser, deleteWorkspaceEnvCredentials, } from '@/lib/credentials/environment' +import { GOOGLE_WORKLOAD_IDENTITY_SECRET_TYPE } from '@/lib/credentials/google-workload-identity' import { OciCredentialVerificationError } from '@/lib/credentials/oci-api-key-service-account.server' +import { keylessFailure } from '@/lib/credentials/orchestration/credential-create' import type { AtlassianProduct, ServiceAccountFieldId, @@ -46,12 +48,20 @@ import { ServiceAccountSecretError, verifyAndBuildServiceAccountSecret, } from '@/lib/credentials/service-account-secret' +import { SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/credentials/token-service-accounts/descriptors' import { TokenServiceAccountValidationError } from '@/lib/credentials/token-service-accounts/errors' +import { + assertReconnectedSubjectUnclaimed, + KeylessCredentialRefusedError, +} from '@/lib/credentials/workload-identity/subject' import { invalidateEffectiveDecryptedEnvCache } from '@/lib/environment/utils' import { findSlackSearchInstallation } from '@/lib/knowledge/application/slack-search/repository' import { ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID, + AWS_SERVICE_ACCOUNT_PROVIDER_ID, + DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, + MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, SLACK_CUSTOM_BOT_PROVIDER_ID, SLACK_CUSTOM_BOT_SECRET_TYPE, } from '@/lib/oauth/types' @@ -169,6 +179,12 @@ function deriveStoredDisplayName(blob: Record | null): string | if (blob.type === GOOGLE_SERVICE_ACCOUNT_KEY_TYPE && typeof blob.client_email === 'string') { return blob.client_email || undefined } + if (blob.type === GOOGLE_WORKLOAD_IDENTITY_SECRET_TYPE) { + const config = blob.config as { serviceAccountEmail?: unknown } | undefined + return typeof config?.serviceAccountEmail === 'string' + ? config.serviceAccountEmail || undefined + : undefined + } return undefined } @@ -226,6 +242,8 @@ export interface PerformCredentialResult { errorCode?: CredentialOrchestrationErrorCode /** Provider-specific code (e.g. Atlassian `invalid_credentials`) for client message mapping. */ providerErrorCode?: string + /** A refused keyless credential, carried unchanged so callers rethrow it with its detail code. */ + keylessRefusal?: KeylessCredentialRefusedError workspaceId?: string updatedFields?: string[] previousDisplayName?: string @@ -235,7 +253,11 @@ export interface PerformCredentialResult { export type UpdateCredentialRecordParams = Omit< PerformUpdateCredentialParams, 'userId' | 'actorName' | 'actorEmail' | 'allowedTypes' | 'reason' | 'request' -> & { credential: CredentialRow } +> & { + credential: CredentialRow + /** The person saving the change; binds owner-scoped secrets such as a keyless subject. */ + actorUserId: string +} /** Updates one already-authorized credential without surface authorization or audit. */ export async function updateCredentialRecord( @@ -306,6 +328,7 @@ export async function updateCredentialRecord( let rotatedSlackBotUserId: string | undefined let rotatedAuditMetadata: Record | undefined + let rotatedKeylessSecret: string | undefined if (hasRotationSecret) { const providerId = params.credential.providerId ?? '' @@ -334,6 +357,15 @@ export async function updateCredentialRecord( providerId === ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID && params.atlassianProduct === undefined const needsStoredUsername = params.username === undefined && isMultiGrantProvider + // A keyless AWS role, Databricks principal, Microsoft app, Google service account, or + // Snowflake service user keeps its trusted subject across reconnects. + const needsStoredBoundSubject = + providerId === AWS_SERVICE_ACCOUNT_PROVIDER_ID || + providerId === GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID || + providerId === DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID || + providerId === MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID || + (providerId === SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID && + params.serviceAccountJson !== undefined) // Rotating to a key that belongs to a different principal makes an // identity-derived label (a Google `client_email`, a Slack team name) @@ -351,7 +383,8 @@ export async function updateCredentialRecord( needsStoredAuthMethod || needsStoredUsername || needsStoredIdentity || - needsStoredAtlassianProduct + needsStoredAtlassianProduct || + needsStoredBoundSubject ? await readStoredSecretBlob(params.credential.id) : null @@ -431,10 +464,18 @@ export async function updateCredentialRecord( fingerprint: params.fingerprint, privateKeyPassphrase: params.privateKeyPassphrase, region: params.region, + owner: { + ...(params.credential.organizationId + ? { organizationId: params.credential.organizationId } + : { workspaceId: params.credential.workspaceId ?? undefined }), + actorUserId: params.actorUserId, + storedSecret: storedBlob, + }, }) updates.encryptedServiceAccountKey = secret.encryptedServiceAccountKey rotatedSlackBotUserId = secret.botUserId rotatedAuditMetadata = secret.auditMetadata + if (needsStoredBoundSubject) rotatedKeylessSecret = secret.encryptedServiceAccountKey if (needsStoredIdentity) { const previousIdentity = deriveStoredDisplayName(storedBlob) @@ -448,6 +489,7 @@ export async function updateCredentialRecord( } } } catch (error) { + if (error instanceof KeylessCredentialRefusedError) return keylessFailure(error) if (error instanceof ServiceAccountSecretError) { return { success: false, error: error.message, errorCode: 'validation' } } @@ -497,7 +539,24 @@ export async function updateCredentialRecord( } updates.updatedAt = new Date() - await db.update(credential).set(updates).where(eq(credential.id, params.credentialId)) + if (rotatedKeylessSecret) { + const rotatedSecret = rotatedKeylessSecret + try { + await db.transaction(async (tx) => { + await assertReconnectedSubjectUnclaimed(tx, { + rotatedSecret, + scope: resourceScopeFromOwner(params.credential), + credentialId: params.credentialId, + }) + await tx.update(credential).set(updates).where(eq(credential.id, params.credentialId)) + }) + } catch (error) { + if (error instanceof KeylessCredentialRefusedError) return keylessFailure(error) + throw error + } + } else { + await db.update(credential).set(updates).where(eq(credential.id, params.credentialId)) + } // The flag rides the environment snapshot into every run's redaction catalog, so a flip // must not serve stale from the same-process snapshot cache. Cross-process readers are @@ -571,7 +630,11 @@ export async function performUpdateCredential( } } - const result = await updateCredentialRecord({ ...params, credential: access.credential }) + const result = await updateCredentialRecord({ + ...params, + credential: access.credential, + actorUserId: params.userId, + }) if (!result.success) return result recordAudit({ diff --git a/apps/sim/lib/credentials/service-account-config.ts b/apps/sim/lib/credentials/service-account-config.ts new file mode 100644 index 00000000000..61eb3904787 --- /dev/null +++ b/apps/sim/lib/credentials/service-account-config.ts @@ -0,0 +1,43 @@ +import { getErrorMessage } from '@sim/utils/errors' +import type { z } from 'zod' + +const MAX_CONFIG_BYTES = 64 * 1024 + +/** + * A failure's message without its closing punctuation, so it can sit inside a + * sentence that adds the remedy — a provider message that already ends in a + * period would otherwise print two. + */ +export function failureClause(error: unknown): string { + return getErrorMessage(error, 'Unknown error').replace(/[.\s]+$/, '') +} + +/** + * Parses a service-account configuration submitted as JSON (the connect modal's + * `serviceAccountJson`) against its provider schema. Returns the first + * user-facing problem instead of throwing, so each provider raises it in its + * own error type. + */ +export function parseServiceAccountConfig( + serialized: string, + schema: T, + label: string +): { success: true; data: z.infer } | { success: false; message: string } { + if (Buffer.byteLength(serialized, 'utf8') > MAX_CONFIG_BYTES) { + return { success: false, message: `${label} configuration is too large` } + } + let raw: unknown + try { + raw = JSON.parse(serialized) + } catch { + return { success: false, message: `Invalid ${label} configuration` } + } + const parsed = schema.safeParse(raw) + if (!parsed.success) { + return { + success: false, + message: parsed.error.issues[0]?.message ?? `Invalid ${label} configuration`, + } + } + return { success: true, data: parsed.data } +} diff --git a/apps/sim/lib/credentials/service-account-fields.test.ts b/apps/sim/lib/credentials/service-account-fields.test.ts new file mode 100644 index 00000000000..379241cbc17 --- /dev/null +++ b/apps/sim/lib/credentials/service-account-fields.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { getMissingServiceAccountFields } from '@/lib/credentials/service-account-fields' + +describe('required fields for a service-account create', () => { + it('accepts a keyless Snowflake configuration in place of the token and host', () => { + expect( + getMissingServiceAccountFields('snowflake-service-account', { serviceAccountJson: '{}' }) + ).toEqual([]) + }) + + it('still asks a Snowflake create with neither mode for the token and host', () => { + expect(getMissingServiceAccountFields('snowflake-service-account', {})).toEqual( + expect.arrayContaining(['apiToken', 'domain']) + ) + }) + + it('does not let a JSON configuration stand in for another token provider’s token', () => { + expect( + getMissingServiceAccountFields('hubspot-service-account', { serviceAccountJson: '{}' }) + ).toEqual(['apiToken']) + }) +}) diff --git a/apps/sim/lib/credentials/service-account-fields.ts b/apps/sim/lib/credentials/service-account-fields.ts index 853679387a3..6070efddd76 100644 --- a/apps/sim/lib/credentials/service-account-fields.ts +++ b/apps/sim/lib/credentials/service-account-fields.ts @@ -1,8 +1,12 @@ import { CLIENT_CREDENTIAL_ACCOUNT_REQUIRED_FIELDS } from '@/lib/credentials/client-credential-accounts/descriptors' import { TOKEN_SERVICE_ACCOUNT_REQUIRED_FIELDS } from '@/lib/credentials/token-service-accounts/descriptors' +import { isWorkloadIdentityServiceAccountProviderId } from '@/lib/credentials/workload-identity/audiences' import { ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID, + AWS_SERVICE_ACCOUNT_PROVIDER_ID, + DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, + MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID, ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID, SLACK_CUSTOM_BOT_PROVIDER_ID, @@ -47,6 +51,9 @@ export type ServiceAccountFieldId = */ export const SERVICE_ACCOUNT_REQUIRED_FIELDS: Record = { [ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID]: ['serviceAccountJson'], + [AWS_SERVICE_ACCOUNT_PROVIDER_ID]: ['serviceAccountJson'], + [DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID]: ['serviceAccountJson'], + [MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID]: ['serviceAccountJson'], [GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID]: ['serviceAccountJson'], [ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID]: ['apiToken', 'domain'], [SLACK_CUSTOM_BOT_PROVIDER_ID]: ['signingSecret', 'botToken'], @@ -70,10 +77,42 @@ export const FALLBACK_SERVICE_ACCOUNT_REQUIRED_FIELDS: readonly ServiceAccountFi 'serviceAccountJson', ] -export function getServiceAccountRequiredFields( +/** Every keyless credential is one JSON configuration carrying its subject and proof. */ +const KEYLESS_REQUIRED_FIELDS: readonly ServiceAccountFieldId[] = ['serviceAccountJson'] + +/** + * The create-body fields a keyless (workload identity) credential sends in + * place of the provider's required fields, or `undefined` for a provider with + * no keyless mode. For most keyless providers this is their only field; for + * Snowflake it replaces the token fields. The keyless builder validates the + * configuration itself. + */ +function getKeylessServiceAccountFields( + providerId: string | null | undefined +): readonly ServiceAccountFieldId[] | undefined { + return isWorkloadIdentityServiceAccountProviderId(providerId) + ? KEYLESS_REQUIRED_FIELDS + : undefined +} + +function getServiceAccountRequiredFields( providerId: string | null | undefined ): readonly ServiceAccountFieldId[] { return providerId && Object.hasOwn(SERVICE_ACCOUNT_REQUIRED_FIELDS, providerId) ? SERVICE_ACCOUNT_REQUIRED_FIELDS[providerId] : FALLBACK_SERVICE_ACCOUNT_REQUIRED_FIELDS } + +/** + * The required create-body fields a submission is missing: none when it + * completes the provider's keyless alternative, otherwise the provider's own + * required fields that are empty. + */ +export function getMissingServiceAccountFields( + providerId: string | null | undefined, + provided: Partial> +): ServiceAccountFieldId[] { + const keyless = getKeylessServiceAccountFields(providerId) + if (keyless?.every((field) => provided[field])) return [] + return getServiceAccountRequiredFields(providerId).filter((field) => !provided[field]) +} diff --git a/apps/sim/lib/credentials/service-account-provider-ids.ts b/apps/sim/lib/credentials/service-account-provider-ids.ts index a060015a393..918b2525551 100644 --- a/apps/sim/lib/credentials/service-account-provider-ids.ts +++ b/apps/sim/lib/credentials/service-account-provider-ids.ts @@ -8,7 +8,10 @@ import { } from '@/lib/credentials/token-service-accounts/descriptors' import { ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID, + AWS_SERVICE_ACCOUNT_PROVIDER_ID, + DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, + MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID, ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID, SLACK_CUSTOM_BOT_PROVIDER_ID, @@ -33,6 +36,9 @@ export function asServiceAccountProviderId( value === SLACK_CUSTOM_BOT_PROVIDER_ID || value === OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID || value === ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID || + value === AWS_SERVICE_ACCOUNT_PROVIDER_ID || + value === DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID || + value === MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID || isTokenServiceAccountProviderId(value) || isClientCredentialAccountProviderId(value) ) { @@ -76,6 +82,9 @@ export function getServiceAccountConnectNoun(providerId: string): string { if (providerId === SLACK_CUSTOM_BOT_PROVIDER_ID) return 'custom bot' if (providerId === OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID) return 'API key' if (providerId === ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID) return 'database connection' + if (providerId === AWS_SERVICE_ACCOUNT_PROVIDER_ID) return 'AWS credential' + if (providerId === DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID) return 'Databricks connection' + if (providerId === MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID) return 'Microsoft app credential' const descriptor = getTokenServiceAccountDescriptor(providerId) ?? getClientCredentialAccountDescriptor(providerId) return descriptor?.connectNoun ?? 'service account' diff --git a/apps/sim/lib/credentials/service-account-secret.test.ts b/apps/sim/lib/credentials/service-account-secret.test.ts index d2e9f241d0a..4cdd2f13815 100644 --- a/apps/sim/lib/credentials/service-account-secret.test.ts +++ b/apps/sim/lib/credentials/service-account-secret.test.ts @@ -380,6 +380,99 @@ describe('verifyAndBuildServiceAccountSecret', () => { expect(result.principal).toBeNull() }) + it.each([ + [ + 'aws-service-account', + { authType: 'oidc_role', roleArn: 'arn:aws:iam::123456789012:role/SimWorkflows' }, + 'Use access keys.', + ], + [ + 'databricks-service-account', + { + authType: 'oidc_federation', + host: 'dbc-abc123.cloud.databricks.com', + clientId: '11111111-2222-3333-4444-555555555555', + }, + 'Use a personal access token or a service principal OAuth secret.', + ], + [ + 'snowflake-service-account', + { authType: 'workload_identity', host: 'myorg-myaccount.snowflakecomputing.com' }, + 'Use a programmatic access token.', + ], + [ + 'microsoft-app-service-account', + { + authType: 'federated', + tenantId: '11111111-2222-3333-4444-555555555555', + clientId: '66666666-7777-8888-9999-000000000000', + }, + 'Use a client secret or certificate.', + ], + [ + 'google-service-account', + { + authType: 'workload_identity', + projectNumber: '123456789012', + poolId: 'sim-pool', + providerId: 'sim-provider', + serviceAccountEmail: 'sim-workflows@my-project.iam.gserviceaccount.com', + }, + 'Use a JSON key.', + ], + ])( + 'refuses a keyless %s credential where this deployment cannot sign', + async (providerId, configuration, remedy) => { + await expect( + verifyAndBuildServiceAccountSecret(providerId, { + serviceAccountJson: JSON.stringify(configuration), + owner: { workspaceId: 'ws-1', actorUserId: 'user-1' }, + }) + ).rejects.toMatchObject({ + code: 'validation', + detailCode: 'WORKLOAD_IDENTITY_NOT_CONFIGURED', + message: `Keyless connections are not available on this deployment. ${remedy}`, + }) + } + ) + + it.each([ + [ + 'aws-service-account', + { authType: 'oidc_role', roleArn: 'arn:aws:iam::123456789012:role/SimWorkflows' }, + { type: 'aws_credential_v1' }, + ], + [ + 'databricks-service-account', + { + authType: 'oidc_federation', + host: 'dbc-abc123.cloud.databricks.com', + clientId: '11111111-2222-3333-4444-555555555555', + }, + { type: 'databricks_credential_v1' }, + ], + ])( + 'refuses reconnecting a keyless %s credential where this deployment cannot sign', + async (providerId, configuration, secret) => { + await expect( + verifyAndBuildServiceAccountSecret(providerId, { + serviceAccountJson: JSON.stringify(configuration), + owner: { + workspaceId: 'ws-1', + actorUserId: 'user-1', + storedSecret: { + ...secret, + config: { + ...configuration, + subject: 'sim:workspace:ws-1:credential:abcdef0123456789', + }, + }, + }, + }) + ).rejects.toMatchObject({ detailCode: 'WORKLOAD_IDENTITY_NOT_CONFIGURED' }) + } + ) + it('throws when client-credential required fields are missing, without minting', async () => { await expect( verifyAndBuildServiceAccountSecret('zoom-service-account', { diff --git a/apps/sim/lib/credentials/service-account-secret.ts b/apps/sim/lib/credentials/service-account-secret.ts index 4bf9d0b8768..dcd62f7ea20 100644 --- a/apps/sim/lib/credentials/service-account-secret.ts +++ b/apps/sim/lib/credentials/service-account-secret.ts @@ -6,6 +6,10 @@ import { normalizeAtlassianDomain, validateAtlassianServiceAccount, } from '@/lib/credentials/atlassian-service-account' +import { + AwsCredentialError, + verifyAndEncryptAwsCredential, +} from '@/lib/credentials/aws-service-account.server' import { CLIENT_CREDENTIAL_ACCOUNT_SECRET_TYPE, type ClientCredentialAccountFieldId, @@ -19,7 +23,20 @@ import { type ClientCredentialAccountSecretBlob, getClientCredentialAccountMinter, } from '@/lib/credentials/client-credential-accounts/server' +import { + DatabricksCredentialError, + verifyAndEncryptDatabricksCredential, +} from '@/lib/credentials/databricks-service-account.server' import { slackCustomBotDisplayName } from '@/lib/credentials/display-name' +import { + GoogleWorkloadIdentityError, + isGoogleWorkloadIdentitySubmission, + verifyAndEncryptGoogleWorkloadIdentityCredential, +} from '@/lib/credentials/google-workload-identity.server' +import { + MicrosoftAppCredentialError, + verifyAndEncryptMicrosoftAppCredential, +} from '@/lib/credentials/microsoft-app-service-account.server' import { verifyAndEncryptOciApiKeyCredential } from '@/lib/credentials/oci-api-key-service-account.server' import { verifyAndEncryptOracleDatabaseCredential } from '@/lib/credentials/oracledb-service-account.server' import { @@ -27,20 +44,32 @@ import { serviceAccountPrincipalMetadata, } from '@/lib/credentials/principal' import type { AtlassianProduct } from '@/lib/credentials/service-account-fields' +import { + SnowflakeCredentialError, + verifyAndEncryptSnowflakeKeylessCredential, +} from '@/lib/credentials/snowflake-service-account.server' import { getTokenServiceAccountDescriptor, isTokenServiceAccountProviderId, + SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, TOKEN_SERVICE_ACCOUNT_SECRET_TYPE, } from '@/lib/credentials/token-service-accounts/descriptors' import { getTokenServiceAccountValidator, type TokenServiceAccountSecretBlob, } from '@/lib/credentials/token-service-accounts/server' +import { + KeylessCredentialRefusedError, + type WorkloadIdentityCredentialOwner, +} from '@/lib/credentials/workload-identity/subject' import { GITHUB_INSTALLATION_PROVIDER_ID } from '@/lib/oauth/github-installation-types' import { ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID, ATLASSIAN_SERVICE_ACCOUNT_SECRET_TYPE, + AWS_SERVICE_ACCOUNT_PROVIDER_ID, + DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, + MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID, ORACLE_DATABASE_SERVICE_ACCOUNT_PROVIDER_ID, SLACK_CUSTOM_BOT_PROVIDER_ID, @@ -71,6 +100,12 @@ export interface ServiceAccountSecretFields { fingerprint?: string privateKeyPassphrase?: string region?: string + /** + * Who owns the credential and who is saving it. Only providers whose secret + * is bound to its owner read it (an AWS OIDC role subject is bound to the + * workspace and the person who prepared it); every other builder ignores it. + */ + owner?: WorkloadIdentityCredentialOwner } export interface ServiceAccountSecretResult { @@ -87,6 +122,11 @@ export interface ServiceAccountSecretResult { principal: ServiceAccountPrincipal | null /** Slack custom bot: the derived bot user id (for reaction self-drop). */ botUserId?: string + /** + * Keyless AWS, Databricks, Snowflake, Microsoft app, or Google: a subject this save binds that + * the credential did not already hold, which must not belong to another credential of the owner. + */ + newWorkloadIdentitySubject?: string } /** Thrown when a service-account secret is missing or fails provider verification. */ @@ -207,6 +247,9 @@ async function buildGoogleServiceAccountSecret( 'serviceAccountJson is required for service account credentials' ) } + if (isGoogleWorkloadIdentitySubmission(serviceAccountJson)) { + return buildGoogleWorkloadIdentitySecret(serviceAccountJson, fields.owner) + } const jsonParseResult = serviceAccountJsonSchema.safeParse(serviceAccountJson) if (!jsonParseResult.success) { throw new ServiceAccountSecretError( @@ -233,6 +276,133 @@ async function buildGoogleServiceAccountSecret( } } +/** A keyless Google Cloud credential: Workload Identity Federation, no stored key. */ +async function buildGoogleWorkloadIdentitySecret( + serviceAccountJson: string, + owner: ServiceAccountSecretFields['owner'] +): Promise { + if (!owner) throw new ServiceAccountSecretError('Google Cloud credential owner is required') + let result + try { + result = await verifyAndEncryptGoogleWorkloadIdentityCredential(serviceAccountJson, owner) + } catch (error) { + if (error instanceof KeylessCredentialRefusedError) throw error + if (error instanceof GoogleWorkloadIdentityError) { + throw new ServiceAccountSecretError(error.message) + } + throw new ServiceAccountSecretError( + `Could not verify the Google Cloud keyless credential: ${getErrorMessage(error, 'Unknown error')}` + ) + } + const principal: ServiceAccountPrincipal = { kind: 'user', id: result.serviceAccountEmail } + return { + providerId: GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, + encryptedServiceAccountKey: result.encryptedServiceAccountKey, + displayName: result.serviceAccountEmail, + auditMetadata: { + googleClientEmail: result.serviceAccountEmail, + googleWorkloadIdentityProvider: result.workloadIdentityProvider, + ...serviceAccountPrincipalMetadata(principal), + }, + principal, + newWorkloadIdentitySubject: result.newSubject, + } +} + +async function buildAwsServiceAccountSecret( + fields: ServiceAccountSecretFields +): Promise { + if (!fields.serviceAccountJson) + throw new ServiceAccountSecretError('AWS credential configuration is required') + if (!fields.owner) throw new ServiceAccountSecretError('AWS credential owner is required') + let result + try { + result = await verifyAndEncryptAwsCredential(fields.serviceAccountJson, fields.owner) + } catch (error) { + if (error instanceof KeylessCredentialRefusedError) throw error + if (error instanceof AwsCredentialError) throw new ServiceAccountSecretError(error.message) + throw new ServiceAccountSecretError( + `Could not verify the AWS credential: ${getErrorMessage(error, 'Unknown error')}` + ) + } + const principal: ServiceAccountPrincipal = { kind: 'user', id: result.callerArn } + return { + providerId: AWS_SERVICE_ACCOUNT_PROVIDER_ID, + encryptedServiceAccountKey: result.encryptedServiceAccountKey, + displayName: result.callerArn, + auditMetadata: serviceAccountPrincipalMetadata(principal), + principal, + newWorkloadIdentitySubject: result.newSubject, + } +} + +async function buildDatabricksServiceAccountSecret( + fields: ServiceAccountSecretFields +): Promise { + if (!fields.serviceAccountJson) + throw new ServiceAccountSecretError('Databricks credential configuration is required') + if (!fields.owner) throw new ServiceAccountSecretError('Databricks credential owner is required') + let result + try { + result = await verifyAndEncryptDatabricksCredential(fields.serviceAccountJson, fields.owner) + } catch (error) { + if (error instanceof KeylessCredentialRefusedError) throw error + if (error instanceof DatabricksCredentialError) { + throw new ServiceAccountSecretError(error.message) + } + throw new ServiceAccountSecretError( + `Could not verify the Databricks credential: ${getErrorMessage(error, 'Unknown error')}` + ) + } + const principal: ServiceAccountPrincipal = { kind: 'user', id: result.identity } + return { + providerId: DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, + encryptedServiceAccountKey: result.encryptedServiceAccountKey, + displayName: result.identity, + auditMetadata: serviceAccountPrincipalMetadata(principal), + principal, + newWorkloadIdentitySubject: result.newSubject, + } +} + +async function buildMicrosoftAppServiceAccountSecret( + fields: ServiceAccountSecretFields +): Promise { + if (!fields.serviceAccountJson) + throw new ServiceAccountSecretError('Microsoft app credential configuration is required') + if (!fields.owner) + throw new ServiceAccountSecretError('Microsoft app credential owner is required') + let result + try { + result = await verifyAndEncryptMicrosoftAppCredential(fields.serviceAccountJson, fields.owner) + } catch (error) { + if (error instanceof KeylessCredentialRefusedError) throw error + if (error instanceof MicrosoftAppCredentialError) { + throw new ServiceAccountSecretError(error.message) + } + throw new ServiceAccountSecretError( + `Could not verify the Microsoft app credential: ${getErrorMessage(error, 'Unknown error')}` + ) + } + // The app's service principal is the identity every app-only token acts as. + const principal: ServiceAccountPrincipal = { + kind: 'user', + id: result.servicePrincipalId, + label: result.displayName, + } + return { + providerId: MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, + encryptedServiceAccountKey: result.encryptedServiceAccountKey, + displayName: result.displayName, + auditMetadata: { + microsoftTenantId: result.tenantId, + ...serviceAccountPrincipalMetadata(principal), + }, + principal, + newWorkloadIdentitySubject: result.newSubject, + } +} + async function buildOracleDatabaseServiceAccountSecret( fields: ServiceAccountSecretFields ): Promise { @@ -319,7 +489,9 @@ async function buildTokenServiceAccountSecret( providerId, apiToken, ...(requiresDomain ? { domain: validation.normalizedDomain ?? domain } : {}), - ...(requiresWorkspace ? { providerWorkspaceId } : {}), + ...(requiresWorkspace + ? { providerWorkspaceId: validation.normalizedWorkspaceId ?? providerWorkspaceId } + : {}), metadata: { ...validation.storedMetadata, ...principalMetadata }, } const { encrypted } = await encryptSecret(JSON.stringify(blob)) @@ -332,6 +504,38 @@ async function buildTokenServiceAccountSecret( } } +/** + * Builds a Snowflake secret. A JSON configuration is the keyless mode (no + * stored secret); otherwise the pasted programmatic access token goes through + * the token-paste builder exactly as before. Snowflake's own refusals keep the + * shared validation codes so callers report them like any token check. + */ +async function buildSnowflakeServiceAccountSecret( + fields: ServiceAccountSecretFields +): Promise { + if (!fields.serviceAccountJson) { + return buildTokenServiceAccountSecret(SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, fields) + } + if (fields.apiToken) { + throw new ServiceAccountSecretError( + 'Send either a programmatic access token or a keyless configuration, not both' + ) + } + if (!fields.owner) throw new ServiceAccountSecretError('Snowflake credential owner is required') + try { + const result = await verifyAndEncryptSnowflakeKeylessCredential( + fields.serviceAccountJson, + fields.owner + ) + return { providerId: SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, ...result } + } catch (error) { + if (error instanceof SnowflakeCredentialError) { + throw new ServiceAccountSecretError(error.message) + } + throw error + } +} + /** * Builds a client-credential service-account secret for any provider registered * in `CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS`: verifies the provider-specific @@ -427,6 +631,10 @@ const SERVICE_ACCOUNT_SECRET_BUILDERS: Record encryptionMock) +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { credential } from '@sim/db/schema' +import { env } from '@/lib/core/config/env' +import { + resolveSnowflakeCredentialToken, + verifyAndEncryptSnowflakeKeylessCredential, +} from '@/lib/credentials/snowflake-service-account.server' +import { + SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, + TOKEN_SERVICE_ACCOUNT_SECRET_TYPE, +} from '@/lib/credentials/token-service-accounts/descriptors' +import { + getWorkloadIdentityIssuer, + getWorkloadIdentityJwks, +} from '@/lib/credentials/workload-identity/issuer' +import { issueWorkloadIdentitySubject } from '@/lib/credentials/workload-identity/subject' + +const HOST = 'myorg-myaccount.snowflakecomputing.com' +const owner = { workspaceId: 'ws-1', actorUserId: 'user-1' } +const secureFetch = inputValidationMockFns.mockSecureFetchWithValidation + +function setEnv(key: string, value: string | undefined) { + Reflect.set(env, key, value) +} + +/** + * Snowflake's side of workload identity federation: the SQL API accepts a + * request only when it carries `WIF.OIDC.` with the federation token type, + * and the JWT verifies against Sim's published keys with Snowflake's audience, + * Sim's issuer, an `iat`, and the subject a service user was created with. + */ +function snowflakeAccountTrusting(trustedSubject: string) { + secureFetch.mockImplementation( + async (_url: string, init: { headers: Record }) => { + const authorization = init.headers.Authorization ?? '' + const isFederation = + init.headers['X-Snowflake-Authorization-Token-Type'] === 'WORKLOAD_IDENTITY_FEDERATION' && + authorization.startsWith('Bearer WIF.OIDC.') + if (!isFederation) return jsonResponse({ message: 'Invalid token type' }, 401) + try { + const { payload } = await jwtVerify( + authorization.slice('Bearer WIF.OIDC.'.length), + createLocalJWKSet(await getWorkloadIdentityJwks()), + { + issuer: getWorkloadIdentityIssuer(), + audience: 'snowflakecomputing.com', + requiredClaims: ['iat'], + } + ) + if (payload.sub !== trustedSubject) throw new Error('no user for this issuer and subject') + } catch { + return jsonResponse({ code: '390144', message: 'Incorrect username or password' }, 401) + } + return jsonResponse({ data: [['SIM_WORKFLOWS', 'MYORG-MYACCOUNT', 'ANALYST']] }) + } + ) +} + +function keylessConfig(fields: Record) { + return JSON.stringify({ authType: 'workload_identity', host: HOST, ...fields }) +} + +async function codeOf(promise: Promise) { + return promise.then( + () => 'saved', + (error: { code?: string; message?: string }) => error.code ?? error.message + ) +} + +beforeAll(() => { + setEnv( + 'WORKLOAD_IDENTITY_SIGNING_KEY', + generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey.export({ + type: 'pkcs8', + format: 'pem', + }) as string + ) + setEnv('WORKLOAD_IDENTITY_ISSUER', 'https://sim.example.com/oidc/workload') +}) + +afterAll(() => { + setEnv('WORKLOAD_IDENTITY_SIGNING_KEY', undefined) + setEnv('WORKLOAD_IDENTITY_ISSUER', undefined) +}) + +beforeEach(() => { + secureFetch.mockReset() + vi.stubGlobal('fetch', vi.fn()) + encryptionMockFns.mockEncryptSecret.mockReset() + encryptionMockFns.mockEncryptSecret.mockResolvedValue({ encrypted: 'ciphertext', iv: 'iv' }) +}) + +describe('saving a keyless Snowflake connection', () => { + it('signs in as the service user created with Sim’s issuer and this credential’s subject', async () => { + const grant = issueWorkloadIdentitySubject('snowflake', owner, owner.actorUserId) + snowflakeAccountTrusting(grant.subject) + + const saved = await verifyAndEncryptSnowflakeKeylessCredential( + keylessConfig({ subject: grant.subject, subjectProof: grant.subjectProof }), + owner + ) + + expect(saved.displayName).toBe('SIM_WORKFLOWS (MYORG-MYACCOUNT)') + expect(saved.newWorkloadIdentitySubject).toBe(grant.subject) + expect(secureFetch.mock.calls[0][0]).toBe(`https://${HOST}/api/v2/statements`) + expect(secureFetch.mock.calls[0][1]).toMatchObject({ maxRedirects: 0 }) + expect(fetch).not.toHaveBeenCalled() + const stored = JSON.parse(encryptionMockFns.mockEncryptSecret.mock.calls[0][0]) + expect(stored.config).toEqual({ + authType: 'workload_identity', + host: HOST, + subject: grant.subject, + }) + }) + + it('reports an account with no user for this issuer and subject as a rejected credential', async () => { + const grant = issueWorkloadIdentitySubject('snowflake', owner, owner.actorUserId) + snowflakeAccountTrusting('sim:workspace:ws-1:credential:someoneelse0000') + + const outcome = await codeOf( + verifyAndEncryptSnowflakeKeylessCredential( + keylessConfig({ subject: grant.subject, subjectProof: grant.subjectProof }), + owner + ) + ) + + expect(outcome).toBe('invalid_credentials') + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + it('reports a mistyped account as a bad host, not a rejected credential', async () => { + const grant = issueWorkloadIdentitySubject('snowflake', owner, owner.actorUserId) + secureFetch.mockResolvedValue(new Response('File not Found', { status: 404 })) + + const outcome = await codeOf( + verifyAndEncryptSnowflakeKeylessCredential( + keylessConfig({ subject: grant.subject, subjectProof: grant.subjectProof }), + owner + ) + ) + + expect(outcome).toBe('site_not_found') + }) + + it('does not save when the egress guard refuses the account host', async () => { + const grant = issueWorkloadIdentitySubject('snowflake', owner, owner.actorUserId) + secureFetch.mockRejectedValue(new Error('URL resolves to a blocked address')) + + const outcome = await codeOf( + verifyAndEncryptSnowflakeKeylessCredential( + keylessConfig({ subject: grant.subject, subjectProof: grant.subjectProof }), + owner + ) + ) + + expect(outcome).toBe('provider_unavailable') + expect(fetch).not.toHaveBeenCalled() + expect(encryptionMockFns.mockEncryptSecret).not.toHaveBeenCalled() + }) + + it.each([ + ['no host', { host: '' }], + ['a host outside Snowflake', { host: 'attacker.example.com' }], + ['a lookalike host', { host: `${HOST}.attacker.example.com` }], + ])('refuses %s before sending a token anywhere', async (_case, fields) => { + const grant = issueWorkloadIdentitySubject('snowflake', owner, owner.actorUserId) + + await expect( + verifyAndEncryptSnowflakeKeylessCredential( + keylessConfig({ subject: grant.subject, subjectProof: grant.subjectProof, ...fields }), + owner + ) + ).rejects.toThrow() + expect(secureFetch).not.toHaveBeenCalled() + }) + + it('refuses a subject issued to someone else, so a member cannot borrow another credential’s trust', async () => { + const grant = issueWorkloadIdentitySubject('snowflake', owner, 'another-user') + + await expect( + verifyAndEncryptSnowflakeKeylessCredential( + keylessConfig({ subject: grant.subject, subjectProof: grant.subjectProof }), + owner + ) + ).rejects.toThrow(/belongs to someone else/) + expect(secureFetch).not.toHaveBeenCalled() + }) +}) + +describe('getting a Snowflake token for a run', () => { + function storedCredential(secret: Record) { + resetDbChainMock() + queueTableRows(credential, [ + { workspaceId: 'ws-1', organizationId: null, encryptedServiceAccountKey: 'ciphertext' }, + ]) + encryptionMockFns.mockDecryptSecret.mockResolvedValueOnce({ decrypted: JSON.stringify(secret) }) + } + + it('mints a federation token for a keyless credential’s own subject and host', async () => { + const subject = 'sim:workspace:ws-1:credential:abcdef0123456789' + storedCredential({ + type: 'snowflake_workload_identity_v1', + config: { authType: 'workload_identity', host: HOST, subject }, + }) + + const resolved = await resolveSnowflakeCredentialToken('cred-1') + + expect(resolved).toMatchObject({ domain: HOST, authStyle: 'snowflake-workload-identity' }) + const { payload } = await jwtVerify( + resolved.accessToken, + createLocalJWKSet(await getWorkloadIdentityJwks()), + { issuer: getWorkloadIdentityIssuer(), audience: 'snowflakecomputing.com' } + ) + expect(payload.sub).toBe(subject) + }) + + it('resolves a programmatic access token exactly as before, with no token type', async () => { + storedCredential({ + type: TOKEN_SERVICE_ACCOUNT_SECRET_TYPE, + providerId: SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, + apiToken: 'pat-secret', + domain: HOST, + }) + + await expect(resolveSnowflakeCredentialToken('cred-1')).resolves.toEqual({ + accessToken: 'pat-secret', + domain: HOST, + providerWorkspaceId: undefined, + }) + }) +}) diff --git a/apps/sim/lib/credentials/snowflake-service-account.server.ts b/apps/sim/lib/credentials/snowflake-service-account.server.ts new file mode 100644 index 00000000000..fdf82264b1a --- /dev/null +++ b/apps/sim/lib/credentials/snowflake-service-account.server.ts @@ -0,0 +1,246 @@ +import { db } from '@sim/db' +import { credential } from '@sim/db/schema' +import { eq } from 'drizzle-orm' +import { z } from 'zod' +import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' +import { + type ServiceAccountPrincipal, + serviceAccountPrincipalMetadata, +} from '@/lib/credentials/principal' +import { parseServiceAccountConfig } from '@/lib/credentials/service-account-config' +import { SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/credentials/token-service-accounts/descriptors' +import { parseTokenServiceAccountSecretBlob } from '@/lib/credentials/token-service-accounts/server' +import { validateSnowflakeWorkloadIdentity } from '@/lib/credentials/token-service-accounts/validators/snowflake' +import { WORKLOAD_IDENTITY_AUDIENCES } from '@/lib/credentials/workload-identity/audiences' +import { + isWorkloadIdentityConfigured, + mintWorkloadIdentityToken, + type WorkloadIdentityOwner, +} from '@/lib/credentials/workload-identity/issuer' +import { + bindWorkloadIdentitySubject, + type WorkloadIdentityCredentialOwner, + WorkloadIdentityNotConfiguredError, +} from '@/lib/credentials/workload-identity/subject' +import { + normalizeSnowflakeHost, + SNOWFLAKE_WORKLOAD_IDENTITY_AUTH_STYLE, +} from '@/tools/snowflake/utils' + +/** + * Keyless Snowflake connections: no secret is stored. Sim signs a short-lived + * OIDC token for the credential's own subject, and the account's service user + * (created with `WORKLOAD_IDENTITY = (TYPE = OIDC ISSUER = … SUBJECT = …)`) + * accepts it through workload identity federation on every SQL API request. + * + * Snowflake matches the exact `ISSUER` + `SUBJECT` pair, unique per account + * (https://docs.snowflake.com/en/user-guide/workload-identity-federation), so + * unlike a Databricks federation policy there is no looser claim match that + * could let another Sim credential's subject in. + * + * Programmatic-access-token credentials share the provider and keep their + * token-paste blob; only the blob type tells the two modes apart. + */ + +const SNOWFLAKE_KEYLESS_SECRET_TYPE = 'snowflake_workload_identity_v1' as const +const SNOWFLAKE_WORKLOAD_IDENTITY_AUDIENCE = WORKLOAD_IDENTITY_AUDIENCES.snowflake + +/** + * Snowflake validates the token on each request, so it only has to outlive one + * tool call. Cortex Analyst reuses it for the SQL it generates after the + * answer, which can take longer than the issuer's five-minute default. + */ +const SNOWFLAKE_TOKEN_TTL_SECONDS = 600 + +const hostSchema = z + .string() + .trim() + .min(1, 'Snowflake account host is required') + .max(255) + .transform((value, context) => { + try { + return new URL(normalizeSnowflakeHost(value)).hostname + } catch { + context.addIssue({ + code: 'custom', + message: 'Enter the account host, like myorg-myaccount.snowflakecomputing.com', + }) + return z.NEVER + } + }) + +const submittedConfigSchema = z + .object({ + authType: z.literal('workload_identity'), + host: hostSchema, + subject: z.string().min(1).max(512).optional(), + subjectProof: z.string().min(1).max(512).optional(), + }) + .strict() + +const storedSecretSchema = z + .object({ + type: z.literal(SNOWFLAKE_KEYLESS_SECRET_TYPE), + config: z + .object({ + authType: z.literal('workload_identity'), + host: z.string().min(1), + subject: z.string().min(1), + }) + .strict(), + metadata: z.record(z.string(), z.string()).optional(), + }) + .strict() + +type StoredConfig = z.infer['config'] + +/** A keyless Snowflake configuration Sim can name the fix for; surfaced to the user as-is. */ +export class SnowflakeCredentialError extends Error { + constructor(message: string) { + super(message) + this.name = 'SnowflakeCredentialError' + } +} + +/** The auth mode a decrypted Snowflake secret holds: keyless, a token, or undefined when neither. */ +export function storedSnowflakeAuthType( + decrypted: string +): 'workload_identity' | 'programmatic_access_token' | undefined { + try { + if (storedSecretSchema.safeParse(JSON.parse(decrypted)).success) return 'workload_identity' + parseTokenServiceAccountSecretBlob(decrypted, SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID) + return 'programmatic_access_token' + } catch { + return undefined + } +} + +function readStoredSubject(storedSecret: Record | null | undefined) { + const parsed = storedSecretSchema.safeParse(storedSecret) + return parsed.success ? parsed.data.config.subject : undefined +} + +function mintSnowflakeToken(subject: string, owner: WorkloadIdentityOwner): Promise { + return mintWorkloadIdentityToken({ + subject, + audience: SNOWFLAKE_WORKLOAD_IDENTITY_AUDIENCE, + owner, + ttlSeconds: SNOWFLAKE_TOKEN_TTL_SECONDS, + }) +} + +export interface SnowflakeKeylessSecret { + encryptedServiceAccountKey: string + displayName: string + auditMetadata: Record + principal: ServiceAccountPrincipal | null + /** A subject this save binds that the credential did not already hold. */ + newWorkloadIdentitySubject?: string +} + +/** + * Validates a keyless configuration, binds its subject to the person saving + * it, proves the account's service user trusts that subject by running the + * identity statement with a freshly minted token, and returns the encrypted + * blob. Snowflake's refusals keep the shared validation codes + * (`invalid_credentials`, `site_not_found`, `provider_unavailable`); only + * configuration Sim itself rejects throws {@link SnowflakeCredentialError}. + */ +export async function verifyAndEncryptSnowflakeKeylessCredential( + serialized: string, + owner: WorkloadIdentityCredentialOwner +): Promise { + const parsed = parseServiceAccountConfig( + serialized, + submittedConfigSchema, + 'Snowflake connection' + ) + if (!parsed.success) throw new SnowflakeCredentialError(parsed.message) + if (!(await isWorkloadIdentityConfigured())) { + throw new WorkloadIdentityNotConfiguredError('Use a programmatic access token.') + } + const { host, subject: submittedSubject, subjectProof } = parsed.data + const subject = bindWorkloadIdentitySubject({ + provider: 'snowflake', + owner, + storedSubject: readStoredSubject(owner.storedSecret), + subject: submittedSubject, + subjectProof, + }) + + const validation = await validateSnowflakeWorkloadIdentity({ + domain: host, + subjectToken: await mintSnowflakeToken(subject, owner), + }) + const config: StoredConfig = { + authType: 'workload_identity', + host: validation.normalizedDomain ?? host, + subject, + } + const principalMetadata = serviceAccountPrincipalMetadata(validation.principal) + const { encrypted } = await encryptSecret( + JSON.stringify({ + type: SNOWFLAKE_KEYLESS_SECRET_TYPE, + config, + metadata: { ...validation.storedMetadata, ...principalMetadata }, + }) + ) + return { + encryptedServiceAccountKey: encrypted, + displayName: validation.displayName, + auditMetadata: { ...validation.auditMetadata, ...principalMetadata }, + principal: validation.principal, + ...(subject !== readStoredSubject(owner.storedSecret) + ? { newWorkloadIdentitySubject: subject } + : {}), + } +} + +/** + * Resolves an executor-authorized Snowflake credential (the executor already + * checked the caller may use it) to the token and account host the tools send. + * A programmatic access token resolves exactly as every token-paste credential + * does; a keyless credential mints a token for its own subject and says so in + * `authStyle`, which is the only thing that switches the tools' token type. + */ +export async function resolveSnowflakeCredentialToken(credentialId: string): Promise<{ + accessToken: string + domain?: string + providerWorkspaceId?: string + authStyle?: typeof SNOWFLAKE_WORKLOAD_IDENTITY_AUTH_STYLE +}> { + const [row] = await db + .select({ + workspaceId: credential.workspaceId, + organizationId: credential.organizationId, + encryptedServiceAccountKey: credential.encryptedServiceAccountKey, + }) + .from(credential) + .where(eq(credential.id, credentialId)) + .limit(1) + if (!row?.encryptedServiceAccountKey) { + throw new Error('Token service account secret not found') + } + const { decrypted } = await decryptSecret(row.encryptedServiceAccountKey) + const keyless = storedSecretSchema.safeParse(JSON.parse(decrypted)) + if (!keyless.success) { + const secret = parseTokenServiceAccountSecretBlob( + decrypted, + SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID + ) + return { + accessToken: secret.apiToken, + domain: secret.domain, + providerWorkspaceId: secret.providerWorkspaceId, + } + } + const { host, subject } = keyless.data.config + const owner: WorkloadIdentityOwner = row.workspaceId + ? { workspaceId: row.workspaceId } + : { organizationId: row.organizationId ?? undefined } + return { + accessToken: await mintSnowflakeToken(subject, owner), + domain: host, + authStyle: SNOWFLAKE_WORKLOAD_IDENTITY_AUTH_STYLE, + } +} diff --git a/apps/sim/lib/credentials/token-service-accounts/descriptors.ts b/apps/sim/lib/credentials/token-service-accounts/descriptors.ts index e1f5c598022..ebd5a56b5d7 100644 --- a/apps/sim/lib/credentials/token-service-accounts/descriptors.ts +++ b/apps/sim/lib/credentials/token-service-accounts/descriptors.ts @@ -24,6 +24,12 @@ export interface TokenServiceAccountField { placeholder: string /** Rendered with SecretInput and never echoed back. */ secret: boolean + /** + * The value is a URL whose scheme and path matter (a self-hosted instance or + * deployment URL), so the connect form sends it as typed and the provider's + * check normalizes it. Other domains are reduced to a bare host first. + */ + url?: boolean /** Soft-format hint shown while the current value doesn't match `hintPattern`. */ hintPattern?: RegExp hintMessage?: string @@ -85,6 +91,104 @@ export const FLOW_SERVICE_ACCOUNT_PROVIDER_ID = 'flow-service-account' as const export const CODA_SERVICE_ACCOUNT_PROVIDER_ID = 'coda-service-account' as const export const GAUGE_SERVICE_ACCOUNT_PROVIDER_ID = 'gauge-service-account' as const export const HIGGSFIELD_SERVICE_ACCOUNT_PROVIDER_ID = 'higgsfield-service-account' as const +export const THRIVE_SERVICE_ACCOUNT_PROVIDER_ID = 'thrive-service-account' as const +export const SUPABASE_SERVICE_ACCOUNT_PROVIDER_ID = 'supabase-service-account' as const +export const INFISICAL_SERVICE_ACCOUNT_PROVIDER_ID = 'infisical-service-account' as const +export const OKTA_SERVICE_ACCOUNT_PROVIDER_ID = 'okta-service-account' as const +export const DEVIN_SERVICE_ACCOUNT_PROVIDER_ID = 'devin-service-account' as const +export const EMAILBISON_SERVICE_ACCOUNT_PROVIDER_ID = 'emailbison-service-account' as const +export const UPSTASH_SERVICE_ACCOUNT_PROVIDER_ID = 'upstash-service-account' as const +export const ALGOLIA_SERVICE_ACCOUNT_PROVIDER_ID = 'algolia-service-account' as const +export const TWILIO_SERVICE_ACCOUNT_PROVIDER_ID = 'twilio-service-account' as const +export const AZURE_DEVOPS_SERVICE_ACCOUNT_PROVIDER_ID = 'azure-devops-service-account' as const +export const PLANE_SERVICE_ACCOUNT_PROVIDER_ID = 'plane-service-account' as const +export const JOTFORM_SERVICE_ACCOUNT_PROVIDER_ID = 'jotform-service-account' as const +export const MAILGUN_SERVICE_ACCOUNT_PROVIDER_ID = 'mailgun-service-account' as const +export const LOGFIRE_SERVICE_ACCOUNT_PROVIDER_ID = 'logfire-service-account' as const +export const NEW_RELIC_SERVICE_ACCOUNT_PROVIDER_ID = 'new-relic-service-account' as const +export const DYNATRACE_SERVICE_ACCOUNT_PROVIDER_ID = 'dynatrace-service-account' as const +export const TINYBIRD_SERVICE_ACCOUNT_PROVIDER_ID = 'tinybird-service-account' as const +export const CONVEX_SERVICE_ACCOUNT_PROVIDER_ID = 'convex-service-account' as const +export const JUPYTER_SERVICE_ACCOUNT_PROVIDER_ID = 'jupyter-service-account' as const +export const GRAFANA_SERVICE_ACCOUNT_PROVIDER_ID = 'grafana-service-account' as const +export const GITLAB_SERVICE_ACCOUNT_PROVIDER_ID = 'gitlab-service-account' as const +export const GITHUB_SERVICE_ACCOUNT_PROVIDER_ID = 'github-service-account' as const +export const BITBUCKET_SERVICE_ACCOUNT_PROVIDER_ID = 'bitbucket-service-account' as const +export const WHATSAPP_SERVICE_ACCOUNT_PROVIDER_ID = 'whatsapp-service-account' as const +export const VERCEL_SERVICE_ACCOUNT_PROVIDER_ID = 'vercel-service-account' as const +export const TYPEFORM_SERVICE_ACCOUNT_PROVIDER_ID = 'typeform-service-account' as const +export const TELEGRAM_SERVICE_ACCOUNT_PROVIDER_ID = 'telegram-service-account' as const +export const STRIPE_SERVICE_ACCOUNT_PROVIDER_ID = 'stripe-service-account' as const +export const SENTRY_SERVICE_ACCOUNT_PROVIDER_ID = 'sentry-service-account' as const +export const ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID = 'rootly-service-account' as const +export const RESEND_SERVICE_ACCOUNT_PROVIDER_ID = 'resend-service-account' as const +export const PAGERDUTY_SERVICE_ACCOUNT_PROVIDER_ID = 'pagerduty-service-account' as const +export const LEMLIST_SERVICE_ACCOUNT_PROVIDER_ID = 'lemlist-service-account' as const +export const GRANOLA_SERVICE_ACCOUNT_PROVIDER_ID = 'granola-service-account' as const +export const GRAIN_SERVICE_ACCOUNT_PROVIDER_ID = 'grain-service-account' as const +export const FATHOM_SERVICE_ACCOUNT_PROVIDER_ID = 'fathom-service-account' as const +export const CHECKR_SERVICE_ACCOUNT_PROVIDER_ID = 'checkr-service-account' as const +export const CALENDLY_SERVICE_ACCOUNT_PROVIDER_ID = 'calendly-service-account' as const +export const ASHBY_SERVICE_ACCOUNT_PROVIDER_ID = 'ashby-service-account' as const +export const FLINT_SERVICE_ACCOUNT_PROVIDER_ID = 'flint-service-account' as const +export const REDUCTO_SERVICE_ACCOUNT_PROVIDER_ID = 'reducto-service-account' as const +export const QUIVER_SERVICE_ACCOUNT_PROVIDER_ID = 'quiver-service-account' as const +export const PULSE_SERVICE_ACCOUNT_PROVIDER_ID = 'pulse-service-account' as const +export const INTERCOM_SERVICE_ACCOUNT_PROVIDER_ID = 'intercom-service-account' as const +export const FIREFLIES_SERVICE_ACCOUNT_PROVIDER_ID = 'fireflies-service-account' as const +export const EXTEND_SERVICE_ACCOUNT_PROVIDER_ID = 'extend-service-account' as const +export const DUB_SERVICE_ACCOUNT_PROVIDER_ID = 'dub-service-account' as const +export const CURSOR_SERVICE_ACCOUNT_PROVIDER_ID = 'cursor-service-account' as const +export const DISCORD_SERVICE_ACCOUNT_PROVIDER_ID = 'discord-service-account' as const +export const ZEP_SERVICE_ACCOUNT_PROVIDER_ID = 'zep-service-account' as const +export const MEM0_SERVICE_ACCOUNT_PROVIDER_ID = 'mem0-service-account' as const +export const YOUTUBE_SERVICE_ACCOUNT_PROVIDER_ID = 'youtube-service-account' as const +export const YOUCOM_SERVICE_ACCOUNT_PROVIDER_ID = 'youcom-service-account' as const +export const UPTIMEROBOT_SERVICE_ACCOUNT_PROVIDER_ID = 'uptimerobot-service-account' as const +export const TRIGGER_DEV_SERVICE_ACCOUNT_PROVIDER_ID = 'trigger-dev-service-account' as const +export const TAILSCALE_SERVICE_ACCOUNT_PROVIDER_ID = 'tailscale-service-account' as const +export const SQUARE_SERVICE_ACCOUNT_PROVIDER_ID = 'square-service-account' as const +export const SPORTMONKS_SERVICE_ACCOUNT_PROVIDER_ID = 'sportmonks-service-account' as const +export const SMARTLEAD_SERVICE_ACCOUNT_PROVIDER_ID = 'smartlead-service-account' as const +export const SIXTYFOUR_SERVICE_ACCOUNT_PROVIDER_ID = 'sixtyfour-service-account' as const +export const SIMILARWEB_SERVICE_ACCOUNT_PROVIDER_ID = 'similarweb-service-account' as const +export const SENDGRID_SERVICE_ACCOUNT_PROVIDER_ID = 'sendgrid-service-account' as const +export const ROCKETLANE_SERVICE_ACCOUNT_PROVIDER_ID = 'rocketlane-service-account' as const +export const RIPPLING_SERVICE_ACCOUNT_PROVIDER_ID = 'rippling-service-account' as const +export const RB2B_SERVICE_ACCOUNT_PROVIDER_ID = 'rb2b-service-account' as const +export const QUARTR_SERVICE_ACCOUNT_PROVIDER_ID = 'quartr-service-account' as const +export const PROFOUND_SERVICE_ACCOUNT_PROVIDER_ID = 'profound-service-account' as const +export const PERSONA_SERVICE_ACCOUNT_PROVIDER_ID = 'persona-service-account' as const +export const OTTER_SERVICE_ACCOUNT_PROVIDER_ID = 'otter-service-account' as const +export const MAILCHIMP_SERVICE_ACCOUNT_PROVIDER_ID = 'mailchimp-service-account' as const +export const LUMA_SERVICE_ACCOUNT_PROVIDER_ID = 'luma-service-account' as const +export const LOOPS_SERVICE_ACCOUNT_PROVIDER_ID = 'loops-service-account' as const +export const LINQ_SERVICE_ACCOUNT_PROVIDER_ID = 'linq-service-account' as const +export const LAUNCHDARKLY_SERVICE_ACCOUNT_PROVIDER_ID = 'launchdarkly-service-account' as const +export const LANGSMITH_SERVICE_ACCOUNT_PROVIDER_ID = 'langsmith-service-account' as const +export const INSTANTLY_SERVICE_ACCOUNT_PROVIDER_ID = 'instantly-service-account' as const +export const INCIDENTIO_SERVICE_ACCOUNT_PROVIDER_ID = 'incidentio-service-account' as const +export const HUGGINGFACE_SERVICE_ACCOUNT_PROVIDER_ID = 'huggingface-service-account' as const +export const HEX_SERVICE_ACCOUNT_PROVIDER_ID = 'hex-service-account' as const +export const GAMMA_SERVICE_ACCOUNT_PROVIDER_ID = 'gamma-service-account' as const +export const ENRICH_SERVICE_ACCOUNT_PROVIDER_ID = 'enrich-service-account' as const +export const ELEVENLABS_SERVICE_ACCOUNT_PROVIDER_ID = 'elevenlabs-service-account' as const +export const DAYTONA_SERVICE_ACCOUNT_PROVIDER_ID = 'daytona-service-account' as const +export const CRUNCHBASE_SERVICE_ACCOUNT_PROVIDER_ID = 'crunchbase-service-account' as const +export const CLOUDFLARE_SERVICE_ACCOUNT_PROVIDER_ID = 'cloudflare-service-account' as const +export const CLERK_SERVICE_ACCOUNT_PROVIDER_ID = 'clerk-service-account' as const +export const CIRCLEBACK_SERVICE_ACCOUNT_PROVIDER_ID = 'circleback-service-account' as const +export const BUFFER_SERVICE_ACCOUNT_PROVIDER_ID = 'buffer-service-account' as const +export const BROWSER_USE_SERVICE_ACCOUNT_PROVIDER_ID = 'browser-use-service-account' as const +export const BRIGHTDATA_SERVICE_ACCOUNT_PROVIDER_ID = 'brightdata-service-account' as const +export const BREX_SERVICE_ACCOUNT_PROVIDER_ID = 'brex-service-account' as const +export const APOLLO_SERVICE_ACCOUNT_PROVIDER_ID = 'apollo-service-account' as const +export const APIFY_SERVICE_ACCOUNT_PROVIDER_ID = 'apify-service-account' as const +export const AHREFS_SERVICE_ACCOUNT_PROVIDER_ID = 'ahrefs-service-account' as const +export const AGENTPHONE_SERVICE_ACCOUNT_PROVIDER_ID = 'agentphone-service-account' as const +export const AGENTMAIL_SERVICE_ACCOUNT_PROVIDER_ID = 'agentmail-service-account' as const +export const AFFINITY_SERVICE_ACCOUNT_PROVIDER_ID = 'affinity-service-account' as const +export const TAVILY_SERVICE_ACCOUNT_PROVIDER_ID = 'tavily-service-account' as const const SHOPIFY_DOMAIN_HINT_REGEX = /^[a-z0-9][a-z0-9-]*\.myshopify\.com$/i @@ -116,6 +220,104 @@ export type TokenServiceAccountProviderId = | typeof CODA_SERVICE_ACCOUNT_PROVIDER_ID | typeof GAUGE_SERVICE_ACCOUNT_PROVIDER_ID | typeof HIGGSFIELD_SERVICE_ACCOUNT_PROVIDER_ID + | typeof THRIVE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof SUPABASE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof INFISICAL_SERVICE_ACCOUNT_PROVIDER_ID + | typeof OKTA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof DEVIN_SERVICE_ACCOUNT_PROVIDER_ID + | typeof EMAILBISON_SERVICE_ACCOUNT_PROVIDER_ID + | typeof UPSTASH_SERVICE_ACCOUNT_PROVIDER_ID + | typeof ALGOLIA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof TWILIO_SERVICE_ACCOUNT_PROVIDER_ID + | typeof AZURE_DEVOPS_SERVICE_ACCOUNT_PROVIDER_ID + | typeof PLANE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof JOTFORM_SERVICE_ACCOUNT_PROVIDER_ID + | typeof MAILGUN_SERVICE_ACCOUNT_PROVIDER_ID + | typeof LOGFIRE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof NEW_RELIC_SERVICE_ACCOUNT_PROVIDER_ID + | typeof DYNATRACE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof TINYBIRD_SERVICE_ACCOUNT_PROVIDER_ID + | typeof CONVEX_SERVICE_ACCOUNT_PROVIDER_ID + | typeof JUPYTER_SERVICE_ACCOUNT_PROVIDER_ID + | typeof GRAFANA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof GITLAB_SERVICE_ACCOUNT_PROVIDER_ID + | typeof GITHUB_SERVICE_ACCOUNT_PROVIDER_ID + | typeof BITBUCKET_SERVICE_ACCOUNT_PROVIDER_ID + | typeof WHATSAPP_SERVICE_ACCOUNT_PROVIDER_ID + | typeof VERCEL_SERVICE_ACCOUNT_PROVIDER_ID + | typeof TYPEFORM_SERVICE_ACCOUNT_PROVIDER_ID + | typeof TELEGRAM_SERVICE_ACCOUNT_PROVIDER_ID + | typeof STRIPE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof SENTRY_SERVICE_ACCOUNT_PROVIDER_ID + | typeof ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID + | typeof RESEND_SERVICE_ACCOUNT_PROVIDER_ID + | typeof PAGERDUTY_SERVICE_ACCOUNT_PROVIDER_ID + | typeof LEMLIST_SERVICE_ACCOUNT_PROVIDER_ID + | typeof GRANOLA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof GRAIN_SERVICE_ACCOUNT_PROVIDER_ID + | typeof FATHOM_SERVICE_ACCOUNT_PROVIDER_ID + | typeof CHECKR_SERVICE_ACCOUNT_PROVIDER_ID + | typeof CALENDLY_SERVICE_ACCOUNT_PROVIDER_ID + | typeof ASHBY_SERVICE_ACCOUNT_PROVIDER_ID + | typeof FLINT_SERVICE_ACCOUNT_PROVIDER_ID + | typeof REDUCTO_SERVICE_ACCOUNT_PROVIDER_ID + | typeof QUIVER_SERVICE_ACCOUNT_PROVIDER_ID + | typeof PULSE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof INTERCOM_SERVICE_ACCOUNT_PROVIDER_ID + | typeof FIREFLIES_SERVICE_ACCOUNT_PROVIDER_ID + | typeof EXTEND_SERVICE_ACCOUNT_PROVIDER_ID + | typeof DUB_SERVICE_ACCOUNT_PROVIDER_ID + | typeof CURSOR_SERVICE_ACCOUNT_PROVIDER_ID + | typeof DISCORD_SERVICE_ACCOUNT_PROVIDER_ID + | typeof ZEP_SERVICE_ACCOUNT_PROVIDER_ID + | typeof MEM0_SERVICE_ACCOUNT_PROVIDER_ID + | typeof YOUTUBE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof YOUCOM_SERVICE_ACCOUNT_PROVIDER_ID + | typeof UPTIMEROBOT_SERVICE_ACCOUNT_PROVIDER_ID + | typeof TRIGGER_DEV_SERVICE_ACCOUNT_PROVIDER_ID + | typeof TAILSCALE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof SQUARE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof SPORTMONKS_SERVICE_ACCOUNT_PROVIDER_ID + | typeof SMARTLEAD_SERVICE_ACCOUNT_PROVIDER_ID + | typeof SIXTYFOUR_SERVICE_ACCOUNT_PROVIDER_ID + | typeof SIMILARWEB_SERVICE_ACCOUNT_PROVIDER_ID + | typeof SENDGRID_SERVICE_ACCOUNT_PROVIDER_ID + | typeof ROCKETLANE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof RIPPLING_SERVICE_ACCOUNT_PROVIDER_ID + | typeof RB2B_SERVICE_ACCOUNT_PROVIDER_ID + | typeof QUARTR_SERVICE_ACCOUNT_PROVIDER_ID + | typeof PROFOUND_SERVICE_ACCOUNT_PROVIDER_ID + | typeof PERSONA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof OTTER_SERVICE_ACCOUNT_PROVIDER_ID + | typeof MAILCHIMP_SERVICE_ACCOUNT_PROVIDER_ID + | typeof LUMA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof LOOPS_SERVICE_ACCOUNT_PROVIDER_ID + | typeof LINQ_SERVICE_ACCOUNT_PROVIDER_ID + | typeof LAUNCHDARKLY_SERVICE_ACCOUNT_PROVIDER_ID + | typeof LANGSMITH_SERVICE_ACCOUNT_PROVIDER_ID + | typeof INSTANTLY_SERVICE_ACCOUNT_PROVIDER_ID + | typeof INCIDENTIO_SERVICE_ACCOUNT_PROVIDER_ID + | typeof HUGGINGFACE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof HEX_SERVICE_ACCOUNT_PROVIDER_ID + | typeof GAMMA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof ENRICH_SERVICE_ACCOUNT_PROVIDER_ID + | typeof ELEVENLABS_SERVICE_ACCOUNT_PROVIDER_ID + | typeof DAYTONA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof CRUNCHBASE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof CLOUDFLARE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof CLERK_SERVICE_ACCOUNT_PROVIDER_ID + | typeof CIRCLEBACK_SERVICE_ACCOUNT_PROVIDER_ID + | typeof BUFFER_SERVICE_ACCOUNT_PROVIDER_ID + | typeof BROWSER_USE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof BRIGHTDATA_SERVICE_ACCOUNT_PROVIDER_ID + | typeof BREX_SERVICE_ACCOUNT_PROVIDER_ID + | typeof APOLLO_SERVICE_ACCOUNT_PROVIDER_ID + | typeof APIFY_SERVICE_ACCOUNT_PROVIDER_ID + | typeof AHREFS_SERVICE_ACCOUNT_PROVIDER_ID + | typeof AGENTPHONE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof AGENTMAIL_SERVICE_ACCOUNT_PROVIDER_ID + | typeof AFFINITY_SERVICE_ACCOUNT_PROVIDER_ID + | typeof TAVILY_SERVICE_ACCOUNT_PROVIDER_ID | typeof FLOW_SERVICE_ACCOUNT_PROVIDER_ID export const TOKEN_SERVICE_ACCOUNT_DESCRIPTORS: Record< @@ -523,6 +725,1621 @@ export const TOKEN_SERVICE_ACCOUNT_DESCRIPTORS: Record< invalidCredentialsHelp: 'Snowflake rejected this token. Check that it belongs to a user on this exact account host, that it has not expired or been revoked, that a network policy allows Sim to reach the account, and that the SQL API is enabled for the account.', }, + [TAVILY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: TAVILY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Tavily', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'tvly-...', + secret: true, + hintPattern: /^tvly-/, + hintMessage: 'Tavily API keys start with tvly-.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/tavily', + }, + [AFFINITY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: AFFINITY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Affinity', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Affinity API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/affinity', + }, + [AGENTMAIL_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: AGENTMAIL_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'AgentMail', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your AgentMail API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/agentmail', + }, + [AGENTPHONE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: AGENTPHONE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'AgentPhone', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your AgentPhone API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/agentphone', + }, + [AHREFS_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: AHREFS_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Ahrefs', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Ahrefs API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/ahrefs', + helpText: 'The key needs access to the Ahrefs API v3 endpoints you plan to use.', + }, + [APIFY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: APIFY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Apify', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { + id: 'apiToken', + label: 'API token', + placeholder: 'Paste your Apify API token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/apify', + helpText: 'Find your API token in Apify Console under Settings, then API & Integrations.', + }, + [APOLLO_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: APOLLO_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Apollo', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Apollo API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/apollo', + helpText: 'Account, bulk, deal creation, and email account operations need a master API key.', + }, + [BREX_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: BREX_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Brex', + tokenNoun: 'user token', + connectNoun: 'user token', + fields: [ + { + id: 'apiToken', + label: 'User token', + placeholder: 'Paste your Brex user token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/brex', + helpText: 'The user token needs the Brex scopes for the data your workflows read or write.', + }, + [BRIGHTDATA_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: BRIGHTDATA_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Bright Data', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { + id: 'apiToken', + label: 'API token', + placeholder: 'Paste your Bright Data API token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/brightdata', + }, + [BROWSER_USE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: BROWSER_USE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Browser Use', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [{ id: 'apiToken', label: 'API key', placeholder: 'bu_...', secret: true }], + docsUrl: 'https://docs.sim.ai/integrations/browser_use', + }, + [BUFFER_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: BUFFER_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Buffer', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Buffer API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/buffer', + }, + [CIRCLEBACK_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: CIRCLEBACK_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Circleback', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'cb_...', + secret: true, + hintPattern: /^cb_/, + hintMessage: 'Circleback API keys start with cb_.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/circleback', + helpText: 'Create an API key in Circleback under Settings, then API keys.', + }, + [CLERK_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: CLERK_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Clerk', + tokenNoun: 'secret key', + connectNoun: 'secret key', + fields: [ + { + id: 'apiToken', + label: 'Secret key', + placeholder: 'sk_live_...', + secret: true, + hintPattern: /^sk_(live|test)_/, + hintMessage: 'Clerk secret keys start with sk_live_ or sk_test_.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/clerk', + helpText: + 'Copy the secret key from API keys in the Clerk Dashboard for the instance you want to manage.', + }, + [CLOUDFLARE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: CLOUDFLARE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Cloudflare', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { + id: 'apiToken', + label: 'API token', + placeholder: 'Paste your Cloudflare API token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/cloudflare', + helpText: + 'Use a scoped API token, not the Global API Key. Grant it the permissions for the zones and accounts your workflows manage.', + }, + [CRUNCHBASE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: CRUNCHBASE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Crunchbase', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Crunchbase API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/crunchbase', + }, + [DAYTONA_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: DAYTONA_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Daytona', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Daytona API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/daytona', + }, + [ELEVENLABS_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: ELEVENLABS_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'ElevenLabs', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your ElevenLabs API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/elevenlabs', + helpText: + 'Restricted keys are accepted; give the key access to each endpoint your workflows call.', + invalidCredentialsHelp: + 'ElevenLabs rejected this key. Check that it was copied in full and has not been deleted.', + }, + [ENRICH_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: ENRICH_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Enrich', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Enrich API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/enrich', + }, + [GAMMA_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: GAMMA_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Gamma', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'sk-gamma-...', + secret: true, + hintPattern: /^sk-gamma-/, + hintMessage: 'Gamma API keys start with sk-gamma-.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/gamma', + helpText: 'Create a key under Settings > API Keys in Gamma.', + }, + [HEX_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: HEX_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Hex', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { id: 'apiToken', label: 'API token', placeholder: 'Paste your Hex API token', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/hex', + helpText: 'Sim checks the token by listing projects, so it needs Read projects access.', + }, + [HUGGINGFACE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: HUGGINGFACE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Hugging Face', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { + id: 'apiToken', + label: 'Access token', + placeholder: 'Paste your Hugging Face access token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/huggingface', + }, + [INCIDENTIO_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: INCIDENTIO_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'incident.io', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your incident.io API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/incidentio', + helpText: 'Create a key in incident.io under Settings, API keys.', + }, + [INSTANTLY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: INSTANTLY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Instantly', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Instantly API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/instantly', + helpText: 'Scoped keys are accepted; give the key the scopes your workflows call.', + }, + [LANGSMITH_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: LANGSMITH_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'LangSmith', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your LangSmith API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/langsmith', + }, + [LAUNCHDARKLY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: LAUNCHDARKLY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'LaunchDarkly', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { + id: 'apiToken', + label: 'Access token', + placeholder: 'Paste your LaunchDarkly access token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/launchdarkly', + }, + [LINQ_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: LINQ_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Linq', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Linq API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/linq', + }, + [LOOPS_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: LOOPS_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Loops', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Loops API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/loops', + }, + [LUMA_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: LUMA_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Luma', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Luma API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/luma', + }, + [MAILCHIMP_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: MAILCHIMP_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Mailchimp', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'abc123...-us21', + secret: true, + hintPattern: /-[a-z]+[0-9]+$/, + hintMessage: 'Mailchimp API keys end with your data center, like -us21.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/mailchimp', + helpText: 'Use the full key, including the data center suffix after the dash.', + }, + [OTTER_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: OTTER_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Otter.ai', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Otter.ai API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/otter', + helpText: 'The Otter API is available on Enterprise workspaces only.', + }, + [PERSONA_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: PERSONA_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Persona', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Persona API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/persona', + helpText: 'Grant the key the permissions your workflows need in Persona.', + }, + [PROFOUND_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: PROFOUND_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Profound', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Profound API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/profound', + }, + [QUARTR_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: QUARTR_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Quartr', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Quartr API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/quartr', + }, + [RB2B_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: RB2B_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'RB2B', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your RB2B API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/rb2b', + }, + [RIPPLING_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: RIPPLING_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Rippling', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { + id: 'apiToken', + label: 'API token', + placeholder: 'Paste your Rippling API token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/rippling', + helpText: 'Grant the token the scopes your workflows need.', + }, + [ROCKETLANE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: ROCKETLANE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Rocketlane', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Rocketlane API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/rocketlane', + }, + [SENDGRID_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: SENDGRID_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'SendGrid', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your SendGrid API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/sendgrid', + helpText: + 'Create a key in SendGrid under Settings > API Keys. A restricted key only works for the actions it is scoped to.', + }, + [SIMILARWEB_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: SIMILARWEB_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Similarweb', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Similarweb API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/similarweb', + }, + [SIXTYFOUR_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: SIXTYFOUR_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Sixtyfour', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Sixtyfour API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/sixtyfour', + }, + [SMARTLEAD_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: SMARTLEAD_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Smartlead', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Smartlead API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/smartlead', + }, + [SPORTMONKS_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: SPORTMONKS_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Sportmonks', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { + id: 'apiToken', + label: 'API token', + placeholder: 'Paste your Sportmonks API token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/sportmonks', + }, + [SQUARE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: SQUARE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Square', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { + id: 'apiToken', + label: 'Access token', + placeholder: 'Paste your Square production access token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/square', + helpText: + 'Use a production access token from your app in the Square Developer Console; sandbox tokens are not supported.', + }, + [TAILSCALE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: TAILSCALE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Tailscale', + tokenNoun: 'API access token', + connectNoun: 'access token', + fields: [ + { + id: 'apiToken', + label: 'API access token', + placeholder: 'Paste your Tailscale API access token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/tailscale', + helpText: + "Generate an API access token in the Tailscale admin console under Settings > Keys. OAuth access tokens expire after an hour, so don't save one here.", + }, + [TRIGGER_DEV_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: TRIGGER_DEV_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Trigger.dev', + tokenNoun: 'secret API key', + connectNoun: 'secret key', + fields: [ + { + id: 'apiToken', + label: 'Secret API key', + placeholder: 'tr_prod_...', + secret: true, + hintPattern: /^tr_(dev|stg|prod|preview)_/, + hintMessage: + 'Trigger.dev secret keys start with tr_dev_, tr_stg_, tr_prod_, or tr_preview_.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/trigger_dev', + helpText: + 'Each secret key belongs to one project environment; copy it from the API Keys page of that environment.', + }, + [UPTIMEROBOT_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: UPTIMEROBOT_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'UptimeRobot', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your UptimeRobot API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/uptimerobot', + helpText: + "Use the main API key from Integrations > API in UptimeRobot; read-only keys can't make changes.", + }, + [YOUCOM_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: YOUCOM_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'You.com', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your You.com API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/youcom', + }, + [YOUTUBE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: YOUTUBE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'YouTube', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your YouTube Data API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/youtube', + helpText: 'Create an API key in Google Cloud Console with the YouTube Data API v3 enabled.', + }, + [MEM0_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: MEM0_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Mem0', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Mem0 API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/mem0', + }, + [ZEP_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: ZEP_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Zep', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Zep project API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/zep', + }, + [DISCORD_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: DISCORD_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Discord', + tokenNoun: 'bot token', + connectNoun: 'bot token', + fields: [ + { + id: 'apiToken', + label: 'Bot token', + placeholder: 'Paste your Discord bot token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/discord', + helpText: + 'Create a bot in the Discord Developer Portal and copy its token. The bot must be added to the servers your workflows use.', + }, + [CURSOR_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: CURSOR_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Cursor', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Cursor API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/cursor', + helpText: 'Create an API key in the Cursor dashboard under API Keys.', + }, + [DUB_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: DUB_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Dub', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'dub_...', + secret: true, + hintPattern: /^dub_/, + hintMessage: 'Dub API keys start with dub_.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/dub', + }, + [EXTEND_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: EXTEND_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Extend', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Extend API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/extend', + }, + [FIREFLIES_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: FIREFLIES_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Fireflies', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your Fireflies API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/fireflies', + }, + [INTERCOM_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: INTERCOM_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Intercom', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { + id: 'apiToken', + label: 'Access token', + placeholder: 'Paste your Intercom access token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/intercom', + helpText: "Create it in the Intercom Developer Hub, on your app's Authentication page.", + }, + [PULSE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: PULSE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Pulse', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Pulse API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/pulse', + helpText: 'Get your key from the Pulse Platform at platform.runpulse.com.', + }, + [QUIVER_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: QUIVER_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Quiver', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'Paste your QuiverAI API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/quiver', + }, + [REDUCTO_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: REDUCTO_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Reducto', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Reducto API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/reducto', + }, + [FLINT_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: FLINT_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Flint', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [{ id: 'apiToken', label: 'API key', placeholder: 'ak_...', secret: true }], + docsUrl: 'https://docs.sim.ai/integrations/flint', + helpText: 'Create a key in your Flint team settings. It needs at least the member role.', + }, + [ASHBY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: ASHBY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Ashby', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Ashby API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/ashby', + helpText: + 'Create an API key in Ashby under Admin > API Keys and grant read and write access to the modules your workflows use.', + invalidCredentialsHelp: + 'Ashby rejected this key. Check that it was copied in full and has not been deactivated.', + }, + [CALENDLY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: CALENDLY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Calendly', + tokenNoun: 'personal access token', + connectNoun: 'personal access token', + fields: [ + { + id: 'apiToken', + label: 'Personal access token', + placeholder: 'Paste your Calendly personal access token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/calendly', + helpText: + 'Create a personal access token in Calendly under Settings > Integrations > API & Webhooks.', + }, + [CHECKR_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: CHECKR_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Checkr', + tokenNoun: 'secret API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'Secret API key', + placeholder: 'Paste your Checkr secret API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/checkr', + helpText: + 'Use a secret API key from your Checkr production account (Account Settings > Developer Settings). Staging keys do not work, since Sim calls api.checkr.com.', + invalidCredentialsHelp: + 'Checkr rejected this key. Check that it is a production secret key, not a publishable or staging key.', + }, + [FATHOM_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: FATHOM_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Fathom', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Fathom API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/fathom', + helpText: + 'Create an API key in Fathom under User Settings > API Access. The key sees the meetings you recorded or that were shared with you or your team.', + }, + [GRAIN_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: GRAIN_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Grain', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { + id: 'apiToken', + label: 'Access token', + placeholder: 'Paste your Grain access token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/grain', + helpText: + 'Create a personal or workspace access token in Grain under Settings > Integrations > API. A personal token has the same access you have in Grain.', + }, + [GRANOLA_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: GRANOLA_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Granola', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Granola API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/granola', + helpText: + 'Create an API key in the Granola desktop app under Settings > Connectors > API keys (Business or Enterprise plan). Workspace admins can create a workspace API key, which does not expire and reads public notes and spaces with API access enabled.', + }, + [LEMLIST_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: LEMLIST_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Lemlist', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Lemlist API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/lemlist', + helpText: 'Find your API key in Lemlist under Settings > Integrations.', + invalidCredentialsHelp: + 'Lemlist rejected the key. A key whose user is blocked also fails; generate a new key under Settings > Integrations.', + }, + [PAGERDUTY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: PAGERDUTY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'PagerDuty', + tokenNoun: 'REST API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'REST API key', + placeholder: 'Paste your PagerDuty REST API key', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/pagerduty', + helpText: + 'Use an account REST API key (an administrator creates it under Integrations > API Access Keys) or a personal user API token (User Settings > API Access). Creating, updating, snoozing, merging, and noting incidents needs a full-access key; a read-only key covers the list and get operations.', + }, + [RESEND_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: RESEND_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Resend', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 're_...', + secret: true, + hintPattern: /^re_/, + hintMessage: 'Resend API keys start with re_.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/resend', + helpText: + 'A sending-access key can only send emails. Use a full-access key to manage contacts, audiences, broadcasts, and domains.', + invalidCredentialsHelp: + 'Resend rejected this key. Check that it was copied in full and has not been deleted, deactivated, or suspended.', + }, + [ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Rootly', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Rootly API key', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/rootly', + helpText: + 'Use a Global or Team API key. A Personal key stops working if the person who created it leaves the organization.', + invalidCredentialsHelp: + 'Rootly rejected this key. Check that it was copied in full and has not been revoked.', + }, + [SENTRY_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: SENTRY_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Sentry', + tokenNoun: 'auth token', + connectNoun: 'auth token', + fields: [ + { + id: 'apiToken', + label: 'Auth token', + placeholder: 'Paste your Sentry auth token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/sentry', + helpText: + 'Use a user auth token or an internal integration token with the scopes your operations need, such as org:read, project:read, project:write, and event:read. The organization slug stays a field on the block.', + invalidCredentialsHelp: + 'Sentry rejected this token. Check that it was copied in full, has not been revoked, and belongs to sentry.io rather than a self-hosted Sentry.', + }, + [STRIPE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: STRIPE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Stripe', + tokenNoun: 'secret key', + connectNoun: 'secret key', + fields: [ + { + id: 'apiToken', + label: 'Secret key', + placeholder: 'sk_live_...', + secret: true, + hintPattern: /^(sk|rk)_(live|test)_/, + hintMessage: + 'Stripe secret keys start with sk_live_ or sk_test_ (restricted keys with rk_).', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/stripe', + helpText: + 'Create a secret or restricted key under Developers > API keys in the Stripe Dashboard. A restricted key needs read or write access to the resources your workflows use.', + }, + [TELEGRAM_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: TELEGRAM_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Telegram', + tokenNoun: 'bot token', + connectNoun: 'bot token', + fields: [ + { + id: 'apiToken', + label: 'Bot token', + placeholder: '123456789:ABC...', + secret: true, + hintPattern: /^\d+:/, + hintMessage: 'Telegram bot tokens look like 123456789:ABC-DEF...', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/telegram', + helpText: + 'Message /newbot to @BotFather in Telegram to create a bot and copy the token it gives you.', + }, + [TYPEFORM_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: TYPEFORM_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Typeform', + tokenNoun: 'personal access token', + connectNoun: 'personal access token', + fields: [ + { + id: 'apiToken', + label: 'Personal access token', + placeholder: 'Paste personal access token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/typeform', + helpText: + 'Create a personal access token in Typeform under Account > Personal tokens. Grant the forms and responses scopes your workflows use.', + invalidCredentialsHelp: + 'Typeform rejected this token. Check that it has not been revoked and that it includes the forms:read scope.', + }, + [VERCEL_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: VERCEL_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Vercel', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { id: 'apiToken', label: 'Access token', placeholder: 'Paste access token', secret: true }, + ], + docsUrl: 'https://docs.sim.ai/integrations/vercel', + helpText: + 'Create a token under Account Settings > Tokens in the Vercel dashboard, scoped to the team your workflows manage.', + }, + [WHATSAPP_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: WHATSAPP_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'WhatsApp', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { + id: 'apiToken', + label: 'Access token', + placeholder: 'Paste WhatsApp Cloud API access token', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/whatsapp', + helpText: + 'Use a permanent system user access token from Meta Business Settings with the whatsapp_business_messaging permission. Temporary tokens from the App Dashboard expire after 24 hours.', + invalidCredentialsHelp: + 'Meta rejected this token. Check that it has not expired or been revoked; temporary App Dashboard tokens last only 24 hours.', + }, + [GITLAB_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: GITLAB_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'GitLab', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { id: 'apiToken', label: 'Access token', placeholder: 'glpat-...', secret: true }, + { id: 'domain', label: 'GitLab host', placeholder: 'gitlab.com', secret: false }, + ], + docsUrl: 'https://docs.sim.ai/integrations/gitlab', + helpText: + 'Use a personal, project, or group access token with the api scope (read_api for read-only workflows). Enter gitlab.com, or your self-managed host.', + invalidCredentialsHelp: + 'GitLab rejected this token. Check that it is active, has the api or read_api scope, and belongs to the host you entered.', + }, + [GITHUB_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: GITHUB_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'GitHub', + tokenNoun: 'personal access token', + connectNoun: 'personal access token', + fields: [ + { + id: 'apiToken', + label: 'Personal access token', + placeholder: 'github_pat_... or ghp_...', + secret: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/github', + helpText: + 'Use a fine-grained personal access token limited to the repositories the workflow needs, or a classic token with the repo scope. GitHub Enterprise Server hosts are not supported.', + invalidCredentialsHelp: + 'GitHub rejected this token. Check that it has not expired or been revoked, and that it is a personal access token for github.com rather than a GitHub App or Enterprise Server token.', + }, + [BITBUCKET_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: BITBUCKET_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Bitbucket', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { + id: 'apiToken', + label: 'Access token', + placeholder: 'Paste a workspace, project, or repository access token', + secret: true, + }, + { + id: 'providerWorkspaceId', + label: 'Repository', + placeholder: 'my-workspace/my-repository', + secret: false, + hintPattern: /^[A-Za-z0-9][A-Za-z0-9_-]*\/[A-Za-z0-9_-][A-Za-z0-9._-]*$/, + hintMessage: + 'Enter a repository the token can read as workspace/repository, as in bitbucket.org/my-workspace/my-repository.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/bitbucket-service-account', + helpText: + 'Use a Bitbucket Cloud workspace, project, or repository access token with at least the repository scope. Enter any repository the token can read, as workspace/repository; Sim reads it once to verify the token.', + invalidCredentialsHelp: + 'Bitbucket rejected this token. Check that it has not expired or been revoked, has the repository scope, and can read the repository you entered.', + }, + [GRAFANA_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: GRAFANA_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Grafana', + tokenNoun: 'service account token', + connectNoun: 'service account token', + fields: [ + { id: 'apiToken', label: 'Service account token', placeholder: 'glsa_...', secret: true }, + { + id: 'domain', + label: 'Grafana URL', + placeholder: 'https://your-stack.grafana.net', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/grafana', + helpText: + 'Create a service account and token in Grafana under Administration > Users and access > Service accounts. Give the service account a role that covers what your workflows change (Editor for dashboards and annotations, Admin for alerting and folders). Enter your Grafana Cloud stack URL or your self-managed instance URL.', + invalidCredentialsHelp: + 'Grafana rejected this token. Check that it has not expired or been revoked, and that it belongs to the instance you entered.', + }, + [JUPYTER_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: JUPYTER_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Jupyter', + tokenNoun: 'server token', + connectNoun: 'server token', + fields: [ + { + id: 'apiToken', + label: 'Token', + placeholder: 'Paste your Jupyter server token', + secret: true, + }, + { + id: 'domain', + label: 'Server URL', + placeholder: 'https://jupyter.example.com', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/jupyter', + helpText: + "Use the token your Jupyter server prints at startup (or the API token from JupyterHub under Token). Enter the server's base URL, including any path prefix such as /user/ on JupyterHub. The server must be reachable from Sim.", + invalidCredentialsHelp: + 'The Jupyter server rejected this token. Check that it matches the server you entered and that the server is still running.', + }, + [CONVEX_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: CONVEX_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Convex', + tokenNoun: 'deploy key', + connectNoun: 'deploy key', + fields: [ + { + id: 'apiToken', + label: 'Deploy key', + placeholder: 'prod:your-deployment|...', + secret: true, + }, + { + id: 'domain', + label: 'Deployment URL', + placeholder: 'https://your-deployment.convex.cloud', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/convex', + helpText: + 'Generate a deploy key for the deployment in the Convex dashboard under Settings > General, and copy the deployment URL from the same page. Listing tables and exporting documents need a key with permission to read data.', + invalidCredentialsHelp: + 'Convex rejected this key. Check that it was generated for the deployment URL you entered and has not been revoked.', + }, + [TINYBIRD_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: TINYBIRD_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Tinybird', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { id: 'apiToken', label: 'API token', placeholder: 'p.eyJ...', secret: true }, + { + id: 'domain', + label: 'API base URL', + placeholder: 'https://api.tinybird.co', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/tinybird', + helpText: + "Copy a token from Tokens in your Tinybird workspace. Enter the API base URL for your workspace's region (for example https://api.us-east.aws.tinybird.co), or your self-managed URL. Give the token the scopes your workflows need, such as DATASOURCES:APPEND or PIPES:READ.", + invalidCredentialsHelp: + 'Tinybird rejected this token. Check that it was copied in full and that the base URL is the region your workspace lives in; a token only works in its own region.', + }, + [DYNATRACE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: DYNATRACE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Dynatrace', + tokenNoun: 'access token', + connectNoun: 'access token', + fields: [ + { id: 'apiToken', label: 'Access token', placeholder: 'dt0c01...', secret: true }, + { + id: 'domain', + label: 'Environment URL', + placeholder: 'https://abc12345.live.dynatrace.com', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/dynatrace', + helpText: + 'Generate an access token in Dynatrace under Access tokens with the scopes your workflows use (for example problems.read, metrics.read, logs.read). Enter your environment URL, such as https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/ for Managed.', + invalidCredentialsHelp: + 'Dynatrace rejected this token. Check that it is enabled, has not expired, and was created in the environment at this URL.', + }, + [NEW_RELIC_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: NEW_RELIC_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'New Relic', + tokenNoun: 'User API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'User API key', placeholder: 'NRAK-...', secret: true }, + { id: 'domain', label: 'Region', placeholder: 'us or eu', secret: false }, + ], + docsUrl: 'https://docs.sim.ai/integrations/new_relic', + helpText: + 'Create a User API key (NRAK-...) in New Relic under API keys. Enter us, or eu if your account is in the EU data center.', + invalidCredentialsHelp: + "New Relic rejected this key. Check that it is a User API key (NRAK-...), not a license or browser key, and that the region matches your account's data center.", + }, + [LOGFIRE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: LOGFIRE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Logfire', + tokenNoun: 'read token', + connectNoun: 'read token', + fields: [ + { id: 'apiToken', label: 'Read token', placeholder: 'pylf_v1_us_...', secret: true }, + { + id: 'domain', + label: 'Region or host', + placeholder: 'auto, us, eu, or https://logfire.example.com', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/logfire', + helpText: + 'Create a read token in your Logfire project under Settings > Read tokens. Enter auto to use the region in the token, us or eu to choose a Logfire Cloud region, or the base URL of a self-hosted instance.', + invalidCredentialsHelp: + 'Logfire rejected this read token. Check that it is a read token (not a write token), has not expired, and belongs to the region or host you entered.', + }, + [MAILGUN_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: MAILGUN_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Mailgun', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste Mailgun API key', secret: true }, + { + id: 'domain', + label: 'Region', + placeholder: 'us or eu', + secret: false, + hintPattern: /^(us|eu|US|EU)$/, + hintMessage: 'Enter us or eu, the region your Mailgun account is hosted in.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/mailgun', + helpText: + 'Use an account API key from API Security in Mailgun, and the region (us or eu) your account is hosted in. Domain sending keys only send mail and cannot be saved here.', + invalidCredentialsHelp: + 'Mailgun rejected this key. Check that it is an account API key, not a domain sending key, and that the region matches the one your account is hosted in.', + }, + [JOTFORM_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: JOTFORM_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Jotform', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste Jotform API key', secret: true }, + { + id: 'domain', + label: 'Region', + placeholder: 'us, eu, or hipaa', + secret: false, + hintPattern: /^(us|eu|hipaa|US|EU|HIPAA)$/, + hintMessage: 'Enter us, eu, or hipaa, the region your Jotform account is hosted in.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/jotform', + helpText: + 'Create a key under Account Settings → API. Use eu for EU Safe accounts and hipaa for HIPAA accounts. A Read Access key validates here and then fails on actions that write.', + invalidCredentialsHelp: + 'Jotform rejected this key. Check that it is still enabled and that the region matches your account: a key only works on the host that issued it.', + }, + [PLANE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: PLANE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Plane', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { id: 'apiToken', label: 'API token', placeholder: 'plane_api_...', secret: true }, + { + id: 'domain', + label: 'Instance URL', + placeholder: 'https://api.plane.so', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/plane', + helpText: + 'Create a token under Profile settings → API tokens. Enter https://api.plane.so for Plane Cloud, or your self-hosted instance URL including any reverse-proxy path, without /api/v1.', + invalidCredentialsHelp: + 'Plane rejected this token. Check that it is active and was created on the instance URL you entered.', + }, + [AZURE_DEVOPS_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: AZURE_DEVOPS_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Azure DevOps', + tokenNoun: 'personal access token', + connectNoun: 'personal access token', + fields: [ + { + id: 'apiToken', + label: 'Personal access token', + placeholder: 'Paste your Azure DevOps personal access token', + secret: true, + }, + { + id: 'providerWorkspaceId', + label: 'Organization', + placeholder: 'contoso', + secret: false, + hintPattern: /^[A-Za-z0-9][A-Za-z0-9-]*$/, + hintMessage: 'Enter the organization name only, as in dev.azure.com/contoso.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/azure_devops', + helpText: + 'Create a personal access token in Azure DevOps under User settings > Personal access tokens, scoped to this organization with Build: Read and Work Items: Read & write. Enter the organization name from dev.azure.com/.', + invalidCredentialsHelp: + 'Azure DevOps rejected this token. Check that it has not expired or been revoked, and that it was created for the organization you entered.', + }, + [TWILIO_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: TWILIO_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Twilio', + tokenNoun: 'auth token', + connectNoun: 'auth token', + fields: [ + { + id: 'apiToken', + label: 'Auth token', + placeholder: 'Paste your Twilio auth token', + secret: true, + }, + { + id: 'providerWorkspaceId', + label: 'Account SID', + placeholder: 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + secret: false, + hintPattern: /^AC[0-9a-f]{32}$/, + hintMessage: 'Twilio Account SIDs start with AC followed by 32 characters.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/twilio_sms', + helpText: + 'Copy the Account SID and Auth Token from Account Info on your Twilio Console dashboard.', + invalidCredentialsHelp: + 'Twilio rejected this Account SID and auth token. Check that the token belongs to this account and has not been rotated.', + }, + [ALGOLIA_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: ALGOLIA_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Algolia', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Algolia API key', secret: true }, + { + id: 'providerWorkspaceId', + label: 'Application ID', + placeholder: 'e.g. ABCD1234EF', + secret: false, + hintPattern: /^[A-Za-z0-9]+$/, + hintMessage: 'Algolia application IDs contain only letters and numbers.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/algolia', + helpText: + 'Copy the Application ID and an API key from Settings > API Keys in the Algolia dashboard. Use the Write API key, or a key with the ACLs your workflows need; a Search-only key can only search, and secured (derived) keys are not supported.', + invalidCredentialsHelp: + 'Algolia rejected this application ID and API key. Check that the key belongs to this application and has not been deleted.', + }, + [UPSTASH_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: UPSTASH_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Upstash', + tokenNoun: 'REST token', + connectNoun: 'REST token', + fields: [ + { + id: 'apiToken', + label: 'REST token', + placeholder: 'Paste your Upstash Redis REST token', + secret: true, + }, + { + id: 'domain', + label: 'REST URL', + placeholder: 'https://your-database.upstash.io', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/upstash', + helpText: + 'Copy the REST URL and token from the REST API section of your database in the Upstash console. A read-only token works for read operations only.', + invalidCredentialsHelp: + 'Upstash rejected this REST token. Check that it belongs to the database at this URL and was not reset.', + }, + [EMAILBISON_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: EMAILBISON_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Email Bison', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { + id: 'apiToken', + label: 'API token', + placeholder: 'Paste your Email Bison API token', + secret: true, + }, + { + id: 'domain', + label: 'Instance URL', + placeholder: 'https://dedi.emailbison.com', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/emailbison', + helpText: + 'Create an API token in Email Bison under Settings > Developer API. Enter the URL you sign in to Email Bison at; a workspace token only works for the workspace it was created in.', + invalidCredentialsHelp: + 'Email Bison rejected this token. Check that it was copied in full and that the instance URL is the one where the token was created.', + }, + [DEVIN_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: DEVIN_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Devin', + tokenNoun: 'service user API key', + connectNoun: 'API key', + fields: [ + { + id: 'apiToken', + label: 'API key', + placeholder: 'cog_...', + secret: true, + hintPattern: /^cog_/, + hintMessage: 'Devin service user keys start with cog_.', + }, + { + id: 'providerWorkspaceId', + label: 'Organization ID', + placeholder: 'org-...', + secret: false, + hintPattern: /^org-/, + hintMessage: 'Devin organization IDs start with org-.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/devin', + helpText: + 'Create a service user in Devin under Settings > Service Users with permission to manage sessions, and copy its API key and your organization ID.', + invalidCredentialsHelp: + 'Devin rejected this key for that organization. Check that the key belongs to a service user in this organization and can view sessions.', + }, + [OKTA_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: OKTA_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Okta', + tokenNoun: 'API token', + connectNoun: 'API token', + fields: [ + { + id: 'apiToken', + label: 'API token', + placeholder: 'Paste your Okta API token', + secret: true, + }, + { id: 'domain', label: 'Okta domain', placeholder: 'dev-123456.okta.com', secret: false }, + ], + docsUrl: 'https://docs.sim.ai/integrations/okta', + helpText: + 'Create an API token in the Okta Admin Console under Security > API > Tokens. The token acts with the admin roles of the user who created it. Enter your Okta domain, such as dev-123456.okta.com; custom domains are not supported.', + invalidCredentialsHelp: + 'Okta rejected this token. Check that it is active, was created in this Okta org, and that the domain is your Okta-hosted domain (okta.com, oktapreview.com, okta-emea.com, okta-gov.com, or trexcloud.com).', + }, + [INFISICAL_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: INFISICAL_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Infisical', + tokenNoun: 'machine identity access token', + connectNoun: 'access token', + fields: [ + { + id: 'apiToken', + label: 'Access token', + placeholder: 'Paste a machine identity access token', + secret: true, + }, + { + id: 'domain', + label: 'Instance URL', + placeholder: 'https://us.infisical.com', + secret: false, + url: true, + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/infisical', + helpText: + 'Use https://us.infisical.com for US Cloud, https://eu.infisical.com for EU Cloud, or your self-hosted URL. Universal Auth tokens expire with their TTL; a Token Auth token with a long TTL keeps runs working.', + invalidCredentialsHelp: + 'Paste a machine identity access token, not a client secret or service token, and make sure the instance URL matches the region the identity lives in.', + }, + [SUPABASE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: SUPABASE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Supabase', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your project API key', secret: true }, + { id: 'domain', label: 'Project ID', placeholder: 'abcdefghijklmnopqrst', secret: false }, + ], + docsUrl: 'https://docs.sim.ai/integrations/supabase', + helpText: + 'Use a key from Project Settings > API Keys. A secret or service_role key bypasses row-level security; an anon or publishable key is limited by your policies.', + invalidCredentialsHelp: + "Supabase rejected this key. Check that it belongs to the project you entered and hasn't been revoked.", + }, + [THRIVE_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: THRIVE_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Thrive', + tokenNoun: 'API key', + connectNoun: 'API key', + fields: [ + { id: 'apiToken', label: 'API key', placeholder: 'Paste your Thrive API key', secret: true }, + { + id: 'providerWorkspaceId', + label: 'Tenant ID', + placeholder: 'Your Thrive tenant ID', + secret: false, + }, + { id: 'domain', label: 'Region host', placeholder: 'public.api.learn.link', secret: false }, + ], + docsUrl: 'https://docs.sim.ai/integrations/thrive', + helpText: + 'Use the API key and tenant ID from your Thrive admin settings. Region host is public.api.learn.link, or public.api.mea.learn.tech for the Middle East and Africa (staging hosts also work).', + invalidCredentialsHelp: + 'Thrive rejected this API key. Check that the tenant ID matches the key and that the region host is the one your tenant uses.', + }, } /** @@ -572,6 +2389,8 @@ export function getTokenServiceAccountErrorMessage( ) case 'site_not_found': return "We couldn't find an account at that domain. Check the spelling and try again." + case 'host_not_allowed': + return "Sim can't connect to that address. Use the service's public https URL." case 'provider_unavailable': return `We couldn't reach ${descriptor.serviceLabel} to verify these credentials. Try again in a moment.` case 'duplicate_display_name': diff --git a/apps/sim/lib/credentials/token-service-accounts/errors.test.ts b/apps/sim/lib/credentials/token-service-accounts/errors.test.ts index dc3eded73a2..f12b7bae6e6 100644 --- a/apps/sim/lib/credentials/token-service-accounts/errors.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/errors.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { fetchProvider, + isHostNotFound, parseProviderJson, readProviderErrorSnippet, TokenServiceAccountValidationError, @@ -64,6 +65,25 @@ describe('token service-account error helpers', () => { }) }) + describe('isHostNotFound', () => { + it('reads ENOTFOUND on the cause even when the outer error carries another code', () => { + const error = Object.assign(new TypeError('fetch failed'), { + code: 'UND_ERR_CONNECT', + cause: Object.assign(new Error('getaddrinfo ENOTFOUND'), { code: 'ENOTFOUND' }), + }) + + expect(isHostNotFound(error)).toBe(true) + }) + + it('does not treat a transient resolver failure as a missing host', () => { + const error = Object.assign(new TypeError('fetch failed'), { + cause: Object.assign(new Error('getaddrinfo EAI_AGAIN'), { code: 'EAI_AGAIN' }), + }) + + expect(isHostNotFound(error)).toBe(false) + }) + }) + describe('parseProviderJson', () => { it('maps a non-JSON body to provider_unavailable 502', async () => { const res = new Response('502 Bad Gateway', { status: 200 }) @@ -124,9 +144,24 @@ describe('token service-account error helpers', () => { expect(snippet).toBe(`${'x'.repeat(500)}...`) }) + it('stops reading an oversized error body instead of buffering all of it', async () => { + let pulled = 0 + const chunk = new Uint8Array(16 * 1024).fill(120) + const body = new ReadableStream({ + pull(controller) { + pulled++ + if (pulled > 1000) controller.close() + else controller.enqueue(chunk) + }, + }) + + await expect(readProviderErrorSnippet(new Response(body, { status: 500 }))).resolves.toBe('') + expect(pulled).toBeLessThan(20) + }) + it('never throws on an unreadable body', async () => { const res = new Response('gone', { status: 500 }) - vi.spyOn(res, 'text').mockRejectedValue(new Error('body stream already read')) + await res.text() await expect(readProviderErrorSnippet(res)).resolves.toBe('') }) diff --git a/apps/sim/lib/credentials/token-service-accounts/errors.ts b/apps/sim/lib/credentials/token-service-accounts/errors.ts index 880469c78b7..79e4d985f33 100644 --- a/apps/sim/lib/credentials/token-service-accounts/errors.ts +++ b/apps/sim/lib/credentials/token-service-accounts/errors.ts @@ -1,5 +1,9 @@ import { getErrorMessage } from '@sim/utils/errors' import { truncate } from '@sim/utils/string' +import { + DEFAULT_MAX_ERROR_BODY_BYTES, + readResponseTextWithLimit, +} from '@/lib/core/utils/stream-limits' /** * Discrete validation failure codes returned to the client for token @@ -10,7 +14,18 @@ export type TokenServiceAccountValidationCode = | 'invalid_credentials' | 'permission_conflict' | 'site_not_found' + /** The egress policy refused the typed host (a private address, or plain http). */ + | 'host_not_allowed' | 'provider_unavailable' + /** The provider requires a one-time user consent before it issues tokens (DocuSign JWT Grant). */ + | 'consent_required' + /** The credential authenticated, but the account it pins is not one its user belongs to. */ + | 'account_not_found' + /** + * The credential works, but not for this service or option: a keyless Google Cloud credential + * asked for a Google Workspace service or to impersonate a Google Workspace user (domain-wide delegation). + */ + | 'unsupported_use' export class TokenServiceAccountValidationError extends Error { constructor( @@ -70,6 +85,19 @@ export function isTransientProviderStatus(status: number): boolean { return status === 408 || status === 429 } +/** + * The shared split for a failed provider call: a non-transient 4xx is the + * caller's input being wrong; anything else (5xx, 408/429, or a reply that + * wasn't an error status but still failed) is the provider. + */ +export function providerFailureCode( + status: number +): Extract { + return status >= 400 && status < 500 && !isTransientProviderStatus(status) + ? 'invalid_credentials' + : 'provider_unavailable' +} + export interface FetchProviderOptions { /** * Validation code thrown when the host does not resolve (`ENOTFOUND` only — @@ -92,6 +120,16 @@ export interface FetchProviderOptions { */ const PROVIDER_FETCH_TIMEOUT_MS = 10_000 +/** + * Whether a failed request proves the host doesn't exist (`ENOTFOUND`). Node's fetch + * puts the resolver code on `cause`, Bun's on the error itself; the transient + * `EAI_AGAIN` is not a missing host. + */ +export function isHostNotFound(error: unknown): boolean { + const failure = error as { code?: unknown; cause?: { code?: unknown } } | undefined + return failure?.code === 'ENOTFOUND' || failure?.cause?.code === 'ENOTFOUND' +} + export async function fetchProvider( url: string, init: RequestInit, @@ -104,10 +142,9 @@ export async function fetchProvider( return await fetch(url, { ...init, signal }) } catch (error) { init.signal?.throwIfAborted() - const causeCode = (error as { cause?: { code?: unknown } })?.cause?.code // Only ENOTFOUND proves the host doesn't exist; EAI_AGAIN is a transient // resolver failure and stays provider_unavailable. - if (options?.dnsFailureCode && causeCode === 'ENOTFOUND') { + if (options?.dnsFailureCode && isHostNotFound(error)) { throw new TokenServiceAccountValidationError(options.dnsFailureCode, 400, { step, reason: options.dnsFailureReason ?? 'host does not resolve', @@ -125,7 +162,10 @@ export async function fetchProvider( * error pages, truncated responses) to `provider_unavailable` instead of an * unhandled SyntaxError that would surface as a generic 500. */ -export async function parseProviderJson(res: Response, step: string): Promise { +export async function parseProviderJson( + res: { json(): Promise }, + step: string +): Promise { try { return (await res.json()) as T } catch { @@ -136,13 +176,26 @@ export async function parseProviderJson(res: Response, step: string): Promise } } +/** The parts of a provider response the checks read; both `fetch` and the guarded egress fetch return them. */ +export interface ProviderResponse { + ok: boolean + status: number + headers: { get(name: string): string | null } + body: ReadableStream | null +} + /** * Reads a bounded snippet of a provider error body for server logs. Never * throws — an unreadable body logs as an empty string. */ -export async function readProviderErrorSnippet(res: Response): Promise { +export async function readProviderErrorSnippet(res: ProviderResponse): Promise { try { - return truncate(await res.text(), ERROR_SNIPPET_MAX_LENGTH) + // Bounded: a provider or proxy can answer with an arbitrarily large error body. + const text = await readResponseTextWithLimit(res, { + maxBytes: DEFAULT_MAX_ERROR_BODY_BYTES, + label: 'provider error response', + }) + return truncate(text, ERROR_SNIPPET_MAX_LENGTH) } catch { return '' } @@ -167,17 +220,15 @@ export async function readProviderErrorSnippet(res: Response): Promise { * Snowflake, Atlassian) already raise `site_not_found` before reaching here. */ export async function throwForProviderResponse( - res: Response, + res: ProviderResponse, step: string, context: Record = {} ): Promise { if (res.ok) return const body = await readProviderErrorSnippet(res) - const callerError = - res.status >= 400 && res.status < 500 && !isTransientProviderStatus(res.status) - throw new TokenServiceAccountValidationError( - callerError ? 'invalid_credentials' : 'provider_unavailable', - res.status, - { step, body, ...context } - ) + throw new TokenServiceAccountValidationError(providerFailureCode(res.status), res.status, { + step, + body, + ...context, + }) } diff --git a/apps/sim/lib/credentials/token-service-accounts/server.ts b/apps/sim/lib/credentials/token-service-accounts/server.ts index 4cc783075ab..e935ef4331c 100644 --- a/apps/sim/lib/credentials/token-service-accounts/server.ts +++ b/apps/sim/lib/credentials/token-service-accounts/server.ts @@ -1,29 +1,127 @@ import type { ServiceAccountPrincipal } from '@/lib/credentials/principal' import { + AFFINITY_SERVICE_ACCOUNT_PROVIDER_ID, + AGENTMAIL_SERVICE_ACCOUNT_PROVIDER_ID, + AGENTPHONE_SERVICE_ACCOUNT_PROVIDER_ID, + AHREFS_SERVICE_ACCOUNT_PROVIDER_ID, AIRTABLE_SERVICE_ACCOUNT_PROVIDER_ID, + ALGOLIA_SERVICE_ACCOUNT_PROVIDER_ID, + APIFY_SERVICE_ACCOUNT_PROVIDER_ID, + APOLLO_SERVICE_ACCOUNT_PROVIDER_ID, ASANA_SERVICE_ACCOUNT_PROVIDER_ID, + ASHBY_SERVICE_ACCOUNT_PROVIDER_ID, ATTIO_SERVICE_ACCOUNT_PROVIDER_ID, + AZURE_DEVOPS_SERVICE_ACCOUNT_PROVIDER_ID, + BITBUCKET_SERVICE_ACCOUNT_PROVIDER_ID, + BREX_SERVICE_ACCOUNT_PROVIDER_ID, + BRIGHTDATA_SERVICE_ACCOUNT_PROVIDER_ID, + BROWSER_USE_SERVICE_ACCOUNT_PROVIDER_ID, + BUFFER_SERVICE_ACCOUNT_PROVIDER_ID, CALCOM_SERVICE_ACCOUNT_PROVIDER_ID, + CALENDLY_SERVICE_ACCOUNT_PROVIDER_ID, + CHECKR_SERVICE_ACCOUNT_PROVIDER_ID, + CIRCLEBACK_SERVICE_ACCOUNT_PROVIDER_ID, CLAUDE_PLATFORM_SERVICE_ACCOUNT_PROVIDER_ID, + CLERK_SERVICE_ACCOUNT_PROVIDER_ID, CLICKUP_SERVICE_ACCOUNT_PROVIDER_ID, + CLOUDFLARE_SERVICE_ACCOUNT_PROVIDER_ID, CODA_SERVICE_ACCOUNT_PROVIDER_ID, + CONVEX_SERVICE_ACCOUNT_PROVIDER_ID, + CRUNCHBASE_SERVICE_ACCOUNT_PROVIDER_ID, + CURSOR_SERVICE_ACCOUNT_PROVIDER_ID, + DAYTONA_SERVICE_ACCOUNT_PROVIDER_ID, + DEVIN_SERVICE_ACCOUNT_PROVIDER_ID, + DISCORD_SERVICE_ACCOUNT_PROVIDER_ID, + DUB_SERVICE_ACCOUNT_PROVIDER_ID, + DYNATRACE_SERVICE_ACCOUNT_PROVIDER_ID, + ELEVENLABS_SERVICE_ACCOUNT_PROVIDER_ID, + EMAILBISON_SERVICE_ACCOUNT_PROVIDER_ID, + ENRICH_SERVICE_ACCOUNT_PROVIDER_ID, + EXTEND_SERVICE_ACCOUNT_PROVIDER_ID, + FATHOM_SERVICE_ACCOUNT_PROVIDER_ID, + FIREFLIES_SERVICE_ACCOUNT_PROVIDER_ID, + FLINT_SERVICE_ACCOUNT_PROVIDER_ID, FLOW_SERVICE_ACCOUNT_PROVIDER_ID, + GAMMA_SERVICE_ACCOUNT_PROVIDER_ID, GAUGE_SERVICE_ACCOUNT_PROVIDER_ID, + GITHUB_SERVICE_ACCOUNT_PROVIDER_ID, + GITLAB_SERVICE_ACCOUNT_PROVIDER_ID, + GRAFANA_SERVICE_ACCOUNT_PROVIDER_ID, + GRAIN_SERVICE_ACCOUNT_PROVIDER_ID, + GRANOLA_SERVICE_ACCOUNT_PROVIDER_ID, HARMONIC_SERVICE_ACCOUNT_PROVIDER_ID, + HEX_SERVICE_ACCOUNT_PROVIDER_ID, HIGGSFIELD_SERVICE_ACCOUNT_PROVIDER_ID, HUBSPOT_SERVICE_ACCOUNT_PROVIDER_ID, + HUGGINGFACE_SERVICE_ACCOUNT_PROVIDER_ID, + INCIDENTIO_SERVICE_ACCOUNT_PROVIDER_ID, + INFISICAL_SERVICE_ACCOUNT_PROVIDER_ID, + INSTANTLY_SERVICE_ACCOUNT_PROVIDER_ID, + INTERCOM_SERVICE_ACCOUNT_PROVIDER_ID, isTokenServiceAccountProviderId, + JOTFORM_SERVICE_ACCOUNT_PROVIDER_ID, + JUPYTER_SERVICE_ACCOUNT_PROVIDER_ID, + LANGSMITH_SERVICE_ACCOUNT_PROVIDER_ID, + LAUNCHDARKLY_SERVICE_ACCOUNT_PROVIDER_ID, + LEMLIST_SERVICE_ACCOUNT_PROVIDER_ID, LINEAR_SERVICE_ACCOUNT_PROVIDER_ID, + LINQ_SERVICE_ACCOUNT_PROVIDER_ID, + LOGFIRE_SERVICE_ACCOUNT_PROVIDER_ID, + LOOPS_SERVICE_ACCOUNT_PROVIDER_ID, + LUMA_SERVICE_ACCOUNT_PROVIDER_ID, + MAILCHIMP_SERVICE_ACCOUNT_PROVIDER_ID, + MAILGUN_SERVICE_ACCOUNT_PROVIDER_ID, + MEM0_SERVICE_ACCOUNT_PROVIDER_ID, MONDAY_SERVICE_ACCOUNT_PROVIDER_ID, + NEW_RELIC_SERVICE_ACCOUNT_PROVIDER_ID, NOTION_SERVICE_ACCOUNT_PROVIDER_ID, + OKTA_SERVICE_ACCOUNT_PROVIDER_ID, + OTTER_SERVICE_ACCOUNT_PROVIDER_ID, + PAGERDUTY_SERVICE_ACCOUNT_PROVIDER_ID, + PERSONA_SERVICE_ACCOUNT_PROVIDER_ID, PIPEDRIVE_SERVICE_ACCOUNT_PROVIDER_ID, + PLANE_SERVICE_ACCOUNT_PROVIDER_ID, + PROFOUND_SERVICE_ACCOUNT_PROVIDER_ID, + PULSE_SERVICE_ACCOUNT_PROVIDER_ID, + QUARTR_SERVICE_ACCOUNT_PROVIDER_ID, + QUIVER_SERVICE_ACCOUNT_PROVIDER_ID, + RB2B_SERVICE_ACCOUNT_PROVIDER_ID, + REDUCTO_SERVICE_ACCOUNT_PROVIDER_ID, + RESEND_SERVICE_ACCOUNT_PROVIDER_ID, + RIPPLING_SERVICE_ACCOUNT_PROVIDER_ID, + ROCKETLANE_SERVICE_ACCOUNT_PROVIDER_ID, + ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID, + SENDGRID_SERVICE_ACCOUNT_PROVIDER_ID, + SENTRY_SERVICE_ACCOUNT_PROVIDER_ID, SHOPIFY_SERVICE_ACCOUNT_PROVIDER_ID, + SIMILARWEB_SERVICE_ACCOUNT_PROVIDER_ID, + SIXTYFOUR_SERVICE_ACCOUNT_PROVIDER_ID, + SMARTLEAD_SERVICE_ACCOUNT_PROVIDER_ID, SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, + SPORTMONKS_SERVICE_ACCOUNT_PROVIDER_ID, + SQUARE_SERVICE_ACCOUNT_PROVIDER_ID, + STRIPE_SERVICE_ACCOUNT_PROVIDER_ID, + SUPABASE_SERVICE_ACCOUNT_PROVIDER_ID, + TAILSCALE_SERVICE_ACCOUNT_PROVIDER_ID, + TAVILY_SERVICE_ACCOUNT_PROVIDER_ID, + TELEGRAM_SERVICE_ACCOUNT_PROVIDER_ID, + THRIVE_SERVICE_ACCOUNT_PROVIDER_ID, + TINYBIRD_SERVICE_ACCOUNT_PROVIDER_ID, TOKEN_SERVICE_ACCOUNT_SECRET_TYPE, type TokenServiceAccountProviderId, TRELLO_SERVICE_ACCOUNT_PROVIDER_ID, + TRIGGER_DEV_SERVICE_ACCOUNT_PROVIDER_ID, + TWILIO_SERVICE_ACCOUNT_PROVIDER_ID, + TYPEFORM_SERVICE_ACCOUNT_PROVIDER_ID, + UPSTASH_SERVICE_ACCOUNT_PROVIDER_ID, + UPTIMEROBOT_SERVICE_ACCOUNT_PROVIDER_ID, + VERCEL_SERVICE_ACCOUNT_PROVIDER_ID, WEALTHBOX_SERVICE_ACCOUNT_PROVIDER_ID, WEBFLOW_SERVICE_ACCOUNT_PROVIDER_ID, + WHATSAPP_SERVICE_ACCOUNT_PROVIDER_ID, + YOUCOM_SERVICE_ACCOUNT_PROVIDER_ID, + YOUTUBE_SERVICE_ACCOUNT_PROVIDER_ID, + ZEP_SERVICE_ACCOUNT_PROVIDER_ID, } from '@/lib/credentials/token-service-accounts/descriptors' import { validateAirtableServiceAccount } from '@/lib/credentials/token-service-accounts/validators/airtable' import { validateAsanaServiceAccount } from '@/lib/credentials/token-service-accounts/validators/asana' @@ -43,6 +141,106 @@ import { validateNotionServiceAccount } from '@/lib/credentials/token-service-ac import { validatePipedriveServiceAccount } from '@/lib/credentials/token-service-accounts/validators/pipedrive' import { validateShopifyServiceAccount } from '@/lib/credentials/token-service-accounts/validators/shopify' import { validateSnowflakeServiceAccount } from '@/lib/credentials/token-service-accounts/validators/snowflake' +import { + validateAffinityServiceAccount, + validateAgentMailServiceAccount, + validateAgentPhoneServiceAccount, + validateAhrefsServiceAccount, + validateAlgoliaServiceAccount, + validateApifyServiceAccount, + validateApolloServiceAccount, + validateAshbyServiceAccount, + validateAzureDevOpsServiceAccount, + validateBitbucketServiceAccount, + validateBrexServiceAccount, + validateBrightDataServiceAccount, + validateBrowserUseServiceAccount, + validateBufferServiceAccount, + validateCalendlyServiceAccount, + validateCheckrServiceAccount, + validateCirclebackServiceAccount, + validateClerkServiceAccount, + validateCloudflareServiceAccount, + validateConvexServiceAccount, + validateCrunchbaseServiceAccount, + validateCursorServiceAccount, + validateDaytonaServiceAccount, + validateDevinServiceAccount, + validateDiscordServiceAccount, + validateDubServiceAccount, + validateDynatraceServiceAccount, + validateElevenLabsServiceAccount, + validateEmailBisonServiceAccount, + validateEnrichServiceAccount, + validateExtendServiceAccount, + validateFathomServiceAccount, + validateFirefliesServiceAccount, + validateFlintServiceAccount, + validateGammaServiceAccount, + validateGitHubServiceAccount, + validateGitLabServiceAccount, + validateGrafanaServiceAccount, + validateGrainServiceAccount, + validateGranolaServiceAccount, + validateHexServiceAccount, + validateHuggingfaceServiceAccount, + validateIncidentioServiceAccount, + validateInfisicalServiceAccount, + validateInstantlyServiceAccount, + validateIntercomServiceAccount, + validateJotformServiceAccount, + validateJupyterServiceAccount, + validateLangsmithServiceAccount, + validateLaunchdarklyServiceAccount, + validateLemlistServiceAccount, + validateLinqServiceAccount, + validateLogfireServiceAccount, + validateLoopsServiceAccount, + validateLumaServiceAccount, + validateMailchimpServiceAccount, + validateMailgunServiceAccount, + validateMem0ServiceAccount, + validateNewRelicServiceAccount, + validateOktaServiceAccount, + validateOtterServiceAccount, + validatePagerDutyServiceAccount, + validatePersonaServiceAccount, + validatePlaneServiceAccount, + validateProfoundServiceAccount, + validatePulseServiceAccount, + validateQuartrServiceAccount, + validateQuiverServiceAccount, + validateRB2BServiceAccount, + validateReductoServiceAccount, + validateResendServiceAccount, + validateRipplingServiceAccount, + validateRocketlaneServiceAccount, + validateRootlyServiceAccount, + validateSendGridServiceAccount, + validateSentryServiceAccount, + validateSimilarwebServiceAccount, + validateSixtyfourServiceAccount, + validateSmartleadServiceAccount, + validateSportmonksServiceAccount, + validateSquareServiceAccount, + validateStripeServiceAccount, + validateSupabaseServiceAccount, + validateTailscaleServiceAccount, + validateTavilyServiceAccount, + validateTelegramServiceAccount, + validateThriveServiceAccount, + validateTinybirdServiceAccount, + validateTriggerDevServiceAccount, + validateTwilioServiceAccount, + validateTypeformServiceAccount, + validateUpstashServiceAccount, + validateUptimeRobotServiceAccount, + validateVercelServiceAccount, + validateWhatsAppServiceAccount, + validateYouComServiceAccount, + validateYouTubeServiceAccount, + validateZepServiceAccount, +} from '@/lib/credentials/token-service-accounts/validators/token-checks' import { validateTrelloServiceAccount } from '@/lib/credentials/token-service-accounts/validators/trello' import { validateWealthboxServiceAccount } from '@/lib/credentials/token-service-accounts/validators/wealthbox' import { validateWebflowServiceAccount } from '@/lib/credentials/token-service-accounts/validators/webflow' @@ -77,6 +275,8 @@ export interface TokenServiceAccountValidationResult { storedMetadata?: Record /** Normalized domain to persist instead of the raw user input (when collected). */ normalizedDomain?: string + /** Normalized account id to persist instead of the raw user input (when collected). */ + normalizedWorkspaceId?: string } export type TokenServiceAccountValidator = ( @@ -113,6 +313,104 @@ const TOKEN_SERVICE_ACCOUNT_VALIDATORS: Record< [GAUGE_SERVICE_ACCOUNT_PROVIDER_ID]: validateGaugeServiceAccount, [HIGGSFIELD_SERVICE_ACCOUNT_PROVIDER_ID]: validateHiggsfieldServiceAccount, [FLOW_SERVICE_ACCOUNT_PROVIDER_ID]: validateFlowServiceAccount, + [FLINT_SERVICE_ACCOUNT_PROVIDER_ID]: validateFlintServiceAccount, + [REDUCTO_SERVICE_ACCOUNT_PROVIDER_ID]: validateReductoServiceAccount, + [QUIVER_SERVICE_ACCOUNT_PROVIDER_ID]: validateQuiverServiceAccount, + [PULSE_SERVICE_ACCOUNT_PROVIDER_ID]: validatePulseServiceAccount, + [INTERCOM_SERVICE_ACCOUNT_PROVIDER_ID]: validateIntercomServiceAccount, + [FIREFLIES_SERVICE_ACCOUNT_PROVIDER_ID]: validateFirefliesServiceAccount, + [EXTEND_SERVICE_ACCOUNT_PROVIDER_ID]: validateExtendServiceAccount, + [DUB_SERVICE_ACCOUNT_PROVIDER_ID]: validateDubServiceAccount, + [CURSOR_SERVICE_ACCOUNT_PROVIDER_ID]: validateCursorServiceAccount, + [DISCORD_SERVICE_ACCOUNT_PROVIDER_ID]: validateDiscordServiceAccount, + [ZEP_SERVICE_ACCOUNT_PROVIDER_ID]: validateZepServiceAccount, + [MEM0_SERVICE_ACCOUNT_PROVIDER_ID]: validateMem0ServiceAccount, + [YOUTUBE_SERVICE_ACCOUNT_PROVIDER_ID]: validateYouTubeServiceAccount, + [YOUCOM_SERVICE_ACCOUNT_PROVIDER_ID]: validateYouComServiceAccount, + [UPTIMEROBOT_SERVICE_ACCOUNT_PROVIDER_ID]: validateUptimeRobotServiceAccount, + [TRIGGER_DEV_SERVICE_ACCOUNT_PROVIDER_ID]: validateTriggerDevServiceAccount, + [TAILSCALE_SERVICE_ACCOUNT_PROVIDER_ID]: validateTailscaleServiceAccount, + [SQUARE_SERVICE_ACCOUNT_PROVIDER_ID]: validateSquareServiceAccount, + [SPORTMONKS_SERVICE_ACCOUNT_PROVIDER_ID]: validateSportmonksServiceAccount, + [SMARTLEAD_SERVICE_ACCOUNT_PROVIDER_ID]: validateSmartleadServiceAccount, + [SIXTYFOUR_SERVICE_ACCOUNT_PROVIDER_ID]: validateSixtyfourServiceAccount, + [SIMILARWEB_SERVICE_ACCOUNT_PROVIDER_ID]: validateSimilarwebServiceAccount, + [SENDGRID_SERVICE_ACCOUNT_PROVIDER_ID]: validateSendGridServiceAccount, + [ROCKETLANE_SERVICE_ACCOUNT_PROVIDER_ID]: validateRocketlaneServiceAccount, + [RIPPLING_SERVICE_ACCOUNT_PROVIDER_ID]: validateRipplingServiceAccount, + [RB2B_SERVICE_ACCOUNT_PROVIDER_ID]: validateRB2BServiceAccount, + [QUARTR_SERVICE_ACCOUNT_PROVIDER_ID]: validateQuartrServiceAccount, + [PROFOUND_SERVICE_ACCOUNT_PROVIDER_ID]: validateProfoundServiceAccount, + [PERSONA_SERVICE_ACCOUNT_PROVIDER_ID]: validatePersonaServiceAccount, + [OTTER_SERVICE_ACCOUNT_PROVIDER_ID]: validateOtterServiceAccount, + [MAILCHIMP_SERVICE_ACCOUNT_PROVIDER_ID]: validateMailchimpServiceAccount, + [LUMA_SERVICE_ACCOUNT_PROVIDER_ID]: validateLumaServiceAccount, + [LOOPS_SERVICE_ACCOUNT_PROVIDER_ID]: validateLoopsServiceAccount, + [LINQ_SERVICE_ACCOUNT_PROVIDER_ID]: validateLinqServiceAccount, + [LAUNCHDARKLY_SERVICE_ACCOUNT_PROVIDER_ID]: validateLaunchdarklyServiceAccount, + [LANGSMITH_SERVICE_ACCOUNT_PROVIDER_ID]: validateLangsmithServiceAccount, + [INSTANTLY_SERVICE_ACCOUNT_PROVIDER_ID]: validateInstantlyServiceAccount, + [INCIDENTIO_SERVICE_ACCOUNT_PROVIDER_ID]: validateIncidentioServiceAccount, + [HUGGINGFACE_SERVICE_ACCOUNT_PROVIDER_ID]: validateHuggingfaceServiceAccount, + [HEX_SERVICE_ACCOUNT_PROVIDER_ID]: validateHexServiceAccount, + [GAMMA_SERVICE_ACCOUNT_PROVIDER_ID]: validateGammaServiceAccount, + [ENRICH_SERVICE_ACCOUNT_PROVIDER_ID]: validateEnrichServiceAccount, + [ELEVENLABS_SERVICE_ACCOUNT_PROVIDER_ID]: validateElevenLabsServiceAccount, + [DAYTONA_SERVICE_ACCOUNT_PROVIDER_ID]: validateDaytonaServiceAccount, + [CRUNCHBASE_SERVICE_ACCOUNT_PROVIDER_ID]: validateCrunchbaseServiceAccount, + [CLOUDFLARE_SERVICE_ACCOUNT_PROVIDER_ID]: validateCloudflareServiceAccount, + [CLERK_SERVICE_ACCOUNT_PROVIDER_ID]: validateClerkServiceAccount, + [CIRCLEBACK_SERVICE_ACCOUNT_PROVIDER_ID]: validateCirclebackServiceAccount, + [BUFFER_SERVICE_ACCOUNT_PROVIDER_ID]: validateBufferServiceAccount, + [BROWSER_USE_SERVICE_ACCOUNT_PROVIDER_ID]: validateBrowserUseServiceAccount, + [BRIGHTDATA_SERVICE_ACCOUNT_PROVIDER_ID]: validateBrightDataServiceAccount, + [BITBUCKET_SERVICE_ACCOUNT_PROVIDER_ID]: validateBitbucketServiceAccount, + [BREX_SERVICE_ACCOUNT_PROVIDER_ID]: validateBrexServiceAccount, + [APOLLO_SERVICE_ACCOUNT_PROVIDER_ID]: validateApolloServiceAccount, + [APIFY_SERVICE_ACCOUNT_PROVIDER_ID]: validateApifyServiceAccount, + [AHREFS_SERVICE_ACCOUNT_PROVIDER_ID]: validateAhrefsServiceAccount, + [AGENTPHONE_SERVICE_ACCOUNT_PROVIDER_ID]: validateAgentPhoneServiceAccount, + [AGENTMAIL_SERVICE_ACCOUNT_PROVIDER_ID]: validateAgentMailServiceAccount, + [AFFINITY_SERVICE_ACCOUNT_PROVIDER_ID]: validateAffinityServiceAccount, + [TAVILY_SERVICE_ACCOUNT_PROVIDER_ID]: validateTavilyServiceAccount, + [ALGOLIA_SERVICE_ACCOUNT_PROVIDER_ID]: validateAlgoliaServiceAccount, + [ASHBY_SERVICE_ACCOUNT_PROVIDER_ID]: validateAshbyServiceAccount, + [AZURE_DEVOPS_SERVICE_ACCOUNT_PROVIDER_ID]: validateAzureDevOpsServiceAccount, + [CALENDLY_SERVICE_ACCOUNT_PROVIDER_ID]: validateCalendlyServiceAccount, + [CHECKR_SERVICE_ACCOUNT_PROVIDER_ID]: validateCheckrServiceAccount, + [CONVEX_SERVICE_ACCOUNT_PROVIDER_ID]: validateConvexServiceAccount, + [DEVIN_SERVICE_ACCOUNT_PROVIDER_ID]: validateDevinServiceAccount, + [DYNATRACE_SERVICE_ACCOUNT_PROVIDER_ID]: validateDynatraceServiceAccount, + [EMAILBISON_SERVICE_ACCOUNT_PROVIDER_ID]: validateEmailBisonServiceAccount, + [FATHOM_SERVICE_ACCOUNT_PROVIDER_ID]: validateFathomServiceAccount, + [GITHUB_SERVICE_ACCOUNT_PROVIDER_ID]: validateGitHubServiceAccount, + [GITLAB_SERVICE_ACCOUNT_PROVIDER_ID]: validateGitLabServiceAccount, + [GRAFANA_SERVICE_ACCOUNT_PROVIDER_ID]: validateGrafanaServiceAccount, + [GRAIN_SERVICE_ACCOUNT_PROVIDER_ID]: validateGrainServiceAccount, + [GRANOLA_SERVICE_ACCOUNT_PROVIDER_ID]: validateGranolaServiceAccount, + [INFISICAL_SERVICE_ACCOUNT_PROVIDER_ID]: validateInfisicalServiceAccount, + [JOTFORM_SERVICE_ACCOUNT_PROVIDER_ID]: validateJotformServiceAccount, + [JUPYTER_SERVICE_ACCOUNT_PROVIDER_ID]: validateJupyterServiceAccount, + [LEMLIST_SERVICE_ACCOUNT_PROVIDER_ID]: validateLemlistServiceAccount, + [LOGFIRE_SERVICE_ACCOUNT_PROVIDER_ID]: validateLogfireServiceAccount, + [MAILGUN_SERVICE_ACCOUNT_PROVIDER_ID]: validateMailgunServiceAccount, + [NEW_RELIC_SERVICE_ACCOUNT_PROVIDER_ID]: validateNewRelicServiceAccount, + [OKTA_SERVICE_ACCOUNT_PROVIDER_ID]: validateOktaServiceAccount, + [PAGERDUTY_SERVICE_ACCOUNT_PROVIDER_ID]: validatePagerDutyServiceAccount, + [PLANE_SERVICE_ACCOUNT_PROVIDER_ID]: validatePlaneServiceAccount, + [RESEND_SERVICE_ACCOUNT_PROVIDER_ID]: validateResendServiceAccount, + [ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID]: validateRootlyServiceAccount, + [SENTRY_SERVICE_ACCOUNT_PROVIDER_ID]: validateSentryServiceAccount, + [STRIPE_SERVICE_ACCOUNT_PROVIDER_ID]: validateStripeServiceAccount, + [SUPABASE_SERVICE_ACCOUNT_PROVIDER_ID]: validateSupabaseServiceAccount, + [TELEGRAM_SERVICE_ACCOUNT_PROVIDER_ID]: validateTelegramServiceAccount, + [THRIVE_SERVICE_ACCOUNT_PROVIDER_ID]: validateThriveServiceAccount, + [TINYBIRD_SERVICE_ACCOUNT_PROVIDER_ID]: validateTinybirdServiceAccount, + [TWILIO_SERVICE_ACCOUNT_PROVIDER_ID]: validateTwilioServiceAccount, + [TYPEFORM_SERVICE_ACCOUNT_PROVIDER_ID]: validateTypeformServiceAccount, + [UPSTASH_SERVICE_ACCOUNT_PROVIDER_ID]: validateUpstashServiceAccount, + [VERCEL_SERVICE_ACCOUNT_PROVIDER_ID]: validateVercelServiceAccount, + [WHATSAPP_SERVICE_ACCOUNT_PROVIDER_ID]: validateWhatsAppServiceAccount, } export function getTokenServiceAccountValidator( diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/bitbucket.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/bitbucket.test.ts new file mode 100644 index 00000000000..8cc4860dea8 --- /dev/null +++ b/apps/sim/lib/credentials/token-service-accounts/validators/bitbucket.test.ts @@ -0,0 +1,80 @@ +/** + * @vitest-environment node + */ +import { jsonResponse } from '@sim/testing/helpers/http' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { validateBitbucketServiceAccount } from '@/lib/credentials/token-service-accounts/validators/token-checks' + +const TOKEN = 'test-bitbucket-token' +const repository = { full_name: 'acme/api', slug: 'api' } + +describe('Bitbucket saved access token verification', () => { + const fetch = vi.fn() + + beforeEach(() => { + fetch.mockReset() + vi.stubGlobal('fetch', fetch) + }) + + it("reads the typed repository at Bitbucket's fixed API host", async () => { + fetch.mockResolvedValue(jsonResponse(repository)) + + await expect( + validateBitbucketServiceAccount({ apiToken: TOKEN, providerWorkspaceId: ' acme/api ' }) + ).resolves.toMatchObject({ + displayName: 'Bitbucket (acme/api)', + auditMetadata: { accountId: 'acme/api' }, + }) + expect(fetch).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/acme/api', + expect.objectContaining({ + redirect: 'error', + headers: expect.objectContaining({ Authorization: `Bearer ${TOKEN}` }), + }) + ) + }) + + it.each([ + ['a repository that climbs out of the workspace', 'acme/..'], + ['a path that climbs further', 'acme/../../user'], + ['a query that rewrites the request', 'acme/api?role=owner'], + ['another host', 'evil.example.com/api'], + ['an encoded separator', 'acme/api%2F..'], + ['a workspace without a repository', 'acme'], + ['nothing at all', ' '], + ])('refuses %s as the repository without calling Bitbucket', async (_, workspace) => { + await expect( + validateBitbucketServiceAccount({ apiToken: TOKEN, providerWorkspaceId: workspace }) + ).rejects.toMatchObject({ code: 'invalid_credentials' }) + expect(fetch).not.toHaveBeenCalled() + }) + + it.each([ + ['a bad, expired, or revoked token', 401], + ['a token without the repository scope', 403], + ['a repository that does not exist or the token cannot reach', 404], + ])('rejects %s as invalid credentials, never echoing the token', async (_, status) => { + fetch.mockResolvedValue( + jsonResponse({ type: 'error', error: { message: 'Token is invalid' } }, status) + ) + + const error = await validateBitbucketServiceAccount({ + apiToken: TOKEN, + providerWorkspaceId: 'acme/api', + }).catch((e) => e) + + expect(error).toMatchObject({ code: 'invalid_credentials' }) + expect(JSON.stringify(error.logDetail)).not.toContain(TOKEN) + }) + + it.each([ + ['an outage', new Response('', { status: 502 })], + ['a reply that is not a repository', jsonResponse({ values: [] })], + ])('reports %s as Bitbucket being unavailable, not as a bad token', async (_, reply) => { + fetch.mockResolvedValue(reply) + + await expect( + validateBitbucketServiceAccount({ apiToken: TOKEN, providerWorkspaceId: 'acme/api' }) + ).rejects.toMatchObject({ code: 'provider_unavailable' }) + }) +}) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/flow.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/flow.test.ts new file mode 100644 index 00000000000..c9464c73fe3 --- /dev/null +++ b/apps/sim/lib/credentials/token-service-accounts/validators/flow.test.ts @@ -0,0 +1,38 @@ +import { + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing/mocks/input-validation.mock' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { EgressRefusedError } from '@/lib/core/network/transport.server' +import { validateFlowServiceAccount } from '@/lib/credentials/token-service-accounts/validators/flow' + +const fields = { apiToken: 'tok', domain: 'flow.internal', providerWorkspaceId: 'ws-1' } + +describe('validateFlowServiceAccount', () => { + it('reports a host the egress policy refuses as the user input, not a retryable outage', async () => { + inputValidationMockFns.mockSecureFetchWithValidation.mockRejectedValue( + new EgressRefusedError('url resolves to a private address') + ) + + await expect(validateFlowServiceAccount(fields)).rejects.toMatchObject({ + code: 'host_not_allowed', + status: 400, + }) + }) + + it('reports a host that does not exist as not found', async () => { + inputValidationMockFns.mockSecureFetchWithValidation.mockRejectedValue( + new Error('url hostname could not be resolved', { + cause: Object.assign(new Error('getaddrinfo ENOTFOUND'), { code: 'ENOTFOUND' }), + }) + ) + + await expect(validateFlowServiceAccount(fields)).rejects.toMatchObject({ + code: 'site_not_found', + status: 400, + }) + }) +}) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/flow.ts b/apps/sim/lib/credentials/token-service-accounts/validators/flow.ts index 9e819f73bb0..5096fc6860e 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/flow.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/flow.ts @@ -1,7 +1,9 @@ import { toRecord } from '@sim/utils/object' +import { EgressRefusedError } from '@/lib/core/network/transport.server' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { userPrincipal } from '@/lib/credentials/principal' import { + isHostNotFound, isTransientProviderStatus, TokenServiceAccountValidationError, } from '@/lib/credentials/token-service-accounts/errors' @@ -60,6 +62,13 @@ export async function validateFlowServiceAccount( } } catch (error) { if (error instanceof TokenServiceAccountValidationError) throw error + // A host the egress policy refuses, or one that doesn't exist, is the user's input. + if (error instanceof EgressRefusedError) { + throw new TokenServiceAccountValidationError('host_not_allowed', 400) + } + if (error instanceof Error && isHostNotFound(error.cause)) { + throw new TokenServiceAccountValidationError('site_not_found', 400) + } throw new TokenServiceAccountValidationError('provider_unavailable', 502) } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/github.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/github.test.ts new file mode 100644 index 00000000000..088cddc504a --- /dev/null +++ b/apps/sim/lib/credentials/token-service-accounts/validators/github.test.ts @@ -0,0 +1,64 @@ +/** + * @vitest-environment node + */ +import { jsonResponse } from '@sim/testing/helpers/http' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { validateGitHubServiceAccount } from '@/lib/credentials/token-service-accounts/validators/token-checks' + +const TOKEN = 'test-github-token' + +describe('GitHub saved personal access token verification', () => { + const fetch = vi.fn() + + beforeEach(() => { + fetch.mockReset() + vi.stubGlobal('fetch', fetch) + }) + + it('verifies the token against api.github.com as the tools send it and names it by login', async () => { + fetch.mockResolvedValue(jsonResponse({ login: 'octo-bot', id: 1 })) + + await expect(validateGitHubServiceAccount({ apiToken: TOKEN })).resolves.toMatchObject({ + displayName: 'octo-bot (GitHub)', + }) + expect(fetch).toHaveBeenCalledWith( + 'https://api.github.com/user', + expect.objectContaining({ + redirect: 'error', + headers: expect.objectContaining({ + Authorization: `Bearer ${TOKEN}`, + 'User-Agent': 'Sim', + }), + }) + ) + }) + + it.each([ + ['a bad or revoked token', 401, { message: 'Bad credentials' }], + [ + 'a GitHub App installation token, which cannot act as a user', + 403, + { message: 'Resource not accessible by integration' }, + ], + ])('rejects %s as invalid credentials, never echoing the token', async (_, status, body) => { + fetch.mockResolvedValue(jsonResponse(body, status)) + + const error = await validateGitHubServiceAccount({ apiToken: TOKEN }).catch((e) => e) + + expect(error).toMatchObject({ code: 'invalid_credentials' }) + expect(JSON.stringify(error.logDetail)).not.toContain(TOKEN) + expect(error.message).not.toContain(TOKEN) + }) + + it.each([ + ['a rate limit', jsonResponse({ message: 'API rate limit exceeded' }, 429)], + ['an outage', new Response('unicorn', { status: 503 })], + ['a reply that is not a user', jsonResponse({ ok: true })], + ])('reports %s as GitHub being unavailable, not as a bad token', async (_, reply) => { + fetch.mockResolvedValue(reply) + + await expect(validateGitHubServiceAccount({ apiToken: TOKEN })).rejects.toMatchObject({ + code: 'provider_unavailable', + }) + }) +}) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/gitlab.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/gitlab.test.ts new file mode 100644 index 00000000000..9edffc5060b --- /dev/null +++ b/apps/sim/lib/credentials/token-service-accounts/validators/gitlab.test.ts @@ -0,0 +1,102 @@ +/** + * @vitest-environment node + */ +import { jsonResponse } from '@sim/testing/helpers/http' +import { + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing/mocks/input-validation.mock' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { validateGitLabServiceAccount } from '@/lib/credentials/token-service-accounts/validators/token-checks' + +const activeToken = { name: 'sim-workflows', active: true, revoked: false, scopes: ['api'] } + +describe('GitLab saved token verification', () => { + const fetch = inputValidationMockFns.mockSecureFetchWithPinnedIP + const validateUrl = inputValidationMockFns.mockValidateUrlWithDNS + + beforeEach(() => { + fetch.mockReset() + validateUrl.mockReset().mockResolvedValue({ + isValid: true, + resolvedIP: '203.0.113.7', + originalHostname: 'gitlab.example.com', + }) + }) + + it('verifies the token at the host it was typed with and stores that host', async () => { + fetch.mockResolvedValue(jsonResponse(activeToken)) + + await expect( + validateGitLabServiceAccount({ + apiToken: 'test-gitlab-token', + domain: 'https://gitlab.example.com/', + }) + ).resolves.toMatchObject({ + normalizedDomain: 'gitlab.example.com', + displayName: 'sim-workflows (gitlab.example.com)', + auditMetadata: { domain: 'gitlab.example.com' }, + }) + expect(fetch).toHaveBeenCalledWith( + 'https://gitlab.example.com/api/v4/personal_access_tokens/self', + '203.0.113.7', + expect.objectContaining({ maxRedirects: 0, profile: 'configuredEndpoint' }) + ) + }) + + it('refuses a host that could send the token somewhere else, without calling it', async () => { + await expect( + validateGitLabServiceAccount({ + apiToken: 'test-gitlab-token', + domain: 'evil.example.com@gitlab.com', + }) + ).rejects.toMatchObject({ code: 'invalid_credentials' }) + expect(fetch).not.toHaveBeenCalled() + }) + + it.each([ + ['a revoked token', { ...activeToken, active: false, revoked: true }], + ['a token without an API scope', { ...activeToken, scopes: ['read_repository'] }], + ])('rejects %s', async (_, token) => { + fetch.mockResolvedValue(jsonResponse(token)) + + await expect( + validateGitLabServiceAccount({ apiToken: 'test-gitlab-token', domain: 'gitlab.com' }) + ).rejects.toMatchObject({ code: 'invalid_credentials' }) + }) + + it.each([ + ['an HTML page', new Response('', { status: 200 })], + ['JSON that is not a token lookup', jsonResponse({ ok: true })], + ])('reports %s as GitLab misbehaving, not as a bad token', async (_, reply) => { + fetch.mockResolvedValue(reply) + + await expect( + validateGitLabServiceAccount({ apiToken: 'test-gitlab-token', domain: 'gitlab.com' }) + ).rejects.toMatchObject({ code: 'provider_unavailable' }) + }) + + it('reports a host that does not exist as not found, so the user fixes it instead of retrying', async () => { + validateUrl.mockResolvedValue({ + isValid: false, + error: 'url hostname could not be resolved', + refusal: 'dns', + cause: Object.assign(new Error('getaddrinfo ENOTFOUND'), { code: 'ENOTFOUND' }), + }) + + await expect( + validateGitLabServiceAccount({ apiToken: 'test-gitlab-token', domain: 'gitlab.exmaple.com' }) + ).rejects.toMatchObject({ code: 'site_not_found' }) + }) + + it('reports an unreachable GitLab instance as unavailable, not as a bad token', async () => { + fetch.mockResolvedValue(jsonResponse({ message: 'Bad Gateway' }, 502)) + + await expect( + validateGitLabServiceAccount({ apiToken: 'test-gitlab-token', domain: 'gitlab.com' }) + ).rejects.toMatchObject({ code: 'provider_unavailable' }) + }) +}) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/http.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/http.test.ts new file mode 100644 index 00000000000..6fad7b593e9 --- /dev/null +++ b/apps/sim/lib/credentials/token-service-accounts/validators/http.test.ts @@ -0,0 +1,291 @@ +import { jsonResponse } from '@sim/testing/helpers/http' +import { + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing/mocks/input-validation.mock' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { EgressRefusedError } from '@/lib/core/network/transport.server' +import { + normalizeHttpBaseUrl, + refuseUnsafeTypedUrl, + tokenCheckValidator, +} from '@/lib/credentials/token-service-accounts/validators/http' + +const validate = tokenCheckValidator({ + serviceLabel: 'Example', + request: ({ apiToken }) => ({ + url: 'https://api.example.com/me', + headers: { Authorization: `Bearer ${apiToken}` }, + }), +}) + +describe('tokenCheckValidator', () => { + it('accepts a token the provider answers 2xx for, sending it only to the declared host', async () => { + const fetch = vi.fn().mockResolvedValue(jsonResponse({ ok: true })) + vi.stubGlobal('fetch', fetch) + + await expect(validate({ apiToken: 'good' })).resolves.toMatchObject({ + displayName: 'Example', + principal: null, + }) + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/me', + expect.objectContaining({ + headers: { Authorization: 'Bearer good' }, + redirect: 'error', + }) + ) + }) + + it.each([ + { status: 401, code: 'invalid_credentials' }, + { status: 403, code: 'invalid_credentials' }, + { status: 429, code: 'provider_unavailable' }, + { status: 503, code: 'provider_unavailable' }, + ])('maps a $status answer to $code', async ({ status, code }) => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse({}, { status }))) + + await expect(validate({ apiToken: 'token' })).rejects.toMatchObject({ code }) + }) + + it('treats a network failure, including a refused redirect, as the provider being unavailable', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockRejectedValue(new TypeError('redirect mode is set to error')) + ) + + await expect(validate({ apiToken: 'token' })).rejects.toMatchObject({ + code: 'provider_unavailable', + }) + }) + + it('treats a 403 as a scoped but valid key only where the vendor says so', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse({}, { status: 403 }))) + const scoped = tokenCheckValidator({ + serviceLabel: 'Scoped', + request: () => ({ url: 'https://api.example.com/me', headers: {} }), + forbiddenMeansScoped: true, + }) + + await expect(scoped({ apiToken: 'token' })).resolves.toMatchObject({ displayName: 'Scoped' }) + + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + new Response('Access denied', { + status: 403, + headers: { 'Content-Type': 'text/html' }, + }) + ) + ) + await expect(scoped({ apiToken: 'token' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + await expect(validate({ apiToken: 'token' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) + + it('rejects a key a successful response body does not prove', async () => { + const graphql = tokenCheckValidator({ + serviceLabel: 'GraphQL', + request: () => ({ url: 'https://api.example.com/graphql', headers: {} }), + acceptsBody: (body) => (body as { data?: { id?: string } }).data?.id === 'account', + }) + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue(jsonResponse({ errors: [{ extensions: { code: 'UNAUTHORIZED' } }] })) + ) + await expect(graphql({ apiToken: 'bad' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse({ data: { id: 'account' } }))) + await expect(graphql({ apiToken: 'good' })).resolves.toMatchObject({ displayName: 'GraphQL' }) + }) + + it('reports a key the vendor rejects with a server error as invalid, not an outage', async () => { + const check = tokenCheckValidator({ + serviceLabel: 'ErrorBody', + request: () => ({ url: 'https://api.example.com/graphql', headers: {} }), + rejectsKey: (body) => (body as { code?: string }).code === 'auth_failed', + }) + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(jsonResponse({ code: 'auth_failed' }, { status: 500 })) + ) + await expect(check({ apiToken: 'bad' })).rejects.toMatchObject({ code: 'invalid_credentials' }) + + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(jsonResponse({ code: 'other' }, { status: 500 })) + ) + await expect(check({ apiToken: 'any' })).rejects.toMatchObject({ code: 'provider_unavailable' }) + }) +}) + +describe('tokenCheckValidator with a typed host', () => { + const validateUrl = inputValidationMockFns.mockValidateUrlWithDNS + const pinnedFetch = inputValidationMockFns.mockSecureFetchWithPinnedIP + const validateHost = tokenCheckValidator({ + serviceLabel: 'Example', + domain: ({ domain }) => { + const url = new URL(`https://${domain?.trim()}`) + if (url.username || url.pathname !== '/') throw new Error('not a bare host') + return url.host + }, + egress: 'configuredEndpoint', + request: ({ apiToken, domain }) => ({ + url: `https://${domain}/api/me`, + headers: { Authorization: `Bearer ${apiToken}` }, + }), + }) + + beforeEach(() => { + validateUrl.mockReset() + validateUrl.mockResolvedValue({ + isValid: true, + resolvedIP: '203.0.113.7', + originalHostname: 'tools.example.com', + }) + pinnedFetch.mockReset() + }) + + it('verifies at the normalized host through the guarded egress and stores that host', async () => { + pinnedFetch.mockResolvedValue(jsonResponse({ ok: true })) + + await expect( + validateHost({ apiToken: 'good', domain: ' Tools.Example.com ' }) + ).resolves.toMatchObject({ normalizedDomain: 'tools.example.com' }) + expect(validateUrl).toHaveBeenCalledWith( + 'https://tools.example.com/api/me', + 'url', + 'configuredEndpoint', + expect.anything() + ) + expect(pinnedFetch).toHaveBeenCalledWith( + 'https://tools.example.com/api/me', + '203.0.113.7', + expect.objectContaining({ profile: 'configuredEndpoint', maxRedirects: 0 }) + ) + }) + + it('refuses a mistyped scheme instead of reading the scheme as a host', () => { + expect(() => normalizeHttpBaseUrl('https:/grafana.example.com')).toThrow(/scheme/) + expect(() => refuseUnsafeTypedUrl('https:/evil.com')).toThrow(/scheme/) + expect(() => refuseUnsafeTypedUrl('HTTP:evil.com')).toThrow(/scheme/) + // The URL parser drops tabs and newlines, so they must not hide a scheme from the guard. + expect(() => refuseUnsafeTypedUrl('ht\ntps:/evil.com')).toThrow(/scheme/) + expect(() => normalizeHttpBaseUrl('ht\ttps:/evil.com')).toThrow(/scheme/) + expect(refuseUnsafeTypedUrl('https://grafana.example.com')).toBe('https://grafana.example.com') + expect(normalizeHttpBaseUrl('grafana.example.com:3000/')).toBe( + 'https://grafana.example.com:3000' + ) + }) + + it('refuses a host its normalizer rejects without sending the token anywhere', async () => { + await expect( + validateHost({ apiToken: 'good', domain: 'evil.example.com@tools.example.com' }) + ).rejects.toMatchObject({ code: 'invalid_credentials' }) + expect(validateUrl).not.toHaveBeenCalled() + expect(pinnedFetch).not.toHaveBeenCalled() + }) + + it('reports a host the egress policy refuses as the user input, not a retryable outage', async () => { + validateUrl.mockResolvedValue({ + isValid: false, + error: 'url resolves to a private address', + refusal: 'policy', + }) + + await expect( + validateHost({ apiToken: 'good', domain: 'grafana.internal' }) + ).rejects.toMatchObject({ code: 'host_not_allowed', status: 400 }) + expect(pinnedFetch).not.toHaveBeenCalled() + }) + + describe('behind an environment proxy', () => { + it("reports the proxy route's own refusal as the host, not an outage", async () => { + pinnedFetch.mockRejectedValue(new EgressRefusedError('url resolves to a private address')) + + await expect( + validateHost({ apiToken: 'good', domain: 'tools.example.com' }) + ).rejects.toMatchObject({ code: 'host_not_allowed', status: 400 }) + }) + }) + + it('reports a host that cannot be reached as the provider being unavailable', async () => { + pinnedFetch.mockRejectedValue(new Error('connect ECONNREFUSED')) + + await expect( + validateHost({ apiToken: 'good', domain: 'tools.example.com' }) + ).rejects.toMatchObject({ code: 'provider_unavailable', status: 502 }) + }) + + it.each([ + [ + 'Node (code on cause)', + new Error('lookup failed', { + cause: Object.assign(new Error('getaddrinfo ENOTFOUND'), { code: 'ENOTFOUND' }), + }), + ], + ['Bun (code on the error)', Object.assign(new Error('lookup failed'), { code: 'ENOTFOUND' })], + ])('reports a host that does not exist as not found under %s', async (_runtime, cause) => { + validateUrl.mockResolvedValue({ + isValid: false, + error: 'url hostname could not be resolved', + cause, + refusal: 'dns', + }) + + await expect( + validateHost({ apiToken: 'good', domain: 'tools.exmaple.com' }) + ).rejects.toMatchObject({ code: 'site_not_found' }) + }) + + it('reports a fixed-host vendor whose typed account does not resolve as not found under Bun', async () => { + const accountHost = tokenCheckValidator({ + serviceLabel: 'Account', + workspaceId: ({ providerWorkspaceId }) => (providerWorkspaceId ?? '').trim(), + request: ({ apiToken, providerWorkspaceId = '' }) => ({ + url: `https://${providerWorkspaceId}.api.example.com/me`, + headers: { Authorization: `Bearer ${apiToken}` }, + }), + }) + vi.stubGlobal( + 'fetch', + vi.fn().mockRejectedValue(Object.assign(new TypeError('fetch failed'), { code: 'ENOTFOUND' })) + ) + + await expect( + accountHost({ apiToken: 'good', providerWorkspaceId: 'typo' }) + ).rejects.toMatchObject({ + code: 'site_not_found', + }) + }) + + it('keeps an error body that echoes the token out of the error details', async () => { + const echoing = tokenCheckValidator({ + serviceLabel: 'Echo', + errorBodyEchoesToken: true, + request: ({ apiToken }) => ({ + url: 'https://api.example.com/me', + headers: { Token: apiToken }, + }), + }) + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(jsonResponse({ error: 'invalid token tok-secret' }, 401)) + ) + + const error = await echoing({ apiToken: 'tok-secret' }).catch((caught: unknown) => caught) + + expect(error).toMatchObject({ code: 'invalid_credentials' }) + expect(JSON.stringify(error)).not.toContain('tok-secret') + }) +}) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/http.ts b/apps/sim/lib/credentials/token-service-accounts/validators/http.ts new file mode 100644 index 00000000000..e31c2be069e --- /dev/null +++ b/apps/sim/lib/credentials/token-service-accounts/validators/http.ts @@ -0,0 +1,331 @@ +import { truncate } from '@sim/utils/string' +import { EgressRefusedError } from '@/lib/core/network/transport.server' +import type { EgressProfile } from '@/lib/core/security/egress/profiles' +import { + secureFetchWithPinnedIP, + validateUrlWithDNS, +} from '@/lib/core/security/input-validation.server' +import { readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' +import { + fetchProvider, + isHostNotFound, + type ProviderResponse, + providerFailureCode, + TokenServiceAccountValidationError, + throwForProviderResponse, +} from '@/lib/credentials/token-service-accounts/errors' +import type { + TokenServiceAccountFields, + TokenServiceAccountValidationResult, + TokenServiceAccountValidator, +} from '@/lib/credentials/token-service-accounts/server' + +interface TokenCheckRequest { + url: string + method?: 'GET' | 'POST' + headers: Record + body?: string +} + +interface TokenCheck { + /** Service name used as the default display name. */ + serviceLabel: string + /** + * For a credential whose domain is part of the connection (a host, instance + * URL, or region): normalizes what was typed into exactly the form the tools + * use, throwing for anything they couldn't safely use. The result is what + * {@link request} sees and what the credential stores, so runs reach only the + * verified host. + */ + domain?: (fields: TokenServiceAccountFields) => string + /** + * For a credential whose account id is part of the connection (an org slug, + * account SID): returns it trimmed, throwing for a value that could steer the + * request's path or host. + */ + workspaceId?: (fields: TokenServiceAccountFields) => string + /** + * The request goes to a host the user chose (self-hosted or white-label), so + * it runs through the same SSRF-guarded egress profile as the tools instead + * of a plain fetch. + */ + egress?: Extract + /** The vendor echoes the token in its error body, so that body never reaches logs or error details. */ + errorBodyEchoesToken?: boolean + /** + * A cheap documented read that succeeds only with a valid token — the same + * host and auth scheme the service's tools use. `fields` carries the + * normalized domain and account id. + */ + request: (fields: TokenServiceAccountFields) => TokenCheckRequest + /** + * The vendor answers 403 to a valid key that lacks this endpoint's scope, so + * the vendor's JSON 403 still proves the key authenticated. A vendor that + * also answers an unknown key with 403 must set {@link rejectsKey} to tell + * the two apart. + */ + forbiddenMeansScoped?: boolean + /** + * For a vendor that reports a rejected key inside a successful response (a + * GraphQL `errors` array): returns whether the parsed body proves the key, or + * a display name read from it (a GitLab token's name) when it does. + */ + acceptsBody?: (body: unknown) => boolean | { displayName: string } + /** + * For a vendor whose error status alone doesn't say the key is bad (Fireflies + * returns 500 with `auth_failed`; Brex returns its scope-error 403 for an + * unknown key): returns whether the error body names a bad key. Checked + * before {@link forbiddenMeansScoped} accepts a 403. + */ + rejectsKey?: (errorBody: unknown) => boolean + /** + * For a vendor that answers a valid but restricted key with an error naming + * the missing permission (Resend's `restricted_api_key` 401): returns whether + * the error body proves the key authenticated. Checked after + * {@link rejectsKey}. + */ + provesKey?: (errorBody: unknown) => boolean +} + +const MAX_CHECK_RESPONSE_BYTES = 64 * 1024 +/** The guarded fetch refuses a larger declared body outright; JSON reads stay capped at {@link MAX_CHECK_RESPONSE_BYTES}. */ +const MAX_EGRESS_RESPONSE_BYTES = 1024 * 1024 + +/** Reads a credential check's JSON response, capped so a hostile body can't exhaust memory. */ +export function readJsonBody(response: ProviderResponse, serviceLabel: string): Promise { + return readResponseJsonWithLimit(response, { + maxBytes: MAX_CHECK_RESPONSE_BYTES, + label: `${serviceLabel} credential check`, + }) +} + +/** + * Drops the tabs and newlines the URL parser ignores, then refuses `https:/host`, which + * would otherwise get a second scheme and parse as a host named `https`. + */ +function refuseMalformedScheme(typed: string): string { + const stripped = typed.replace(/[\t\n\r]/g, '') + if (/^https?:(?!\/\/)/i.test(stripped)) throw new Error('malformed scheme') + return stripped +} + +/** + * A typed http(s) base URL in the form tools build request paths on: a bare host + * is taken as https, trailing slashes are dropped, and any other scheme, + * credentials, a query, or a fragment is refused, since each would retarget the + * request. + */ +export function normalizeHttpBaseUrl(raw: string | undefined): string { + const trimmed = (raw ?? '').trim() + if (!trimmed) throw new Error('missing URL') + const typed = refuseMalformedScheme(trimmed) + const url = new URL(/^[a-z][a-z\d+.-]*:\/\//i.test(typed) ? typed : `https://${typed}`) + if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error('http(s) only') + if (url.username || url.password || url.search || url.hash) { + throw new Error('credentials, query, or fragment in URL') + } + return `${url.origin}${url.pathname.replace(/\/+$/, '')}` +} + +/** + * Refuses a typed URL that carries credentials or a non-http(s) scheme, before a + * tool's own normalizer can drop the credentials silently or read the scheme as + * a host, and so verify a different identity or host than the user meant. + */ +export function refuseUnsafeTypedUrl(raw: string | undefined): string { + const typed = refuseMalformedScheme((raw ?? '').trim()) + if (/^(?!https?:\/\/)[a-z][a-z\d+.-]*:\/\//i.test(typed)) throw new Error('http(s) only') + const authority = typed.replace(/^[a-z][a-z\d+.-]*:\/\//i, '').split(/[/?#]/)[0] + if (authority.includes('@')) throw new Error('credentials in URL') + return typed +} + +/** A domain that must be one of a fixed set (a region or regional host), compared case-insensitively. */ +export function allowedDomain(allowed: ReadonlySet) { + return ({ domain }: TokenServiceAccountFields): string => { + const value = (domain ?? '').trim().toLowerCase() + if (!allowed.has(value)) throw new Error('unknown region') + return value + } +} + +/** Runs `normalize` on a typed connection field, rejecting what it refuses as the user's input. */ +function normalizedField( + normalize: ((fields: TokenServiceAccountFields) => string) | undefined, + fields: TokenServiceAccountFields, + field: 'domain' | 'account id' +): string | undefined { + if (!normalize) return undefined + try { + return normalize(fields) + } catch { + throw new TokenServiceAccountValidationError('invalid_credentials', 400, { + step: 'verify', + reason: `invalid ${field}`, + }) + } +} + +const hostNotAllowed = () => + new TokenServiceAccountValidationError('host_not_allowed', 400, { + step: 'verify', + reason: 'host not allowed', + }) + +/** + * Sends a check through the guarded egress profile. A host the policy refuses + * (a private address, or plain http where https is required) is the user's + * input, so it fails as `host_not_allowed` rather than `provider_unavailable`, + * which renders as a retryable 503. A host that doesn't exist is `site_not_found`. + */ +async function sendThroughEgress( + profile: NonNullable, + { url, method = 'GET', headers, body }: TokenCheckRequest +): Promise { + // The transport applies the stricter every-address rule itself when it routes through an + // environment proxy; its refusal is classified below, so a gateway route is not pre-judged. + const validation = await validateUrlWithDNS(url, 'url', profile, { logDetails: false }) + if (!validation.isValid) { + if (validation.refusal !== 'dns') throw hostNotAllowed() + const notFound = isHostNotFound(validation.cause) + throw new TokenServiceAccountValidationError( + notFound ? 'site_not_found' : 'provider_unavailable', + notFound ? 400 : 502, + { step: 'verify', reason: notFound ? 'host does not resolve' : 'host lookup failed' } + ) + } + try { + return await secureFetchWithPinnedIP(url, validation.resolvedIP, { + profile, + method, + headers, + body, + timeout: 10_000, + maxRedirects: 0, + maxResponseBytes: MAX_EGRESS_RESPONSE_BYTES, + logUrlValidationDetails: false, + }) + } catch (error) { + // The proxy route judges the host again; its answer may have changed since the check above. + if (error instanceof EgressRefusedError) throw hostNotAllowed() + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'host unreachable', + }) + } +} + +/** + * Validator for a service whose token is accepted or rejected by one + * authenticated request: 2xx accepts, a non-transient 4xx rejects the token, + * anything else is the provider being unavailable. Redirects are refused, so a + * token is never forwarded to another host. + */ +export function tokenCheckValidator(check: TokenCheck): TokenServiceAccountValidator { + return async (typed): Promise => { + const domain = normalizedField(check.domain, typed, 'domain') + const workspaceId = normalizedField(check.workspaceId, typed, 'account id') + const fields = { + ...typed, + ...(domain === undefined ? {} : { domain }), + ...(workspaceId === undefined ? {} : { providerWorkspaceId: workspaceId }), + } + const where = domain ?? workspaceId + const accepted: TokenServiceAccountValidationResult = { + displayName: where ? `${check.serviceLabel} (${where})` : check.serviceLabel, + principal: null, + auditMetadata: { + ...(domain === undefined ? {} : { domain }), + ...(workspaceId === undefined ? {} : { accountId: workspaceId }), + }, + ...(domain === undefined ? {} : { normalizedDomain: domain }), + ...(workspaceId === undefined ? {} : { normalizedWorkspaceId: workspaceId }), + } + const request = check.request(fields) + const response: ProviderResponse = check.egress + ? await sendThroughEgress(check.egress, request) + : await fetchProvider( + request.url, + { + method: request.method ?? 'GET', + headers: request.headers, + body: request.body, + redirect: 'error', + }, + 'verify', + // A host built from the typed domain or account id that doesn't exist is the user's typo. + domain === undefined && workspaceId === undefined + ? undefined + : { dnsFailureCode: 'site_not_found' } + ) + // A JSON 403 is the vendor's own answer; an HTML one is a firewall or CDN page. + const scopedForbidden = + check.forbiddenMeansScoped && + response.status === 403 && + (response.headers.get('content-type') ?? '').includes('application/json') + if ((check.rejectsKey || check.provesKey) && !response.ok) { + const unreadable = Symbol('unreadable') + const errorBody = await readJsonBody(response, check.serviceLabel).catch(() => unreadable) + if (errorBody !== unreadable) { + if (check.rejectsKey?.(errorBody)) { + throw new TokenServiceAccountValidationError('invalid_credentials', 400, { + step: 'verify', + }) + } + if (check.provesKey?.(errorBody)) return accepted + } + if (scopedForbidden) { + // Only a body that was read and doesn't name a bad key proves the token. + if (errorBody === unreadable) { + throw new TokenServiceAccountValidationError('provider_unavailable', response.status, { + step: 'verify', + reason: 'unreadable 403 body', + }) + } + return accepted + } + throw new TokenServiceAccountValidationError( + providerFailureCode(response.status), + response.status, + { + step: 'verify', + body: + errorBody === unreadable || check.errorBodyEchoesToken + ? '' + : truncate(JSON.stringify(errorBody) ?? '', 500), + } + ) + } + if (!scopedForbidden && !response.ok) { + if (!check.errorBodyEchoesToken) await throwForProviderResponse(response, 'verify') + // The body echoes the token, so it is dropped unread. + await response.body?.cancel().catch(() => undefined) + throw new TokenServiceAccountValidationError( + providerFailureCode(response.status), + response.status, + { step: 'verify' } + ) + } + if (check.acceptsBody && response.ok) { + let body: unknown + try { + body = await readJsonBody(response, check.serviceLabel) + } catch { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + }) + } + const verdict = check.acceptsBody(body) + if (!verdict) { + throw new TokenServiceAccountValidationError('invalid_credentials', 400, { step: 'verify' }) + } + if (typeof verdict === 'object') { + const name = verdict.displayName + return { ...accepted, displayName: where ? `${name} (${where})` : name } + } + return accepted + } + await response.body?.cancel().catch(() => undefined) + return accepted + } +} diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/snowflake.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/snowflake.test.ts index 3555dfb8df8..adce4905ef4 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/snowflake.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/snowflake.test.ts @@ -1,7 +1,18 @@ import { jsonResponse } from '@sim/testing/helpers/http' +import { + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing/mocks/input-validation.mock' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { EgressRefusedError } from '@/lib/core/network/transport.server' import { TokenServiceAccountValidationError } from '@/lib/credentials/token-service-accounts/errors' -import { validateSnowflakeServiceAccount } from '@/lib/credentials/token-service-accounts/validators/snowflake' +import { + validateSnowflakeServiceAccount, + validateSnowflakeWorkloadIdentity, +} from '@/lib/credentials/token-service-accounts/validators/snowflake' const mockFetch = vi.fn() @@ -76,3 +87,33 @@ describe('validateSnowflakeServiceAccount', () => { expect(result.auditMetadata).toEqual({ account: 'MYORG-MYACCOUNT' }) }) }) + +describe('validateSnowflakeWorkloadIdentity', () => { + afterEach(() => { + vi.resetAllMocks() + }) + + it("reports an account host the egress policy refuses as the user's input, not an outage", async () => { + inputValidationMockFns.mockSecureFetchWithValidation.mockRejectedValueOnce( + new EgressRefusedError('private address') + ) + + await expectCode( + validateSnowflakeWorkloadIdentity({ domain: fields.domain, subjectToken: 'signed-token' }), + 'host_not_allowed', + 400 + ) + }) + + it('still reports an unreachable account host as a provider outage', async () => { + inputValidationMockFns.mockSecureFetchWithValidation.mockRejectedValueOnce( + new Error('connect timeout') + ) + + await expectCode( + validateSnowflakeWorkloadIdentity({ domain: fields.domain, subjectToken: 'signed-token' }), + 'provider_unavailable', + 502 + ) + }) +}) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/snowflake.ts b/apps/sim/lib/credentials/token-service-accounts/validators/snowflake.ts index 1b7a0271cdc..72966f2ea08 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/snowflake.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/snowflake.ts @@ -1,6 +1,10 @@ +import { EgressRefusedError } from '@/lib/core/network/transport.server' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { tenantPrincipal, userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, + isHostNotFound, + type ProviderResponse, parseProviderJson, readProviderErrorSnippet, TokenServiceAccountValidationError, @@ -10,7 +14,11 @@ import type { TokenServiceAccountFields, TokenServiceAccountValidationResult, } from '@/lib/credentials/token-service-accounts/server' -import { buildSnowflakeAuthHeaders, normalizeSnowflakeHost } from '@/tools/snowflake/utils' +import { + buildSnowflakeAuthHeaders, + normalizeSnowflakeHost, + SNOWFLAKE_WORKLOAD_IDENTITY_AUTH_STYLE, +} from '@/tools/snowflake/utils' /** * Context functions only — they resolve from the session the token opens and @@ -25,6 +33,9 @@ const IDENTITY_STATEMENT = 'SELECT CURRENT_USER(), CURRENT_ACCOUNT(), CURRENT_RO * the user sees a statement error rather than "network error reaching provider". */ const IDENTITY_STATEMENT_TIMEOUT_SECONDS = 5 +/** Matches `fetchProvider`'s abort on the PAT path. */ +const IDENTITY_FETCH_TIMEOUT_MS = 10_000 +const IDENTITY_MAX_RESPONSE_BYTES = 1024 * 1024 interface SnowflakeStatementApiResponse { message?: string @@ -37,49 +48,49 @@ function cell(row: Array | undefined, index: number): string | un return typeof value === 'string' && value.trim() ? value.trim() : undefined } -/** - * Validates a Snowflake programmatic access token by running a context-only - * statement against the account's SQL API — the same endpoint, headers, and - * host normalization every Sim Snowflake tool uses, so a passing validation - * proves the credential works with the tools as written. - * - * `normalizeSnowflakeHost` is the SSRF guard: it rejects anything that is not - * a bare `*.snowflakecomputing.com`/`.cn` hostname before any outbound fetch. - * - * Snowflake wildcard-resolves `*.snowflakecomputing.com`, so an account that - * does not exist still resolves and answers **404** rather than failing DNS — - * that 404 is what identifies a mistyped host, and without the branch below it - * would be blamed on the provider forever. The DNS mapping is kept only as a - * defensive fallback for hosts outside the wildcard. - */ -export async function validateSnowflakeServiceAccount( - fields: TokenServiceAccountFields -): Promise { - let baseUrl: string +/** Sends the identity statement to the account; the PAT and keyless paths differ only here. */ +type SendIdentityStatement = ( + url: string, + init: { headers: Record; body: string } +) => Promise }> + +/** The account origin, or `site_not_found` before any request when the host is not a Snowflake account host. */ +function accountBaseUrl(domain: string | undefined): string { try { - baseUrl = normalizeSnowflakeHost(fields.domain ?? '') + return normalizeSnowflakeHost(domain ?? '') } catch { throw new TokenServiceAccountValidationError('site_not_found', 400, { step: 'host_validation', - domain: fields.domain, + domain, reason: 'host is not a Snowflake account hostname', }) } +} + +/** + * Runs the context-only identity statement and reads who the token signed in + * as. Shared by both credential modes so they classify Snowflake's answers the + * same way. + * + * Snowflake wildcard-resolves `*.snowflakecomputing.com`, so an account that + * does not exist still resolves and answers **404** rather than failing DNS — + * that 404 is what identifies a mistyped host, and without the branch below it + * would be blamed on the provider forever. + */ +async function verifySnowflakeIdentity( + baseUrl: string, + headers: Record, + send: SendIdentityStatement +): Promise { const domain = new URL(baseUrl).hostname - const res = await fetchProvider( - `${baseUrl}/api/v2/statements`, - { - method: 'POST', - headers: buildSnowflakeAuthHeaders(fields.apiToken), - body: JSON.stringify({ - statement: IDENTITY_STATEMENT, - timeout: IDENTITY_STATEMENT_TIMEOUT_SECONDS, - }), - }, - 'identity_statement', - { dnsFailureCode: 'site_not_found', dnsFailureReason: 'account host does not resolve' } - ) + const res = await send(`${baseUrl}/api/v2/statements`, { + headers, + body: JSON.stringify({ + statement: IDENTITY_STATEMENT, + timeout: IDENTITY_STATEMENT_TIMEOUT_SECONDS, + }), + }) // A wrong account host resolves through Snowflake's wildcard DNS and answers // 404, so it must be reported as a bad host rather than a provider outage. @@ -125,7 +136,7 @@ export async function validateSnowflakeServiceAccount( const metadata = { account, ...(role ? { role } : {}) } return { displayName: user ? `${user} (${account})` : account, - // A PAT belongs to one Snowflake user, so the user is the finest identity + // A token belongs to one Snowflake user, so the user is the finest identity // available; accounts that mask it fall back to the account itself. The // account is carried in metadata rather than the principal's label slot, // which names the human actor. @@ -135,3 +146,75 @@ export async function validateSnowflakeServiceAccount( normalizedDomain: domain, } } + +/** + * Validates a Snowflake programmatic access token by running a context-only + * statement against the account's SQL API — the same endpoint, headers, and + * host normalization every Sim Snowflake tool uses, so a passing validation + * proves the credential works with the tools as written. + * + * `normalizeSnowflakeHost` is the SSRF guard: it rejects anything that is not + * a bare `*.snowflakecomputing.com`/`.cn` hostname before any outbound fetch. + * The DNS mapping is kept only as a defensive fallback for hosts outside + * Snowflake's wildcard. + */ +export async function validateSnowflakeServiceAccount( + fields: TokenServiceAccountFields +): Promise { + return verifySnowflakeIdentity( + accountBaseUrl(fields.domain), + buildSnowflakeAuthHeaders(fields.apiToken), + (url, init) => + fetchProvider(url, { method: 'POST', ...init }, 'identity_statement', { + dnsFailureCode: 'site_not_found', + dnsFailureReason: 'account host does not resolve', + }) + ) +} + +/** + * Verifies a keyless Snowflake connection: the token Sim signed for the + * credential's subject is presented as workload identity federation at the + * account host the credential pins, through the guarded egress path, so it can + * neither be redirected nor reach a private address. + */ +export async function validateSnowflakeWorkloadIdentity(input: { + domain: string + subjectToken: string +}): Promise { + return verifySnowflakeIdentity( + accountBaseUrl(input.domain), + buildSnowflakeAuthHeaders(input.subjectToken, SNOWFLAKE_WORKLOAD_IDENTITY_AUTH_STYLE), + async (url, init) => { + try { + return await secureFetchWithValidation(url, { + profile: 'configuredEndpoint', + method: 'POST', + headers: init.headers, + body: init.body, + timeout: IDENTITY_FETCH_TIMEOUT_MS, + maxRedirects: 0, + maxResponseBytes: IDENTITY_MAX_RESPONSE_BYTES, + logUrlValidationDetails: false, + }) + } catch (error) { + // A host the egress policy refuses (a private address) is the user's input, not an outage. + if (error instanceof EgressRefusedError) { + throw new TokenServiceAccountValidationError('host_not_allowed', 400, { + step: 'identity_statement', + reason: 'account host not allowed', + }) + } + const notFound = isHostNotFound(error) + throw new TokenServiceAccountValidationError( + notFound ? 'site_not_found' : 'provider_unavailable', + notFound ? 400 : 502, + { + step: 'identity_statement', + reason: notFound ? 'account host does not resolve' : 'account host unreachable', + } + ) + } + } + ) +} diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/token-checks.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/token-checks.test.ts new file mode 100644 index 00000000000..0e5ccc3b1bc --- /dev/null +++ b/apps/sim/lib/credentials/token-service-accounts/validators/token-checks.test.ts @@ -0,0 +1,451 @@ +import { jsonResponse } from '@sim/testing/helpers/http' +import { + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing/mocks/input-validation.mock' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { + validateAlgoliaServiceAccount, + validateAshbyServiceAccount, + validateAzureDevOpsServiceAccount, + validateBrexServiceAccount, + validateConvexServiceAccount, + validateDevinServiceAccount, + validateDynatraceServiceAccount, + validateElevenLabsServiceAccount, + validateEmailBisonServiceAccount, + validateFlintServiceAccount, + validateGrafanaServiceAccount, + validateInfisicalServiceAccount, + validateInstantlyServiceAccount, + validateJotformServiceAccount, + validateJupyterServiceAccount, + validateLogfireServiceAccount, + validateMailgunServiceAccount, + validateNewRelicServiceAccount, + validateOktaServiceAccount, + validatePlaneServiceAccount, + validateResendServiceAccount, + validateSupabaseServiceAccount, + validateThriveServiceAccount, + validateTinybirdServiceAccount, + validateTwilioServiceAccount, + validateTypeformServiceAccount, + validateUpstashServiceAccount, + validateVercelServiceAccount, + validateYouTubeServiceAccount, +} from '@/lib/credentials/token-service-accounts/validators/token-checks' + +describe('Flint saved API key verification', () => { + it("accepts a key Flint answers with its own 'Task not found' for an unknown task", async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(jsonResponse({ error: 'Task not found' }, 404)) + ) + + await expect(validateFlintServiceAccount({ apiToken: 'ak_valid' })).resolves.toMatchObject({ + displayName: 'Flint', + }) + }) + + it('rejects a key Flint refuses before the task lookup', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(new Response('Authentication Error', { status: 401 })) + ) + + await expect(validateFlintServiceAccount({ apiToken: 'bad' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) + + it('does not treat a proxy 404 page as proof of the key', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + new Response('Not Found', { + status: 404, + headers: { 'Content-Type': 'text/html' }, + }) + ) + ) + + await expect(validateFlintServiceAccount({ apiToken: 'any' })).rejects.toMatchObject({ + code: 'provider_unavailable', + }) + }) +}) + +describe('Brex saved token verification', () => { + it('rejects an unknown token Brex answers with its scope-style 403', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse({ type: 'FORBIDDEN', message: 'Invalid or Revoked Token' }, 403) + ) + ) + + await expect(validateBrexServiceAccount({ apiToken: 'bxt_unknown' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) + + it('does not accept a 403 whose body cannot be read to tell a bad token apart', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + new Response('{"message": "Invalid or Revoked', { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }) + ) + ) + + await expect(validateBrexServiceAccount({ apiToken: 'bxt_unknown' })).rejects.toMatchObject({ + code: 'provider_unavailable', + }) + }) + + it('accepts a valid token that only lacks the user scope', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse({ type: 'FORBIDDEN', message: 'Insufficient scope for this request' }, 403) + ) + ) + + await expect(validateBrexServiceAccount({ apiToken: 'bxt_scoped' })).resolves.toMatchObject({ + displayName: 'Brex', + }) + }) +}) + +describe('saved key verification for vendors with scoped keys', () => { + it('accepts a valid Instantly key that lacks the campaigns scope', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(jsonResponse({ message: 'Insufficient scope' }, 403)) + ) + + await expect(validateInstantlyServiceAccount({ apiToken: 'leads-only' })).resolves.toBeTruthy() + }) + + it('accepts a valid YouTube key that is out of quota for today', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse({ error: { code: 403, errors: [{ reason: 'quotaExceeded' }] } }, 403) + ) + ) + + await expect(validateYouTubeServiceAccount({ apiToken: 'AIza-valid' })).resolves.toBeTruthy() + }) + + it('rejects a YouTube key whose project has the API turned off', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse({ error: { code: 403, errors: [{ reason: 'accessNotConfigured' }] } }, 403) + ) + ) + + await expect(validateYouTubeServiceAccount({ apiToken: 'AIza-off' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) +}) + +describe('Ashby saved API key verification', () => { + it('accepts a valid key that lacks the read permission for this endpoint', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse({ success: false, errorInfo: { code: 'missing_endpoint_permission' } }, 403) + ) + ) + + await expect(validateAshbyServiceAccount({ apiToken: 'scoped' })).resolves.toMatchObject({ + displayName: 'Ashby', + }) + }) + + it('accepts a valid key whose missing permission Ashby names in its errors list', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse( + { success: false, errors: [{ message: 'missing_endpoint_permission' }] }, + 403 + ) + ) + ) + + await expect(validateAshbyServiceAccount({ apiToken: 'scoped' })).resolves.toMatchObject({ + displayName: 'Ashby', + }) + }) + + it('rejects a deactivated key Ashby answers with a 403', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse({ success: false, errorInfo: { code: 'invalid_api_key' } }, 403) + ) + ) + + await expect(validateAshbyServiceAccount({ apiToken: 'revoked' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) +}) + +describe('Resend saved API key verification', () => { + it('accepts a sending-only key Resend refuses for listing domains', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue(jsonResponse({ name: 'restricted_api_key', message: 'restricted' }, 401)) + ) + + await expect(validateResendServiceAccount({ apiToken: 're_send' })).resolves.toMatchObject({ + displayName: 'Resend', + }) + }) + + it('rejects an unknown key', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse({ name: 'validation_error', message: 'API key is invalid' }, 401) + ) + ) + + await expect(validateResendServiceAccount({ apiToken: 're_bad' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) +}) + +describe('Typeform saved token verification', () => { + it('rejects an unknown token Typeform answers with a 403', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(jsonResponse({ code: 'AUTHENTICATION_FAILED' }, 403)) + ) + + await expect(validateTypeformServiceAccount({ apiToken: 'bad' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) +}) + +describe('Vercel saved token verification', () => { + it('rejects an unknown token Vercel flags as invalid in its 403', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue(jsonResponse({ error: { code: 'forbidden', invalidToken: true } }, 403)) + ) + + await expect(validateVercelServiceAccount({ apiToken: 'bad' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) + + it('accepts a valid token without access to the user record', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(jsonResponse({ error: { code: 'forbidden' } }, 403)) + ) + + await expect(validateVercelServiceAccount({ apiToken: 'scoped' })).resolves.toMatchObject({ + displayName: 'Vercel', + }) + }) +}) + +describe('ElevenLabs saved API key verification', () => { + it('accepts a valid key without the user permission', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse({ detail: { status: 'missing_permissions', message: 'user_read' } }, 401) + ) + ) + + await expect( + validateElevenLabsServiceAccount({ apiToken: 'sk_scoped' }) + ).resolves.toMatchObject({ displayName: 'ElevenLabs' }) + }) + + it('rejects an unknown key', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + jsonResponse( + { detail: { status: 'invalid_api_key', message: 'API key is invalid.' } }, + 400 + ) + ) + ) + + await expect(validateElevenLabsServiceAccount({ apiToken: 'sk_bad' })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + }) +}) + +describe('saved credentials whose connection names a host, region, or account', () => { + it.each([ + ['Algolia', validateAlgoliaServiceAccount, { providerWorkspaceId: 'evil.example.com/' }], + ['Azure DevOps', validateAzureDevOpsServiceAccount, { providerWorkspaceId: 'org/../other' }], + ['Devin', validateDevinServiceAccount, { providerWorkspaceId: 'org/sessions?x' }], + ['Jotform', validateJotformServiceAccount, { domain: 'evil' }], + ['Mailgun', validateMailgunServiceAccount, { domain: 'mars' }], + ['New Relic', validateNewRelicServiceAccount, { domain: 'mars' }], + ['Okta', validateOktaServiceAccount, { domain: 'evil.example.com' }], + ['Supabase', validateSupabaseServiceAccount, { domain: 'ref.evil.example.com' }], + [ + 'Thrive', + validateThriveServiceAccount, + { domain: 'evil.example.com', providerWorkspaceId: 't1' }, + ], + ['Twilio', validateTwilioServiceAccount, { providerWorkspaceId: 'AC123/../Messages' }], + ])( + '%s refuses a typed value that could steer the request, before sending the key', + async (_, validate, typed) => { + const fetch = vi.fn() + vi.stubGlobal('fetch', fetch) + + await expect(validate({ apiToken: 'secret', ...typed })).rejects.toMatchObject({ + code: 'invalid_credentials', + }) + expect(fetch).not.toHaveBeenCalled() + } + ) + + it('rejects an Azure DevOps key that a public organization answers as an anonymous visitor', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + jsonResponse({ + authenticatedUser: { + id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + descriptor: 'System:PublicAccess;aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + }, + }) + ) + ) + + await expect( + validateAzureDevOpsServiceAccount({ apiToken: 'bad', providerWorkspaceId: 'public-org' }) + ).rejects.toMatchObject({ code: 'invalid_credentials' }) + }) +}) + +describe('saved credentials for a host the user chose', () => { + const HOST_CHOSEN_VALIDATORS = [ + ['Convex', validateConvexServiceAccount], + ['Dynatrace', validateDynatraceServiceAccount], + ['Email Bison', validateEmailBisonServiceAccount], + ['Grafana', validateGrafanaServiceAccount], + ['Infisical', validateInfisicalServiceAccount], + ['Jupyter', validateJupyterServiceAccount], + ['Logfire', validateLogfireServiceAccount], + ['Plane', validatePlaneServiceAccount], + ['Tinybird', validateTinybirdServiceAccount], + ['Upstash', validateUpstashServiceAccount], + ] as const + + it.each(HOST_CHOSEN_VALIDATORS)( + '%s refuses credentials smuggled into the URL before sending the key anywhere', + async (_, validate) => { + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockReset() + + await expect( + validate({ apiToken: 'secret', domain: 'https://user:pass@instance.example.com' }) + ).rejects.toMatchObject({ code: 'invalid_credentials' }) + expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + } + ) + + it.each(HOST_CHOSEN_VALIDATORS)( + '%s refuses a non-http scheme instead of reading it as a host', + async (_, validate) => { + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockReset() + + await expect( + validate({ apiToken: 'secret', domain: 'ftp://instance.example.com' }) + ).rejects.toMatchObject({ code: 'invalid_credentials' }) + expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + } + ) +}) + +describe('saved credentials whose vendor answers a restricted token in its error', () => { + const secureFetch = inputValidationMockFns.mockSecureFetchWithPinnedIP + + it.each([ + [ + 'rejects an unknown Tinybird token', + validateTinybirdServiceAccount, + jsonResponse({ error: 'invalid authentication token. Token tok not found' }, 403), + 'invalid_credentials', + ], + [ + 'accepts a Tinybird token without read access', + validateTinybirdServiceAccount, + jsonResponse({ error: 'Not enough permissions to read from datasources' }, 403), + null, + ], + [ + 'accepts an Upstash token whose ACL refuses PING', + validateUpstashServiceAccount, + jsonResponse({ error: 'NOPERM this user has no permissions to run the ping command' }, 400), + null, + ], + [ + 'rejects an unknown Upstash token', + validateUpstashServiceAccount, + jsonResponse({ error: 'WRONGPASS invalid or missing auth token' }, 401), + 'invalid_credentials', + ], + ])('%s', async (_, validate, reply, code) => { + inputValidationMockFns.mockValidateUrlWithDNS.mockReset().mockResolvedValue({ + isValid: true, + resolvedIP: '203.0.113.7', + originalHostname: 'instance.example.com', + }) + secureFetch.mockReset().mockResolvedValue(reply) + + const result = validate({ apiToken: 'tok', domain: 'https://instance.example.com' }) + + if (code) await expect(result).rejects.toMatchObject({ code }) + else await expect(result).resolves.toMatchObject({ principal: null }) + }) +}) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/token-checks.ts b/apps/sim/lib/credentials/token-service-accounts/validators/token-checks.ts new file mode 100644 index 00000000000..e4f57a094e4 --- /dev/null +++ b/apps/sim/lib/credentials/token-service-accounts/validators/token-checks.ts @@ -0,0 +1,1572 @@ +import { toArray, toRecord } from '@sim/utils/object' +import { validateOktaDomain, validateSupabaseProjectId } from '@/lib/core/security/input-validation' +import { + fetchProvider, + TokenServiceAccountValidationError, + throwForProviderResponse, +} from '@/lib/credentials/token-service-accounts/errors' +import type { + TokenServiceAccountFields, + TokenServiceAccountValidationResult, +} from '@/lib/credentials/token-service-accounts/server' +import { + allowedDomain, + normalizeHttpBaseUrl, + readJsonBody, + refuseUnsafeTypedUrl, + tokenCheckValidator, +} from '@/lib/credentials/token-service-accounts/validators/http' +import { buildJupyterAuthHeaders, normalizeJupyterServerUrl } from '@/lib/internal/jupyter/protocol' +import { ashbyAuthHeaders } from '@/tools/ashby/utils' +import { BITBUCKET_API_BASE, bitbucketHeaders } from '@/tools/bitbucket/utils' +import { checkrHeaders } from '@/tools/checkr/utils' +import { convexApiUrl, convexAuthHeaders } from '@/tools/convex/utils' +import { buildDynatraceUrl, dynatraceHeaders } from '@/tools/dynatrace/utils' +import { normalizeEmailBisonBaseUrl } from '@/tools/emailbison/utils' +import { normalizeGitLabHost } from '@/tools/gitlab/utils' +import { GRANOLA_API_BASE, granolaHeaders } from '@/tools/granola/utils' +import { buildJotformHeaders, buildJotformUrl, JOTFORM_REGION_HOSTS } from '@/tools/jotform/utils' +import { + getLogfireBaseUrl, + LOGFIRE_READ_TOKEN_INFO_PATH, + logfireHeaders, +} from '@/tools/logfire/utils' +import type { MailgunRegion } from '@/tools/mailgun/types' +import { getMailgunApiBaseUrl } from '@/tools/mailgun/utils' +import { getNerdGraphEndpoint, newRelicHeaders } from '@/tools/new_relic/utils' +import { oktaHeaders } from '@/tools/okta/utils' +import { planeApiUrl, planeHeaders } from '@/tools/plane/utils' +import { supabaseBaseUrl } from '@/tools/supabase/utils' +import { getThriveBaseUrl, getThriveHeaders, THRIVE_HOSTS } from '@/tools/thrive/utils' +import { WHATSAPP_GRAPH_VERSION } from '@/tools/whatsapp/utils' + +/** + * Token checks for saved-credential providers whose key is verified by one + * documented, read-only request. Most are declarations for + * `tokenCheckValidator`; the few that need more (a host taken from the key, an + * unknown-id lookup) sit alongside. Kept in one module so each new provider + * costs a declaration, not a module, in every graph that verifies credentials. + */ + +/** Regions for vendors with one US and one EU API host. */ +const US_OR_EU: ReadonlySet = new Set(['us', 'eu']) + +/** Affinity's whoami endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateAffinityServiceAccount = tokenCheckValidator({ + serviceLabel: 'Affinity', + request: ({ apiToken }) => ({ + url: 'https://api.affinity.co/v2/auth/whoami', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Listing one AgentMail inbox answers 200 for a valid key and 403 otherwise. */ +export const validateAgentMailServiceAccount = tokenCheckValidator({ + serviceLabel: 'AgentMail', + request: ({ apiToken }) => ({ + url: 'https://api.agentmail.to/v0/inboxes?limit=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** AgentPhone's usage endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateAgentPhoneServiceAccount = tokenCheckValidator({ + serviceLabel: 'AgentPhone', + request: ({ apiToken }) => ({ + url: 'https://api.agentphone.to/v1/usage', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Ahrefs' limits-and-usage endpoint is free, answers 200 for a valid key and 401 otherwise. */ +export const validateAhrefsServiceAccount = tokenCheckValidator({ + serviceLabel: 'Ahrefs', + request: ({ apiToken }) => ({ + url: 'https://api.ahrefs.com/v3/subscription-info/limits-and-usage', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Algolia application IDs are alphanumeric; anything else could rewrite the tools' host. */ +const ALGOLIA_APPLICATION_ID = /^[A-Za-z0-9]{1,32}$/ + +/** + * Algolia's get-API-key endpoint lets any key read its own permissions, at the + * `https://{applicationId}.algolia.net` host the tools use: 200 with the key's `acl` + * for a valid key, a JSON 403 ("Invalid Application-ID or API key") otherwise. + */ +export const validateAlgoliaServiceAccount = tokenCheckValidator({ + serviceLabel: 'Algolia', + workspaceId: ({ providerWorkspaceId }) => { + const applicationId = (providerWorkspaceId ?? '').trim() + if (!ALGOLIA_APPLICATION_ID.test(applicationId)) { + throw new Error('Algolia application IDs contain only letters and numbers') + } + return applicationId + }, + request: ({ apiToken, providerWorkspaceId = '' }) => ({ + url: `https://${providerWorkspaceId}.algolia.net/1/keys/${encodeURIComponent(apiToken)}`, + headers: { + 'x-algolia-application-id': providerWorkspaceId, + 'x-algolia-api-key': apiToken, + Accept: 'application/json', + }, + }), + acceptsBody: (body) => { + if (!Array.isArray(toRecord(body).acl)) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Algolia returned an unexpected key lookup', + }) + } + return true + }, +}) + +/** Apify's current-user endpoint answers 200 for a valid token and 401 otherwise. */ +export const validateApifyServiceAccount = tokenCheckValidator({ + serviceLabel: 'Apify', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: 'https://api.apify.com/v2/users/me', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * A one-row contact search costs no credits, works without a master key, and + * answers 200 for a valid key and 401 otherwise (the health endpoint answers + * 200 even for a bad key, so it cannot be used). + */ +export const validateApolloServiceAccount = tokenCheckValidator({ + serviceLabel: 'Apollo', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: 'https://api.apollo.io/api/v1/contacts/search', + method: 'POST', + headers: { + 'X-Api-Key': apiToken, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ page: 1, per_page: 1 }), + }), +}) + +/** + * Listing Ashby candidate sources answers 200 for a valid key. Ashby refuses an + * unknown, wrong or deactivated key with 401 or 403; a valid key without the + * module's read permission gets an error naming `missing_endpoint_permission`, + * which still proves the key. + */ +export const validateAshbyServiceAccount = tokenCheckValidator({ + serviceLabel: 'Ashby', + request: ({ apiToken }) => ({ + url: 'https://api.ashbyhq.com/source.list', + method: 'POST', + headers: ashbyAuthHeaders(apiToken), + body: '{}', + }), + provesKey: (errorBody) => { + const { errorInfo, errors } = toRecord(errorBody) + const messages = toArray(errors).map((entry) => + typeof entry === 'string' ? entry : toRecord(entry).message + ) + return [toRecord(errorInfo).code, ...messages].includes('missing_endpoint_permission') + }, +}) + +/** Azure DevOps organization names: letters, digits, and hyphens, up to 50 characters. */ +const AZURE_DEVOPS_ORGANIZATION = /^[A-Za-z0-9][A-Za-z0-9-]{0,49}$/ +/** The identity Azure DevOps reports when a request carries no usable credential. */ +const AZURE_DEVOPS_ANONYMOUS_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + +/** + * Azure DevOps' connection-data endpoint (what the official clients call on connect) answers any + * PAT valid for the organization with its user, and 401 for an unknown PAT or one issued for + * another organization once the sign-in redirect is suppressed. A public organization answers a + * bad PAT with 200 and the anonymous identity, so the body must name a real user. + */ +export const validateAzureDevOpsServiceAccount = tokenCheckValidator({ + serviceLabel: 'Azure DevOps', + workspaceId: ({ providerWorkspaceId }) => { + const organization = (providerWorkspaceId ?? '').trim() + if (!AZURE_DEVOPS_ORGANIZATION.test(organization)) throw new Error('invalid organization') + return organization + }, + request: ({ apiToken, providerWorkspaceId }) => ({ + url: `https://dev.azure.com/${providerWorkspaceId}/_apis/connectionData`, + headers: { + Authorization: `Basic ${Buffer.from(`:${apiToken}`).toString('base64')}`, + 'X-TFS-FedAuthRedirect': 'Suppress', + Accept: 'application/json', + }, + }), + acceptsBody: (body) => { + const user = toRecord(toRecord(body).authenticatedUser) + return ( + typeof user.id === 'string' && + user.id.length > 0 && + user.id !== AZURE_DEVOPS_ANONYMOUS_ID && + !(typeof user.descriptor === 'string' && user.descriptor.startsWith('System:PublicAccess')) + ) + }, +}) + +/** + * Brex's current-user endpoint answers 200 for a valid user token. An unknown + * token gets the same JSON 403 as a token missing the user scope, told apart + * only by its message. + */ +export const validateBrexServiceAccount = tokenCheckValidator({ + serviceLabel: 'Brex', + forbiddenMeansScoped: true, + rejectsKey: (errorBody) => /invalid or revoked token/i.test(String(toRecord(errorBody).message)), + request: ({ apiToken }) => ({ + url: 'https://api.brex.com/v2/users/me', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Bright Data's active-zones listing answers 200 for a valid token and 401 otherwise. */ +export const validateBrightDataServiceAccount = tokenCheckValidator({ + serviceLabel: 'Bright Data', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: 'https://api.brightdata.com/zone/get_active_zones', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Browser Use's account billing endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateBrowserUseServiceAccount = tokenCheckValidator({ + serviceLabel: 'Browser Use', + request: ({ apiToken }) => ({ + url: 'https://api.browser-use.com/api/v2/billing/account', + headers: { 'X-Browser-Use-API-Key': apiToken, Accept: 'application/json' }, + }), +}) + +/** + * Buffer's GraphQL API can report a rejected key as an `UNAUTHORIZED` error in + * a 200 body, so the key is proven only when the account query returns an id. + */ +export const validateBufferServiceAccount = tokenCheckValidator({ + serviceLabel: 'Buffer', + request: ({ apiToken }) => ({ + url: 'https://api.buffer.com', + method: 'POST', + headers: { + Authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ query: 'query { account { id } }' }), + }), + acceptsBody: (body) => { + const account = toRecord(toRecord(toRecord(body).data).account) + return typeof account.id === 'string' && account.id.length > 0 + }, +}) + +/** + * Calendly's current-user endpoint answers 200 for a valid token and 401 + * otherwise; a valid token without the `users:read` scope gets a JSON 403. + */ +export const validateCalendlyServiceAccount = tokenCheckValidator({ + serviceLabel: 'Calendly', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: 'https://api.calendly.com/users/me', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Checkr's account endpoint answers 200 for a valid production secret key and 401 otherwise. */ +export const validateCheckrServiceAccount = tokenCheckValidator({ + serviceLabel: 'Checkr', + request: ({ apiToken }) => ({ + url: 'https://api.checkr.com/v1/account', + headers: checkrHeaders(apiToken), + }), +}) + +/** Listing Circleback tags answers 200 for a valid key and 401 otherwise. */ +export const validateCirclebackServiceAccount = tokenCheckValidator({ + serviceLabel: 'Circleback', + request: ({ apiToken }) => ({ + url: 'https://circleback.ai/api/tag', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Clerk's Backend API lists users only for a valid secret key and answers 401 otherwise. */ +export const validateClerkServiceAccount = tokenCheckValidator({ + serviceLabel: 'Clerk', + request: ({ apiToken }) => ({ + url: 'https://api.clerk.com/v1/users?limit=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Listing accounts succeeds for any valid user- or account-owned API token + * (`/user/tokens/verify` rejects account-owned ones) and fails for a bad token. + */ +export const validateCloudflareServiceAccount = tokenCheckValidator({ + serviceLabel: 'Cloudflare', + request: ({ apiToken }) => ({ + url: 'https://api.cloudflare.com/client/v4/accounts?per_page=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Listing table schemas is a read the List Tables tool already makes: a bad Convex deploy + * key gets 401, a key minted for another deployment gets 403 `BadDeployKey`, and any other + * JSON 403 is a valid key whose permissions exclude data reads. The deployment URL is + * normalized and SSRF-checked by the tools' own `convexApiUrl`. + */ +export const validateConvexServiceAccount = tokenCheckValidator({ + serviceLabel: 'Convex', + domain: ({ domain }) => { + const deploymentUrl = convexApiUrl(domain ?? '', '') + const parsed = new URL(deploymentUrl) + if (parsed.username || parsed.password) throw new Error('credentials in URL') + return deploymentUrl + }, + egress: 'configuredEndpoint', + request: ({ apiToken, domain = '' }) => ({ + url: convexApiUrl(domain, '/api/json_schemas?format=json'), + headers: { ...convexAuthHeaders(apiToken), Accept: 'application/json' }, + }), + forbiddenMeansScoped: true, + rejectsKey: (errorBody) => toRecord(errorBody).code === 'BadDeployKey', +}) + +/** Crunchbase's autocomplete endpoint answers 200 for a valid user key and 401 otherwise. */ +export const validateCrunchbaseServiceAccount = tokenCheckValidator({ + serviceLabel: 'Crunchbase', + request: ({ apiToken }) => ({ + url: 'https://api.crunchbase.com/v4/data/autocompletes?query=a&limit=1', + headers: { 'X-cb-user-key': apiToken, Accept: 'application/json' }, + }), +}) + +/** Cursor's API key info endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateCursorServiceAccount = tokenCheckValidator({ + serviceLabel: 'Cursor', + request: ({ apiToken }) => ({ + url: 'https://api.cursor.com/v0/me', + headers: { + Authorization: `Basic ${Buffer.from(`${apiToken}:`).toString('base64')}`, + Accept: 'application/json', + }, + }), +}) + +/** Daytona describes the calling API key only when it is valid, with no extra permission. */ +export const validateDaytonaServiceAccount = tokenCheckValidator({ + serviceLabel: 'Daytona', + request: ({ apiToken }) => ({ + url: 'https://app.daytona.io/api/api-keys/current', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Listing one session of the organization is the tools' own read on their host and auth: 200 + * proves the key and its access to that organization. Devin answers an unknown key, or a key + * from another organization, with a problem+json 403, so a 403 rejects. The id must survive + * URL encoding unchanged, so it can never steer the tools' unencoded path. + */ +export const validateDevinServiceAccount = tokenCheckValidator({ + serviceLabel: 'Devin', + workspaceId: ({ providerWorkspaceId }) => { + const orgId = (providerWorkspaceId ?? '').trim() + if (!/^[A-Za-z0-9_-]+$/.test(orgId)) throw new Error('invalid organization id') + return orgId + }, + request: ({ apiToken, providerWorkspaceId }) => ({ + url: `https://api.devin.ai/v3/organizations/${providerWorkspaceId}/sessions?first=1`, + headers: { Authorization: `Bearer ${apiToken.trim()}`, Accept: 'application/json' }, + }), +}) + +/** Discord returns the bot's own user for a valid bot token and 401 otherwise. */ +export const validateDiscordServiceAccount = tokenCheckValidator({ + serviceLabel: 'Discord', + request: ({ apiToken }) => ({ + url: 'https://discord.com/api/v10/users/@me', + headers: { Authorization: `Bot ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Dub's domain list answers 200 for a valid key, 401 for an unknown one, and a + * JSON 403 for a restricted key without the domains scope. + */ +export const validateDubServiceAccount = tokenCheckValidator({ + serviceLabel: 'Dub', + request: ({ apiToken }) => ({ + url: 'https://api.dub.co/domains?pageSize=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), + forbiddenMeansScoped: true, +}) + +/** + * Dynatrace's token lookup accepts an access token with any scope and returns its own + * metadata, including whether it is enabled; an unknown token gets 401. + */ +export const validateDynatraceServiceAccount = tokenCheckValidator({ + serviceLabel: 'Dynatrace', + domain: ({ domain }) => normalizeHttpBaseUrl(domain).replace(/\/api\/v[12]$/i, ''), + egress: 'configuredEndpoint', + request: ({ apiToken, domain = '' }) => ({ + url: buildDynatraceUrl(domain, '/apiTokens/lookup'), + method: 'POST', + headers: dynatraceHeaders(apiToken), + body: JSON.stringify({ token: apiToken }), + }), + acceptsBody: (body) => { + const token = toRecord(body) + if (typeof token.enabled !== 'boolean') { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Dynatrace returned an unexpected token lookup', + }) + } + return token.enabled + }, +}) + +/** + * ElevenLabs returns the key owner's account for a valid key and 400/401 for a bad one. A key + * without the user permission gets a 401 naming `missing_permissions`, or a 403, both of which + * still prove it authenticated. + */ +export const validateElevenLabsServiceAccount = tokenCheckValidator({ + serviceLabel: 'ElevenLabs', + request: ({ apiToken }) => ({ + url: 'https://api.elevenlabs.io/v1/user', + headers: { 'xi-api-key': apiToken, Accept: 'application/json' }, + }), + forbiddenMeansScoped: true, + provesKey: (errorBody) => toRecord(toRecord(errorBody).detail).status === 'missing_permissions', +}) + +/** + * Email Bison's `GET /api/users` returns the token's user on the instance it was typed with + * (dedi.emailbison.com or a white-labeled host), stored as the HTTPS origin the tools use; an + * unknown token answers 401. A 200 without Email Bison's user envelope is some other site. + */ +export const validateEmailBisonServiceAccount = tokenCheckValidator({ + serviceLabel: 'Email Bison', + domain: ({ domain }) => normalizeEmailBisonBaseUrl(refuseUnsafeTypedUrl(domain)), + egress: 'configuredEndpoint', + request: ({ apiToken, domain }) => ({ + url: `${domain}/api/users`, + headers: { Authorization: `Bearer ${apiToken.trim()}`, Accept: 'application/json' }, + }), + acceptsBody: (body) => { + const user = toRecord(toRecord(body).data) + return user.id !== undefined && user.id !== null + }, +}) + +/** Enrich's credit-balance endpoint costs no credits and succeeds only for a valid key. */ +export const validateEnrichServiceAccount = tokenCheckValidator({ + serviceLabel: 'Enrich', + request: ({ apiToken }) => ({ + url: 'https://api.enrich.so/v1/api/auth', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Extend's file list, on the API version the parser uses, answers 200 for a valid key and 401 otherwise. */ +export const validateExtendServiceAccount = tokenCheckValidator({ + serviceLabel: 'Extend', + request: ({ apiToken }) => ({ + url: 'https://api.extend.ai/files?maxPageSize=1', + headers: { + Authorization: `Bearer ${apiToken}`, + 'x-extend-api-version': '2025-04-21', + Accept: 'application/json', + }, + }), +}) + +/** Fathom's list-teams endpoint answers 200 for a valid API key and 401 otherwise. */ +export const validateFathomServiceAccount = tokenCheckValidator({ + serviceLabel: 'Fathom', + request: ({ apiToken }) => ({ + url: 'https://api.fathom.ai/external/v1/teams', + headers: { 'X-Api-Key': apiToken, Accept: 'application/json' }, + }), +}) + +/** + * Fireflies' GraphQL `user` query returns the key owner for a valid key. A + * rejected key comes back as HTTP 500 with an `auth_failed` error. + */ +export const validateFirefliesServiceAccount = tokenCheckValidator({ + serviceLabel: 'Fireflies', + request: ({ apiToken }) => ({ + url: 'https://api.fireflies.ai/graphql', + method: 'POST', + headers: { + Authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ query: '{ user { user_id } }' }), + }), + rejectsKey: (errorBody) => + toArray(toRecord(errorBody).errors).some((error) => toRecord(error).code === 'auth_failed'), + acceptsBody: (body) => { + if (typeof body !== 'object' || body === null || 'errors' in body) return false + const data = (body as { data?: { user?: { user_id?: unknown } | null } | null }).data + return typeof data?.user?.user_id === 'string' + }, +}) + +/** + * Flint has no identity endpoint, so the key is checked against the documented get-task route + * with an ID no organization owns, shaped like the documented `bg-550e8400-a1b2c3d4` so a format + * check can't answer first: an accepted key gets 404, a rejected one gets 401. + */ +const FLINT_VERIFY_URL = 'https://app.tryflint.com/api/v1/agent/tasks/bg-00000000-00000000' + +export async function validateFlintServiceAccount( + fields: TokenServiceAccountFields +): Promise { + const response = await fetchProvider( + FLINT_VERIFY_URL, + { + headers: { Authorization: `Bearer ${fields.apiToken.trim()}`, Accept: 'application/json' }, + redirect: 'error', + }, + 'get_task' + ) + if (response.ok) { + await response.body?.cancel().catch(() => undefined) + throw new TokenServiceAccountValidationError('provider_unavailable', response.status, { + step: 'get_task', + reason: 'expected 404 for an unknown task', + }) + } + if (response.status !== 404) await throwForProviderResponse(response, 'get_task') + // Only Flint's own JSON answer proves the key; an HTML 404 from a proxy proves nothing. + const body = toRecord(await readJsonBody(response, 'Flint').catch(() => null)) + if (body.error !== 'Task not found') { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'get_task', + reason: 'unexpected 404 body', + }) + } + + return { displayName: 'Flint', principal: null, auditMetadata: {} } +} + +/** Gamma lists themes for a valid API key and answers 401 for a missing or bad one. */ +export const validateGammaServiceAccount = tokenCheckValidator({ + serviceLabel: 'Gamma', + request: ({ apiToken }) => ({ + url: 'https://public-api.gamma.app/v1.0/themes?limit=1', + headers: { 'X-API-KEY': apiToken, Accept: 'application/json' }, + }), +}) + +/** + * GitHub's authenticated-user endpoint is available to fine-grained and classic personal + * access tokens alike (fine-grained tokens need no permission for it): 200 with the token + * owner for a valid token, 401 for a bad one, and 403 for a token that cannot act as a user + * (a GitHub App installation token). Sent with the same host, scheme, and headers as the + * GitHub tools; Enterprise Server hosts are deliberately not offered. + * https://docs.github.com/en/rest/authentication/endpoints-available-for-fine-grained-personal-access-tokens + */ +export const validateGitHubServiceAccount = tokenCheckValidator({ + serviceLabel: 'GitHub', + request: ({ apiToken }) => ({ + url: 'https://api.github.com/user', + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'Sim', + }, + }), + acceptsBody: (body) => { + const { login } = toRecord(body) + if (typeof login !== 'string' || !login.trim()) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'GitHub returned an unexpected user lookup', + }) + } + return { displayName: `${login.trim()} (GitHub)` } + }, +}) + +/** + * A repository as `workspace/repository`: a workspace slug, then a repository slug that + * cannot be `.` or `..`, so neither half can climb out of the request path. + */ +const BITBUCKET_REPOSITORY = /^[A-Za-z0-9][A-Za-z0-9_-]{0,254}\/[A-Za-z0-9_-][A-Za-z0-9._-]{0,61}$/ + +/** + * Bitbucket access tokens belong to a workspace, project, or repository rather than a user, + * so the user endpoints refuse them, and a repository token reaches only its repository. + * Reading one repository the token can see is the request Atlassian documents for all + * three kinds (`GET /2.0/repositories/{workspace}/{repository}` with the `repository` + * scope): 200 with the repository, 401 for a bad token, 403 without the scope, and 404 for + * a repository that does not exist or that the token cannot reach. + * https://support.atlassian.com/bitbucket-cloud/docs/using-access-tokens/ + */ +export const validateBitbucketServiceAccount = tokenCheckValidator({ + serviceLabel: 'Bitbucket', + workspaceId: ({ providerWorkspaceId }) => { + const repository = (providerWorkspaceId ?? '').trim() + if (!BITBUCKET_REPOSITORY.test(repository)) throw new Error('invalid repository') + return repository + }, + request: ({ apiToken, providerWorkspaceId = '' }) => ({ + url: `${BITBUCKET_API_BASE}/repositories/${providerWorkspaceId}`, + headers: bitbucketHeaders(apiToken), + }), + acceptsBody: (body) => { + if (typeof toRecord(body).full_name !== 'string') { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Bitbucket returned an unexpected repository', + }) + } + return true + }, +}) + +/** Scopes that let the GitLab tools call the REST API. */ +const GITLAB_API_SCOPES = new Set(['api', 'read_api']) + +/** + * GitLab's token self-lookup answers for a personal, project, or group access token at the + * instance it was typed with (gitlab.com or self-managed); the token must be active and carry an + * API scope. A reply that isn't a token lookup is the instance misbehaving, not a bad token. + */ +export const validateGitLabServiceAccount = tokenCheckValidator({ + serviceLabel: 'GitLab', + domain: ({ domain }) => normalizeGitLabHost(domain), + egress: 'configuredEndpoint', + request: ({ apiToken, domain }) => ({ + url: `https://${domain}/api/v4/personal_access_tokens/self`, + headers: { 'PRIVATE-TOKEN': apiToken, Accept: 'application/json' }, + }), + acceptsBody: (body) => { + const token = toRecord(body) + if (typeof token.active !== 'boolean' || !Array.isArray(token.scopes)) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'GitLab returned an unexpected token lookup', + }) + } + const usable = + token.active && + token.revoked !== true && + token.scopes.some((scope) => GITLAB_API_SCOPES.has(String(scope))) + const name = typeof token.name === 'string' ? token.name.trim() : '' + return usable && name ? { displayName: name } : usable + }, +}) + +/** + * Grafana's `GET /api/user` needs a signed-in, non-anonymous identity but no role, so it + * answers 200 for any valid service account token and 401 for a bad one, even where + * anonymous access lets most reads answer 200 to any token. + */ +export const validateGrafanaServiceAccount = tokenCheckValidator({ + serviceLabel: 'Grafana', + domain: ({ domain }) => normalizeHttpBaseUrl(domain), + egress: 'configuredEndpoint', + request: ({ apiToken, domain }) => ({ + url: `${domain}/api/user`, + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), + acceptsBody: (body) => { + const login = toRecord(body).login + if (typeof login !== 'string' || !login.trim()) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Grafana returned an unexpected user lookup', + }) + } + return true + }, +}) + +/** + * Grain's list-teams endpoint (a read-only POST, as the tools call it) answers 200 for a valid + * access token and 401 `invalid_token` otherwise. + */ +export const validateGrainServiceAccount = tokenCheckValidator({ + serviceLabel: 'Grain', + request: ({ apiToken }) => ({ + url: 'https://api.grain.com/_/public-api/v2/teams', + method: 'POST', + headers: { + Authorization: `Bearer ${apiToken}`, + 'Public-Api-Version': '2025-10-31', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }), +}) + +/** + * Listing one Granola note answers 200 for a valid personal or workspace API key and 401 otherwise. + * An Enterprise admin can turn off note scopes for members, so a JSON 403 still proves the key. + */ +export const validateGranolaServiceAccount = tokenCheckValidator({ + serviceLabel: 'Granola', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: `${GRANOLA_API_BASE}/notes?page_size=1`, + headers: { ...granolaHeaders(apiToken), Accept: 'application/json' }, + }), +}) + +/** Hex's list-projects endpoint (the one the Hex tools call) answers 200 for a valid token and 401 otherwise. */ +export const validateHexServiceAccount = tokenCheckValidator({ + serviceLabel: 'Hex', + request: ({ apiToken }) => ({ + url: 'https://app.hex.tech/api/v1/projects?limit=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Hugging Face's whoami-v2 endpoint answers 200 for any valid access token and 401 otherwise. */ +export const validateHuggingfaceServiceAccount = tokenCheckValidator({ + serviceLabel: 'Hugging Face', + request: ({ apiToken }) => ({ + url: 'https://huggingface.co/api/whoami-v2', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** incident.io's identity endpoint describes the calling API key, so it answers 200 for any valid key and 401 otherwise. */ +export const validateIncidentioServiceAccount = tokenCheckValidator({ + serviceLabel: 'incident.io', + request: ({ apiToken }) => ({ + url: 'https://api.incident.io/v1/identity', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Infisical's `GET /api/v1/identities/details`, called with a machine identity's own access token + * on its instance (US, EU, or self-hosted), needs no project permission and answers the identity's + * organization; a malformed, expired, or wrong-instance token gets a JSON 403 TokenError. + */ +export const validateInfisicalServiceAccount = tokenCheckValidator({ + serviceLabel: 'Infisical', + domain: ({ domain }) => normalizeHttpBaseUrl(domain), + egress: 'configuredEndpoint', + request: ({ apiToken, domain }) => ({ + url: `${domain}/api/v1/identities/details`, + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), + acceptsBody: (body) => { + const organization = toRecord(toRecord(toRecord(body).identityDetails).organization) + if (typeof organization.id !== 'string' || !organization.id) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Infisical returned unexpected identity details', + }) + } + return true + }, +}) + +/** + * Instantly's campaign list (the one the Instantly tools call) answers 200 for a valid key and + * 401 for a bad one; a valid key without the campaigns scope gets 403. + */ +export const validateInstantlyServiceAccount = tokenCheckValidator({ + serviceLabel: 'Instantly', + request: ({ apiToken }) => ({ + url: 'https://api.instantly.ai/api/v2/campaigns?limit=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), + forbiddenMeansScoped: true, +}) + +/** Intercom's identify-admin endpoint answers 200 with the token's admin and 401 to an unknown token. */ +export const validateIntercomServiceAccount = tokenCheckValidator({ + serviceLabel: 'Intercom', + request: ({ apiToken }) => ({ + url: 'https://api.intercom.io/me', + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: 'application/json', + 'Intercom-Version': '2.14', + }, + }), +}) + +/** + * Jotform's account endpoint on the region's host (via the tools' URL builder) answers 200 for a + * valid key and 401 otherwise; a key only works on the host that issued it, so a wrong region is + * refused too. Jotform reports some failures inside a 200, so `responseCode` must also be 200. + */ +export const validateJotformServiceAccount = tokenCheckValidator({ + serviceLabel: 'Jotform', + domain: allowedDomain(new Set(Object.keys(JOTFORM_REGION_HOSTS))), + request: ({ apiToken, domain }) => ({ + url: buildJotformUrl({ apiKey: apiToken, region: domain }, 'user').toString(), + headers: { ...buildJotformHeaders(apiToken), Accept: 'application/json' }, + }), + acceptsBody: (body) => Number(toRecord(body).responseCode) === 200, +}) + +/** + * Jupyter's `GET /api/status` is authenticated and answers a bad token with 403, never a + * login redirect. The server URL is normalized as the tools normalize it and reached + * under the same `selfHostedService` egress profile. + */ +export const validateJupyterServiceAccount = tokenCheckValidator({ + serviceLabel: 'Jupyter', + domain: ({ domain }) => normalizeJupyterServerUrl(refuseUnsafeTypedUrl(domain)), + egress: 'selfHostedService', + request: ({ apiToken, domain }) => ({ + url: `${domain}/api/status`, + headers: { ...buildJupyterAuthHeaders(apiToken), Accept: 'application/json' }, + }), + acceptsBody: (body) => { + if (typeof toRecord(body).started !== 'string') { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Jupyter returned an unexpected status', + }) + } + return true + }, +}) + +/** LangSmith's workspace list answers 200 for a valid API key and 401/403 otherwise. */ +export const validateLangsmithServiceAccount = tokenCheckValidator({ + serviceLabel: 'LangSmith', + request: ({ apiToken }) => ({ + url: 'https://api.smith.langchain.com/api/v1/workspaces', + headers: { 'X-Api-Key': apiToken, Accept: 'application/json' }, + }), +}) + +/** LaunchDarkly's caller-identity endpoint describes the calling token, so it answers 200 for any valid token and 401 otherwise. */ +export const validateLaunchdarklyServiceAccount = tokenCheckValidator({ + serviceLabel: 'LaunchDarkly', + request: ({ apiToken }) => ({ + url: 'https://app.launchdarkly.com/api/v2/caller-identity', + headers: { Authorization: apiToken, Accept: 'application/json' }, + }), +}) + +/** + * Lemlist's team endpoint answers 200 for a valid API key and 401 otherwise; the key is the + * Basic-auth password with an empty username, as the tools send it. + */ +export const validateLemlistServiceAccount = tokenCheckValidator({ + serviceLabel: 'Lemlist', + request: ({ apiToken }) => ({ + url: 'https://api.lemlist.com/api/team', + headers: { + Authorization: `Basic ${Buffer.from(`:${apiToken}`).toString('base64')}`, + Accept: 'application/json', + }, + }), +}) + +/** Linq's phone-number list (the one the Linq tools call) answers 200 for a valid key and 401 otherwise. */ +export const validateLinqServiceAccount = tokenCheckValidator({ + serviceLabel: 'Linq', + request: ({ apiToken }) => ({ + url: 'https://api.linqapp.com/api/partner/v3/phone_numbers', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +const LOGFIRE_CLOUD_REGIONS = new Set(['auto', 'us', 'eu']) + +/** + * Logfire's read-token-info endpoint answers any read token with its organization and + * project, and 401 for an unknown token (including one from the other region). The + * region or host field resolves through the tools' own `getLogfireBaseUrl` (`us`/`eu` + * picks that Cloud region, `auto` reads it from the token's prefix, anything else is a + * self-hosted base URL), and the stored base URL fills the tools' `host`, which wins. + */ +export const validateLogfireServiceAccount = tokenCheckValidator({ + serviceLabel: 'Logfire', + domain: ({ apiToken, domain }) => { + const typed = (domain ?? '').trim() + if (!typed) throw new Error('missing region or host') + const region = typed.toLowerCase() + if (LOGFIRE_CLOUD_REGIONS.has(region)) { + return getLogfireBaseUrl(apiToken, region as 'auto' | 'us' | 'eu') + } + // A self-hosted base URL names a domain, a port, or a scheme; a bare word is a mistyped region. + if (!/[.:]/.test(typed)) throw new Error('unknown region') + return getLogfireBaseUrl(apiToken, 'auto', refuseUnsafeTypedUrl(typed)) + }, + egress: 'configuredEndpoint', + request: ({ apiToken, domain }) => ({ + url: `${domain}${LOGFIRE_READ_TOKEN_INFO_PATH}`, + headers: logfireHeaders(apiToken), + }), + acceptsBody: (body) => { + const project = toRecord(body).project_name + if (typeof project !== 'string' || !project.trim()) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Logfire returned an unexpected token lookup', + }) + } + return true + }, +}) + +/** Loops' API-key test endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateLoopsServiceAccount = tokenCheckValidator({ + serviceLabel: 'Loops', + request: ({ apiToken }) => ({ + url: 'https://app.loops.so/api/v1/api-key', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Luma's get-self endpoint returns the key's user, so it answers 200 for a valid key and 401 otherwise. */ +export const validateLumaServiceAccount = tokenCheckValidator({ + serviceLabel: 'Luma', + request: ({ apiToken }) => ({ + url: 'https://public-api.luma.com/v1/users/get-self', + headers: { 'x-luma-api-key': apiToken, Accept: 'application/json' }, + }), +}) + +/** The data center suffix (`-us21`) that names the key's API host. */ +const DATA_CENTER_SUFFIX = /-([a-z]+\d+)$/ + +/** + * Mailchimp's ping endpoint answers 200 for a valid key and 401 otherwise. The + * host comes from the key's data center suffix, which must match before any + * request so a crafted key can never point the check at another host. + */ +export const validateMailchimpServiceAccount = tokenCheckValidator({ + serviceLabel: 'Mailchimp', + request: ({ apiToken }) => { + const dataCenter = DATA_CENTER_SUFFIX.exec(apiToken)?.[1] + if (!dataCenter) { + throw new TokenServiceAccountValidationError('invalid_credentials', 400, { + step: 'verify', + reason: 'Mailchimp API key has no data center suffix', + }) + } + return { + url: `https://${dataCenter}.api.mailchimp.com/3.0/ping`, + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + } + }, +}) + +/** + * Listing one domain on the region's host answers 200 for a valid account API key and 401 + * ("Invalid private key") otherwise; a key from the other region is refused the same way, so the + * check also proves the region. The region is checked first because the tools send any value + * other than `eu` to the US host. A role-restricted key's JSON 403 still proves the key. + */ +export const validateMailgunServiceAccount = tokenCheckValidator({ + serviceLabel: 'Mailgun', + domain: allowedDomain(US_OR_EU), + forbiddenMeansScoped: true, + request: ({ apiToken, domain }) => ({ + url: `${getMailgunApiBaseUrl(domain as MailgunRegion)}/domains?limit=1`, + headers: { + Authorization: `Basic ${Buffer.from(`api:${apiToken}`).toString('base64')}`, + Accept: 'application/json', + }, + }), +}) + +/** Mem0's ping endpoint (the one its SDK calls on startup) answers 200 for a valid key and 401 otherwise. */ +export const validateMem0ServiceAccount = tokenCheckValidator({ + serviceLabel: 'Mem0', + request: ({ apiToken }) => ({ + url: 'https://api.mem0.ai/v1/ping/', + headers: { Authorization: `Token ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * A New Relic User API key is bound to its data center, so it is checked against the + * NerdGraph host of the region typed with it (`us` or `eu`, stored lower-cased because + * the tools send anything but `eu` to the US host). An unknown key, or one from the other + * region, gets 401; NerdGraph can also answer 200 with an `errors` array, so only a + * resolved user proves the key. + */ +export const validateNewRelicServiceAccount = tokenCheckValidator({ + serviceLabel: 'New Relic', + domain: allowedDomain(US_OR_EU), + request: ({ apiToken, domain }) => ({ + url: getNerdGraphEndpoint(domain as 'us' | 'eu'), + method: 'POST', + headers: { ...newRelicHeaders(apiToken), Accept: 'application/json' }, + body: JSON.stringify({ query: '{ actor { user { id } } }' }), + }), + acceptsBody: (body) => toRecord(toRecord(toRecord(body).data).actor).user != null, +}) + +/** + * With an API token, Okta's `GET /api/v1/users/me` returns the admin the token acts as on the + * org whose Okta-owned domain the tools' `validateOktaDomain` accepts; a bad or revoked token + * gets 401 `E0000011`. A reply without a user id is Okta misbehaving, not a bad token. + */ +export const validateOktaServiceAccount = tokenCheckValidator({ + serviceLabel: 'Okta', + domain: ({ domain }) => validateOktaDomain(domain ?? ''), + request: ({ apiToken, domain }) => ({ + url: `https://${domain}/api/v1/users/me`, + headers: oktaHeaders(apiToken.trim()), + }), + acceptsBody: (body) => { + const id = toRecord(body).id + if (typeof id !== 'string' || !id) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Okta returned an unexpected user', + }) + } + return true + }, +}) + +/** Otter's workspace endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateOtterServiceAccount = tokenCheckValidator({ + serviceLabel: 'Otter.ai', + request: ({ apiToken }) => ({ + url: 'https://api.otter.ai/v1/workspace', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Listing one PagerDuty user answers 200 for a valid REST API key and 401 for an unknown one; a + * JSON 403 comes only from an authenticated key whose user lacks access to the user list. + */ +export const validatePagerDutyServiceAccount = tokenCheckValidator({ + serviceLabel: 'PagerDuty', + request: ({ apiToken }) => ({ + url: 'https://api.pagerduty.com/users?limit=1', + headers: { + Authorization: `Token token=${apiToken}`, + Accept: 'application/vnd.pagerduty+json;version=2', + }, + }), + forbiddenMeansScoped: true, +}) + +/** Listing one inquiry answers 200 for a valid key with inquiry read access and 401 otherwise. */ +export const validatePersonaServiceAccount = tokenCheckValidator({ + serviceLabel: 'Persona', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: 'https://api.withpersona.com/api/v1/inquiries?page%5Bsize%5D=1', + headers: { + Authorization: `Bearer ${apiToken}`, + 'Persona-Version': '2025-12-08', + Accept: 'application/json', + }, + }), +}) + +/** + * The instance URL in the form the tools build on: an http(s) origin plus any reverse-proxy path + * prefix, without a trailing slash; a bare host is taken as https. `planeApiUrl` applies the tools' + * own checks (no credentials, query, fragment, dot segments, or /api/vN suffix). + */ +function normalizePlaneInstanceUrl(raw: string | undefined): string { + const base = normalizeHttpBaseUrl(raw) + // The tools' own checks: no dot segments or /api/vN suffix. + planeApiUrl(base, '/api/v1/users/me/') + return base +} + +/** + * Plane's documented current-user read, served under /api/v1 by Plane Cloud and Community + * Edition, answers 200 with the user for a valid token on the instance it was typed with, and 401 + * or 403 for a bad one. A reply without a user id is the instance misbehaving. + */ +export const validatePlaneServiceAccount = tokenCheckValidator({ + serviceLabel: 'Plane', + domain: ({ domain }) => normalizePlaneInstanceUrl(domain), + egress: 'configuredEndpoint', + request: ({ apiToken, domain }) => { + let headers: Record + try { + headers = planeHeaders(apiToken) + } catch { + throw new TokenServiceAccountValidationError('invalid_credentials', 400, { + step: 'verify', + reason: 'invalid Plane API token', + }) + } + return { url: planeApiUrl(domain, '/api/v1/users/me/'), headers } + }, + acceptsBody: (body) => { + if (typeof toRecord(body).id !== 'string') { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Plane returned an unexpected current user', + }) + } + return true + }, +}) + +/** Profound's organization categories endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateProfoundServiceAccount = tokenCheckValidator({ + serviceLabel: 'Profound', + request: ({ apiToken }) => ({ + url: 'https://api.tryprofound.com/v1/org/categories', + headers: { 'X-API-Key': apiToken, Accept: 'application/json' }, + }), +}) + +/** Pulse's usage endpoint reads the key's organization for free: 200 for a valid key, 401 otherwise. */ +export const validatePulseServiceAccount = tokenCheckValidator({ + serviceLabel: 'Pulse', + request: ({ apiToken }) => ({ + url: 'https://api.runpulse.com/usage', + headers: { 'x-api-key': apiToken, Accept: 'application/json' }, + }), +}) + +/** Listing one Quartr event type answers 200 for a valid key and 401 otherwise. */ +export const validateQuartrServiceAccount = tokenCheckValidator({ + serviceLabel: 'Quartr', + request: ({ apiToken }) => ({ + url: 'https://api.quartr.com/public/v3/event-types?limit=1', + headers: { 'x-api-key': apiToken, Accept: 'application/json' }, + }), +}) + +/** QuiverAI's list-models endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateQuiverServiceAccount = tokenCheckValidator({ + serviceLabel: 'Quiver', + request: ({ apiToken }) => ({ + url: 'https://api.quiver.ai/v1/models', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** RB2B's credit balance endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateRB2BServiceAccount = tokenCheckValidator({ + serviceLabel: 'RB2B', + request: ({ apiToken }) => ({ + url: 'https://api.rb2b.com/api/v1/credits', + headers: { 'Api-Key': apiToken, Accept: 'application/json' }, + }), +}) + +/** Reducto's list-jobs endpoint answers 200 for a valid key and 401 otherwise; one job, no configs. */ +export const validateReductoServiceAccount = tokenCheckValidator({ + serviceLabel: 'Reducto', + request: ({ apiToken }) => ({ + url: 'https://platform.reducto.ai/jobs?limit=1&exclude_configs=true', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Resend's list-domains endpoint answers 200 for a full-access key. A sending-only key gets a 401 + * named `restricted_api_key`, which still proves Resend authenticated it; an unknown key gets a + * `validation_error` and an inactive or suspended one a 403, both of which reject it. + */ +export const validateResendServiceAccount = tokenCheckValidator({ + serviceLabel: 'Resend', + request: ({ apiToken }) => ({ + url: 'https://api.resend.com/domains', + headers: { Authorization: `Bearer ${apiToken.trim()}`, Accept: 'application/json' }, + }), + provesKey: (errorBody) => toRecord(errorBody).name === 'restricted_api_key', +}) + +/** Rippling's current-user endpoint answers 200 for a valid token with the sso-me.read scope and 401 otherwise. */ +export const validateRipplingServiceAccount = tokenCheckValidator({ + serviceLabel: 'Rippling', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: 'https://rest.ripplingapis.com/sso-me/', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Listing one Rocketlane user answers 200 for a valid key and 401 otherwise. */ +export const validateRocketlaneServiceAccount = tokenCheckValidator({ + serviceLabel: 'Rocketlane', + request: ({ apiToken }) => ({ + url: 'https://api.rocketlane.com/api/1.0/users?pageSize=1', + headers: { 'api-key': apiToken, Accept: 'application/json' }, + }), +}) + +/** + * Listing one Rootly severity (a read every key role has) answers 200 for a valid key and 401 + * for a bad one; a key whose role can't read severities gets the vendor's JSON 403. + */ +export const validateRootlyServiceAccount = tokenCheckValidator({ + serviceLabel: 'Rootly', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: 'https://api.rootly.com/v1/severities?page%5Bsize%5D=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/vnd.api+json' }, + }), +}) + +/** SendGrid's scopes endpoint lists the scopes of the key that calls it, so any valid key gets 200 and a bad one 401. */ +export const validateSendGridServiceAccount = tokenCheckValidator({ + serviceLabel: 'SendGrid', + request: ({ apiToken }) => ({ + url: 'https://api.sendgrid.com/v3/scopes', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Listing the token's Sentry organizations answers 200 for a valid token and 401 for a bad one; + * a token without `org:read` (an organization token, or a narrowly scoped one) gets Sentry's JSON + * 403. + */ +export const validateSentryServiceAccount = tokenCheckValidator({ + serviceLabel: 'Sentry', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: 'https://sentry.io/api/0/organizations/', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Similarweb's capabilities endpoint costs no data credits and answers 200 only for a valid key. */ +export const validateSimilarwebServiceAccount = tokenCheckValidator({ + serviceLabel: 'Similarweb', + request: ({ apiToken }) => ({ + url: `https://api.similarweb.com/capabilities?api_key=${encodeURIComponent(apiToken)}`, + headers: { Accept: 'application/json' }, + }), +}) + +/** Sixtyfour's balance endpoint answers 200 for a valid key and 401 for a missing or invalid one. */ +export const validateSixtyfourServiceAccount = tokenCheckValidator({ + serviceLabel: 'Sixtyfour', + request: ({ apiToken }) => ({ + url: 'https://api.sixtyfour.ai/check-balance', + headers: { 'x-api-key': apiToken, Accept: 'application/json' }, + }), +}) + +/** Smartlead answers 401 for an invalid key; listing one email account is the cheapest authenticated read. */ +export const validateSmartleadServiceAccount = tokenCheckValidator({ + serviceLabel: 'Smartlead', + request: ({ apiToken }) => ({ + url: `https://server.smartlead.ai/api/v1/email-accounts/?api_key=${encodeURIComponent(apiToken)}&offset=0&limit=1`, + headers: { Accept: 'application/json' }, + }), +}) + +/** Sportmonks' my/usage endpoint reads the token's own usage, so it succeeds only for a valid token. */ +export const validateSportmonksServiceAccount = tokenCheckValidator({ + serviceLabel: 'Sportmonks', + request: ({ apiToken }) => ({ + url: 'https://api.sportmonks.com/v3/my/usage?per_page=1', + headers: { Authorization: apiToken, Accept: 'application/json' }, + }), +}) + +/** + * Square's list-locations read answers 200 for a valid production access token and 401 for a bad + * one; a token without the merchant-profile scope gets 403. + */ +export const validateSquareServiceAccount = tokenCheckValidator({ + serviceLabel: 'Square', + request: ({ apiToken }) => ({ + url: 'https://connect.squareup.com/v2/locations', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), + forbiddenMeansScoped: true, +}) + +/** + * Listing one customer answers 200 for a valid secret key and 401 for an unknown + * one; a restricted key without customer read access gets Stripe's JSON 403. + */ +export const validateStripeServiceAccount = tokenCheckValidator({ + serviceLabel: 'Stripe', + forbiddenMeansScoped: true, + request: ({ apiToken }) => ({ + url: 'https://api.stripe.com/v1/customers?limit=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Reduces a typed Supabase project to the bare ref the tools take as `projectId`: the ref, its + * `.supabase.co` host, or the project URL, which must then pass `validateSupabaseProjectId`. + */ +function normalizeSupabaseProjectRef(raw: string | undefined): string { + const typed = (raw ?? '').trim().toLowerCase() + const host = /^https?:\/\//.test(typed) ? new URL(typed).hostname : typed.replace(/\/+$/, '') + const ref = host.endsWith('.supabase.co') ? host.slice(0, -'.supabase.co'.length) : host + const result = validateSupabaseProjectId(ref) + if (!result.isValid || !result.sanitized) throw new Error('invalid project ref') + return result.sanitized +} + +/** + * The API gateway key-checks every `/auth/v1/` route but a few open ones, so Auth's settings read + * on `.supabase.co` (host built by the tools' `supabaseBaseUrl`) answers 200 for any key of + * that project (anon, publishable, secret, or service_role) and 401 for a key it doesn't know. + */ +export const validateSupabaseServiceAccount = tokenCheckValidator({ + serviceLabel: 'Supabase', + domain: ({ domain }) => normalizeSupabaseProjectRef(domain), + request: ({ apiToken, domain }) => ({ + url: `${supabaseBaseUrl(domain ?? '')}/auth/v1/settings`, + headers: { apikey: apiToken, Accept: 'application/json' }, + }), +}) + +/** + * Listing devices in the token's default tailnet (`-`) answers 200 for a valid token and 401 for a + * bad one; an OAuth token without the devices scope gets 403. + */ +export const validateTailscaleServiceAccount = tokenCheckValidator({ + serviceLabel: 'Tailscale', + request: ({ apiToken }) => ({ + url: 'https://api.tailscale.com/api/v2/tailnet/-/devices', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), + forbiddenMeansScoped: true, +}) + +/** Tavily's usage endpoint answers 200 for a valid key and 401 otherwise. */ +export const validateTavilyServiceAccount = tokenCheckValidator({ + serviceLabel: 'Tavily', + request: ({ apiToken }) => ({ + url: 'https://api.tavily.com/usage', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Telegram's getMe exists to test a bot token: 200 for a valid one, 401 or 404 otherwise. */ +export const validateTelegramServiceAccount = tokenCheckValidator({ + serviceLabel: 'Telegram', + request: ({ apiToken }) => ({ + url: `https://api.telegram.org/bot${encodeURIComponent(apiToken)}/getMe`, + headers: { Accept: 'application/json' }, + }), +}) + +/** + * Listing Thrive tags is a read with the same Basic `tenantId:apiKey` auth the tools send, on the + * region host the key belongs to; a bad key or tenant gets 401. + */ +export const validateThriveServiceAccount = tokenCheckValidator({ + serviceLabel: 'Thrive', + domain: allowedDomain(THRIVE_HOSTS), + workspaceId: ({ providerWorkspaceId }) => { + const tenantId = (providerWorkspaceId ?? '').trim() + // The tenant id is the Basic-auth username: printable ASCII, and no colon, which would split it. + if (!/^[\x21-\x39\x3b-\x7e]+$/.test(tenantId)) throw new Error('invalid tenant id') + return tenantId + }, + request: ({ apiToken, domain, providerWorkspaceId = '' }) => ({ + url: `${getThriveBaseUrl(domain, 'v1')}/tags`, + headers: { ...getThriveHeaders(providerWorkspaceId, apiToken), Accept: 'application/json' }, + }), +}) + +/** + * `SELECT 1` on Tinybird's Query API touches no resource and answers 200 for a token with a read + * scope at the base URL it was typed with. An unknown or other-region token gets a 403 naming an + * invalid authentication token (and echoing it); any other JSON 403 is a valid token without a + * read scope, such as an append-only token. + */ +export const validateTinybirdServiceAccount = tokenCheckValidator({ + serviceLabel: 'Tinybird', + domain: ({ domain }) => normalizeHttpBaseUrl(domain), + egress: 'configuredEndpoint', + errorBodyEchoesToken: true, + forbiddenMeansScoped: true, + rejectsKey: (errorBody) => + String(toRecord(errorBody).error ?? '').includes('invalid authentication token'), + request: ({ apiToken, domain }) => ({ + url: `${domain}/v0/sql?q=${encodeURIComponent('SELECT 1 FORMAT JSON')}`, + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Listing one run needs a valid environment secret key; Trigger.dev answers 401 for any other. */ +export const validateTriggerDevServiceAccount = tokenCheckValidator({ + serviceLabel: 'Trigger.dev', + request: ({ apiToken }) => ({ + url: 'https://api.trigger.dev/api/v1/runs?page%5Bsize%5D=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** Twilio Account SIDs: AC followed by 32 lowercase hex digits. */ +const TWILIO_ACCOUNT_SID = /^AC[0-9a-f]{32}$/ + +/** + * Fetching the account resource with the SID and auth token as Basic auth (the tools' scheme) + * answers 200 for a matching pair and 401 (code 20003) for a wrong token, an unknown SID, or a SID + * the token doesn't belong to. The SID is checked first so it can never rewrite the path. + */ +export const validateTwilioServiceAccount = tokenCheckValidator({ + serviceLabel: 'Twilio', + workspaceId: ({ providerWorkspaceId }) => { + const accountSid = (providerWorkspaceId ?? '').trim() + if (!TWILIO_ACCOUNT_SID.test(accountSid)) throw new Error('invalid Account SID') + return accountSid + }, + request: ({ apiToken, providerWorkspaceId }) => ({ + url: `https://api.twilio.com/2010-04-01/Accounts/${providerWorkspaceId}.json`, + headers: { + Authorization: `Basic ${Buffer.from(`${providerWorkspaceId}:${apiToken}`).toString('base64')}`, + Accept: 'application/json', + }, + }), +}) + +/** + * Listing one form answers 200 for a token with the forms:read scope the + * Typeform tools need. Typeform answers an unknown token with a 403, so every + * refusal rejects it. + */ +export const validateTypeformServiceAccount = tokenCheckValidator({ + serviceLabel: 'Typeform', + request: ({ apiToken }) => ({ + url: 'https://api.typeform.com/forms?page_size=1', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * PING answers `{"result":"PONG"}` for a standard token; a bad token gets 401. A token whose + * ACL doesn't allow PING (a read-only or ACL-user token) gets a NOPERM error, which still + * proves it authenticated. Any http(s) origin is accepted, as the legacy block accepted any + * URL, so the check goes through the guarded egress. + */ +export const validateUpstashServiceAccount = tokenCheckValidator({ + serviceLabel: 'Upstash', + domain: ({ domain }) => normalizeHttpBaseUrl(domain), + egress: 'configuredEndpoint', + request: ({ apiToken, domain }) => ({ + url: `${domain}/ping`, + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), + provesKey: (errorBody) => /^NOPERM/i.test(String(toRecord(errorBody).error ?? '')), + acceptsBody: (body) => { + if (toRecord(body).result !== 'PONG') { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'verify', + reason: 'Upstash returned an unexpected PING reply', + }) + } + return true + }, +}) + +/** UptimeRobot's v3 user/me returns the key owner's profile, so it succeeds only for a valid key. */ +export const validateUptimeRobotServiceAccount = tokenCheckValidator({ + serviceLabel: 'UptimeRobot', + request: ({ apiToken }) => ({ + url: 'https://api.uptimerobot.com/v3/user/me', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * Vercel's current-user endpoint answers 200 for a valid token. An unknown token + * gets a 403 flagged `invalidToken`; any other JSON 403 is a valid token + * without access to the user record. + */ +export const validateVercelServiceAccount = tokenCheckValidator({ + serviceLabel: 'Vercel', + forbiddenMeansScoped: true, + rejectsKey: (errorBody) => toRecord(toRecord(errorBody).error).invalidToken === true, + request: ({ apiToken }) => ({ + url: 'https://api.vercel.com/v2/user', + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** + * The Graph API's /me answers 200 for any valid access token and a 4xx + * OAuthException (code 190) for an invalid, expired, or revoked one. + */ +export const validateWhatsAppServiceAccount = tokenCheckValidator({ + serviceLabel: 'WhatsApp', + request: ({ apiToken }) => ({ + url: `https://graph.facebook.com/${WHATSAPP_GRAPH_VERSION}/me?fields=id`, + headers: { Authorization: `Bearer ${apiToken}`, Accept: 'application/json' }, + }), +}) + +/** You.com's account balance read costs no credits and answers 200 only for a valid key. */ +export const validateYouComServiceAccount = tokenCheckValidator({ + serviceLabel: 'You.com', + request: ({ apiToken }) => ({ + url: 'https://api.you.com/v1/billing/account_balance', + headers: { 'X-API-Key': apiToken, Accept: 'application/json' }, + }), +}) + +/** + * Google's 403 reasons for a valid key that is only out of quota for now. Mirrors + * `isGoogleQuotaReason` in the Google connectors, whose module is too heavy for this graph. + */ +const YOUTUBE_QUOTA_REASONS = new Set([ + 'quotaExceeded', + 'dailyLimitExceeded', + 'rateLimitExceeded', + 'userRateLimitExceeded', + 'RATE_LIMIT_EXCEEDED', +]) + +/** + * Listing video categories costs one quota unit. Google answers 400 for an invalid key and 403 + * for a restricted one or a project without the YouTube API; a 403 for spent quota still proves + * the key, so only that one is accepted. + */ +export const validateYouTubeServiceAccount = tokenCheckValidator({ + serviceLabel: 'YouTube', + request: ({ apiToken }) => ({ + url: `https://www.googleapis.com/youtube/v3/videoCategories?part=snippet®ionCode=US&key=${encodeURIComponent(apiToken)}`, + headers: { Accept: 'application/json' }, + }), + forbiddenMeansScoped: true, + rejectsKey: (errorBody) => { + const error = toRecord(toRecord(errorBody).error) + if (error.code !== 403) return false + return !toArray(error.errors).some((entry) => + YOUTUBE_QUOTA_REASONS.has(String(toRecord(entry).reason)) + ) + }, +}) + +/** Listing one thread needs a valid project key; Zep answers 401 for any other. */ +export const validateZepServiceAccount = tokenCheckValidator({ + serviceLabel: 'Zep', + request: ({ apiToken }) => ({ + url: 'https://api.getzep.com/api/v2/threads?page_size=1&page_number=1', + headers: { Authorization: `Api-Key ${apiToken}`, Accept: 'application/json' }, + }), +}) diff --git a/apps/sim/lib/credentials/workload-identity/audiences.ts b/apps/sim/lib/credentials/workload-identity/audiences.ts new file mode 100644 index 00000000000..e8e1be09a51 --- /dev/null +++ b/apps/sim/lib/credentials/workload-identity/audiences.ts @@ -0,0 +1,151 @@ +import { SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/credentials/token-service-accounts/descriptors' +import { + AWS_SERVICE_ACCOUNT_PROVIDER_ID, + DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, + GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, + MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, +} from '@/lib/oauth/types' + +/** Relying parties that trust Sim's workload-identity issuer. */ +export const WORKLOAD_IDENTITY_PROVIDERS = [ + 'aws', + 'databricks', + 'snowflake', + 'microsoft', + 'google', +] as const + +export type WorkloadIdentityProvider = (typeof WORKLOAD_IDENTITY_PROVIDERS)[number] + +/** + * Every auth mode a provider's saved credential can hold, keyless first: the stored + * `authType` a reconnect reopens on, so a key-based credential keeps its own mode. + */ +export const WORKLOAD_IDENTITY_PROVIDER_AUTH_TYPES = { + aws: ['oidc_role', 'assume_role', 'access_keys'], + databricks: ['oidc_federation', 'service_principal', 'personal_access_token'], + snowflake: ['workload_identity', 'programmatic_access_token'], + microsoft: ['federated', 'client_secret', 'certificate'], + google: ['workload_identity', 'json_key'], +} as const satisfies Record + +export type WorkloadIdentityProviderAuthType< + P extends WorkloadIdentityProvider = WorkloadIdentityProvider, +> = (typeof WORKLOAD_IDENTITY_PROVIDER_AUTH_TYPES)[P][number] + +/** Every provider's auth modes in one list, for the reconnect contract. */ +export const WORKLOAD_IDENTITY_AUTH_TYPE_LIST = [ + ...new Set(Object.values(WORKLOAD_IDENTITY_PROVIDER_AUTH_TYPES).flat()), +] as [WorkloadIdentityProviderAuthType, ...WorkloadIdentityProviderAuthType[]] + +/** The service-account provider ID each relying party's keyless credential is created under. */ +const WORKLOAD_IDENTITY_SERVICE_ACCOUNT_PROVIDER_IDS = { + aws: AWS_SERVICE_ACCOUNT_PROVIDER_ID, + databricks: DATABRICKS_SERVICE_ACCOUNT_PROVIDER_ID, + snowflake: SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID, + microsoft: MICROSOFT_APP_SERVICE_ACCOUNT_PROVIDER_ID, + google: GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, +} as const satisfies Record + +export type WorkloadIdentityServiceAccountProviderId = + (typeof WORKLOAD_IDENTITY_SERVICE_ACCOUNT_PROVIDER_IDS)[WorkloadIdentityProvider] + +/** + * What each provider's keyless `serviceAccountJson` holds besides the issued + * `subject` and `subjectProof`: its `authType`, and the values the customer + * supplies from their own cloud setup. + */ +export const WORKLOAD_IDENTITY_KEYLESS_CONFIGS: Record< + WorkloadIdentityProvider, + { + authType: string + requiredFields: readonly string[] + optionalFields: readonly string[] + /** Where a keyless credential works, when that is narrower than the provider. */ + scope?: string + } +> = { + aws: { + authType: 'oidc_role', + requiredFields: ['roleArn'], + optionalFields: ['stsRegion', 'sessionDurationSeconds'], + }, + databricks: { + authType: 'oidc_federation', + requiredFields: ['host', 'clientId'], + optionalFields: [], + }, + snowflake: { authType: 'workload_identity', requiredFields: ['host'], optionalFields: [] }, + microsoft: { + authType: 'federated', + requiredFields: ['tenantId', 'clientId'], + optionalFields: ['environmentUrl'], + }, + google: { + authType: 'workload_identity', + requiredFields: ['projectNumber', 'poolId', 'providerId', 'serviceAccountEmail'], + optionalFields: [], + scope: + 'Keyless works only for Google Cloud services such as BigQuery and Vertex AI, not Google Workspace (Gmail, Drive, Sheets, Calendar, and the like), which need a JSON key with domain-wide delegation.', + }, +} + +/** Every service-account provider ID that can hold a keyless credential. */ +export const WORKLOAD_IDENTITY_SERVICE_ACCOUNT_PROVIDER_ID_LIST = WORKLOAD_IDENTITY_PROVIDERS.map( + (provider) => WORKLOAD_IDENTITY_SERVICE_ACCOUNT_PROVIDER_IDS[provider] +) as [WorkloadIdentityServiceAccountProviderId, ...WorkloadIdentityServiceAccountProviderId[]] + +const PROVIDER_BY_SERVICE_ACCOUNT_PROVIDER_ID: ReadonlyMap = + new Map( + WORKLOAD_IDENTITY_PROVIDERS.map((provider) => [ + WORKLOAD_IDENTITY_SERVICE_ACCOUNT_PROVIDER_IDS[provider], + provider, + ]) + ) + +/** Whether a service-account provider can hold a keyless credential. */ +export function isWorkloadIdentityServiceAccountProviderId( + providerId: string | null | undefined +): providerId is WorkloadIdentityServiceAccountProviderId { + return providerId != null && PROVIDER_BY_SERVICE_ACCOUNT_PROVIDER_ID.has(providerId) +} + +/** The relying party a keyless service-account provider ID trusts Sim through. */ +export function workloadIdentityProviderFor( + providerId: WorkloadIdentityServiceAccountProviderId +): WorkloadIdentityProvider { + const provider = PROVIDER_BY_SERVICE_ACCOUNT_PROVIDER_ID.get(providerId) + if (!provider) throw new Error(`No workload identity provider for ${providerId}`) + return provider +} + +/** + * Relying parties that expect one audience for every credential. Google expects + * each credential's own workload identity provider as its audience (see + * `googleWorkloadIdentityAudience`), so it has no entry here. + */ +type FixedAudienceWorkloadIdentityProvider = Exclude + +/** + * The `aud` claim each relying party expects. AWS STS requires its own client + * ID; Databricks federation policies list whatever audience the customer + * enters, so Sim publishes one fixed value for them to paste. Snowflake accepts + * `snowflakecomputing.com` without any `OIDC_AUDIENCE_LIST` on the user, so the + * customer's `CREATE USER` names only the issuer and subject. Microsoft Entra + * federated identity credentials take one audience, and Microsoft recommends + * `api://AzureADTokenExchange` + * (https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-create-trust). + */ +export const WORKLOAD_IDENTITY_AUDIENCES: Record = { + aws: 'sts.amazonaws.com', + databricks: 'sim-databricks', + snowflake: 'snowflakecomputing.com', + microsoft: 'api://AzureADTokenExchange', +} + +/** The provider's fixed audience, or undefined when each credential has its own (Google). */ +export function fixedWorkloadIdentityAudience( + provider: WorkloadIdentityProvider +): string | undefined { + return provider === 'google' ? undefined : WORKLOAD_IDENTITY_AUDIENCES[provider] +} diff --git a/apps/sim/lib/credentials/workload-identity/issuer.test.ts b/apps/sim/lib/credentials/workload-identity/issuer.test.ts new file mode 100644 index 00000000000..88da9a3419a --- /dev/null +++ b/apps/sim/lib/credentials/workload-identity/issuer.test.ts @@ -0,0 +1,192 @@ +/** + * @vitest-environment node + */ +import { generateKeyPairSync } from 'node:crypto' +import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from 'jose' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { env } from '@/lib/core/config/env' + +/** The shared env mock is mutable at runtime; its declared type is the real readonly env. */ +function setEnv(key: string, value: string | undefined) { + Reflect.set(env, key, value) +} + +function rsaPem() { + return generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey.export({ + type: 'pkcs8', + format: 'pem', + }) as string +} + +async function loadIssuer() { + vi.resetModules() + return import('@/lib/credentials/workload-identity/issuer') +} + +describe('workload identity issuer', () => { + beforeEach(() => { + setEnv('WORKLOAD_IDENTITY_SIGNING_KEY', rsaPem()) + setEnv('WORKLOAD_IDENTITY_ISSUER', 'https://sim.example.com/oidc/workload') + }) + + afterEach(() => { + setEnv('WORKLOAD_IDENTITY_SIGNING_KEY', undefined) + setEnv('WORKLOAD_IDENTITY_PREVIOUS_KEY', undefined) + setEnv('WORKLOAD_IDENTITY_ISSUER', undefined) + }) + + it('mints a token a relying party verifies with the published JWKS and discovery document', async () => { + const issuer = await loadIssuer() + const token = await issuer.mintWorkloadIdentityToken({ + subject: 'sim:workspace:ws-1:credential:abc123def456', + audience: 'sts.amazonaws.com', + owner: { workspaceId: 'ws-1' }, + }) + const discovery = issuer.getWorkloadIdentityDiscoveryDocument() + const jwks = createLocalJWKSet(await issuer.getWorkloadIdentityJwks()) + + const { payload } = await jwtVerify(token, jwks, { + issuer: discovery.issuer, + audience: 'sts.amazonaws.com', + algorithms: discovery.id_token_signing_alg_values_supported, + }) + + expect(discovery.jwks_uri).toBe(`${discovery.issuer}/jwks`) + expect(payload.sub).toBe('sim:workspace:ws-1:credential:abc123def456') + expect(payload.sim_workspace_id).toBe('ws-1') + expect((payload.exp ?? 0) - (payload.iat ?? 0)).toBe(issuer.WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS) + }) + + it('refuses a token lifetime beyond an hour', async () => { + const issuer = await loadIssuer() + + await expect( + issuer.mintWorkloadIdentityToken({ + subject: 'sim:workspace:ws-1:credential:abc123def456', + audience: 'sim-databricks', + owner: { workspaceId: 'ws-1' }, + ttlSeconds: 7200, + }) + ).rejects.toThrow(/an hour/) + }) + + it.each([ + ['another workspace', 'sim:workspace:ws-2:credential:abc123def456'], + ['an organization', 'sim:organization:ws-1:credential:abc123def456'], + ['a lookalike workspace id', 'sim:workspace:ws-10:credential:abc123def456'], + ])('refuses to mint a subject owned by %s', async (_owner, subject) => { + const issuer = await loadIssuer() + + await expect( + issuer.mintWorkloadIdentityToken({ + subject, + audience: 'sts.amazonaws.com', + owner: { workspaceId: 'ws-1' }, + }) + ).rejects.toThrow(/another owner/) + }) + + it('rejects a token presented to a different audience', async () => { + const issuer = await loadIssuer() + const token = await issuer.mintWorkloadIdentityToken({ + subject: 'sim:workspace:ws-1:credential:abc123def456', + audience: 'sts.amazonaws.com', + owner: { workspaceId: 'ws-1' }, + }) + const jwks = createLocalJWKSet(await issuer.getWorkloadIdentityJwks()) + + await expect(jwtVerify(token, jwks, { audience: 'sim-databricks' })).rejects.toThrow() + }) + + it('keeps verifying tokens signed by the previous key during rotation', async () => { + const previousKey = rsaPem() + setEnv('WORKLOAD_IDENTITY_SIGNING_KEY', previousKey) + const before = await loadIssuer() + const oldToken = await before.mintWorkloadIdentityToken({ + subject: 'sim:workspace:ws-1:credential:abc123def456', + audience: 'sts.amazonaws.com', + owner: { workspaceId: 'ws-1' }, + }) + + setEnv('WORKLOAD_IDENTITY_SIGNING_KEY', rsaPem()) + setEnv('WORKLOAD_IDENTITY_PREVIOUS_KEY', previousKey) + const after = await loadIssuer() + const jwks = await after.getWorkloadIdentityJwks() + + expect(jwks.keys).toHaveLength(2) + await expect(jwtVerify(oldToken, createLocalJWKSet(jwks))).resolves.toBeTruthy() + expect(jwks.keys.map((key) => key.kid)).toContain(decodeProtectedHeader(oldToken).kid) + expect(jwks.keys.every((key) => key.d === undefined)).toBe(true) + }) + + it('treats a malformed signing key as not configured instead of advertising a broken issuer', async () => { + setEnv('WORKLOAD_IDENTITY_SIGNING_KEY', 'not a PEM key') + const issuer = await loadIssuer() + const { workloadIdentityDiscoveryResponse, workloadIdentityJwksResponse } = await import( + '@/lib/credentials/workload-identity/responses' + ) + + expect(await issuer.isWorkloadIdentityConfigured()).toBe(false) + expect((await workloadIdentityDiscoveryResponse()).status).toBe(404) + expect((await workloadIdentityJwksResponse()).status).toBe(404) + }) + + it.each([ + 'http://sim.example.com/oidc/workload', + 'https://sim.example.com:3000/oidc/workload', + 'https://sim.example.com/oidc/workload?tenant=1', + 'https://sim.example.com/oidc/workload#keys', + ])('stays off for an issuer no cloud can fetch (%s)', async (url) => { + setEnv('WORKLOAD_IDENTITY_ISSUER', url) + const issuer = await loadIssuer() + const { workloadIdentityDiscoveryResponse } = await import( + '@/lib/credentials/workload-identity/responses' + ) + + expect(await issuer.isWorkloadIdentityConfigured()).toBe(false) + expect((await workloadIdentityDiscoveryResponse()).status).toBe(404) + }) + + it('keeps signing with the current key when the retiring key is malformed', async () => { + setEnv('WORKLOAD_IDENTITY_PREVIOUS_KEY', 'not a PEM key') + const issuer = await loadIssuer() + const token = await issuer.mintWorkloadIdentityToken({ + subject: 'sim:workspace:ws-1:credential:abc123def456', + audience: 'sts.amazonaws.com', + owner: { workspaceId: 'ws-1' }, + }) + const jwks = await issuer.getWorkloadIdentityJwks() + + expect(await issuer.isWorkloadIdentityConfigured()).toBe(true) + await expect(jwtVerify(token, createLocalJWKSet(jwks))).resolves.toBeTruthy() + }) + + it('publishes nothing and refuses to sign when no signing key is configured', async () => { + setEnv('WORKLOAD_IDENTITY_SIGNING_KEY', undefined) + const issuer = await loadIssuer() + const { workloadIdentityDiscoveryResponse, workloadIdentityJwksResponse } = await import( + '@/lib/credentials/workload-identity/responses' + ) + + expect(await issuer.isWorkloadIdentityConfigured()).toBe(false) + const discovery = await workloadIdentityDiscoveryResponse() + expect(discovery.status).toBe(404) + // Never cached, so the issuer is discoverable as soon as a key is configured. + expect(discovery.headers.get('Cache-Control')).toBe('no-store') + expect((await workloadIdentityJwksResponse()).headers.get('Cache-Control')).toBe('no-store') + await expect( + issuer.mintWorkloadIdentityToken({ + subject: 'sim:workspace:ws-1:credential:abc123def456', + audience: 'sts.amazonaws.com', + owner: { workspaceId: 'ws-1' }, + }) + ).rejects.toMatchObject({ detailCode: 'WORKLOAD_IDENTITY_NOT_CONFIGURED' }) + }) + + it('uses the pinned issuer URL when one is configured', async () => { + setEnv('WORKLOAD_IDENTITY_ISSUER', 'https://oidc.example.com/workload/') + const issuer = await loadIssuer() + + expect(issuer.getWorkloadIdentityIssuer()).toBe('https://oidc.example.com/workload') + }) +}) diff --git a/apps/sim/lib/credentials/workload-identity/issuer.ts b/apps/sim/lib/credentials/workload-identity/issuer.ts new file mode 100644 index 00000000000..135a36db6a1 --- /dev/null +++ b/apps/sim/lib/credentials/workload-identity/issuer.ts @@ -0,0 +1,214 @@ +import { createPrivateKey, createPublicKey, type KeyObject } from 'node:crypto' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { calculateJwkThumbprint, type JWK, SignJWT } from 'jose' +import { env } from '@/lib/core/config/env' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { + assertWorkloadIdentitySubjectOwner, + WorkloadIdentityNotConfiguredError, +} from '@/lib/credentials/workload-identity/subject' + +/** + * Sim's workload-identity issuer: a minimal OpenID Connect token issuer that + * lets a customer's cloud trust Sim directly, so a credential can assume an + * AWS role (or federate into another cloud) without any long-lived key stored + * in Sim. + * + * It is deliberately separate from the Better Auth OAuth server, which is an + * opaque-token API-authorization surface with JWT/JWKS disabled. This issuer + * only signs short-lived tokens for Sim's own server-side use while a saved + * credential resolves; tokens never reach a block output, the model, or a + * browser. + */ + +const logger = createLogger('WorkloadIdentityIssuer') + +export const WORKLOAD_IDENTITY_PATH = '/oidc/workload' +export const WORKLOAD_IDENTITY_ALGORITHM = 'RS256' +export const WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS = 300 +/** The longest lifetime a relying party may ask for; Databricks caps its own token at an hour. */ +export const MAX_WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS = 3600 + +const PRIVATE_JWK_FIELDS = ['d', 'p', 'q', 'dp', 'dq', 'qi'] as const + +interface SigningMaterial { + privateKey: KeyObject + kid: string + jwks: { keys: JWK[] } +} + +/** Env values often arrive with escaped newlines; PEM parsing needs real ones. */ +function normalizePem(value: string): string { + return value.includes('\\n') ? value.replace(/\\n/g, '\n') : value +} + +async function toPublicJwk(key: KeyObject): Promise { + const publicKey = key.type === 'public' ? key : createPublicKey(key) + const exported = publicKey.export({ format: 'jwk' }) as JWK + for (const field of PRIVATE_JWK_FIELDS) delete exported[field] + if (exported.kty !== 'RSA') { + throw new Error('Workload identity keys must be RSA keys') + } + const kid = await calculateJwkThumbprint(exported, 'sha256') + return { ...exported, kid, alg: WORKLOAD_IDENTITY_ALGORITHM, use: 'sig' } +} + +/** + * The retiring key's public JWK, or null when it is unset or unusable. A bad + * retiring key only drops that key from the JWKS; the current key keeps signing. + */ +async function loadPreviousPublicJwk(): Promise { + const previousKey = env.WORKLOAD_IDENTITY_PREVIOUS_KEY + if (!previousKey) return null + try { + return await toPublicJwk(createPublicKey(normalizePem(previousKey))) + } catch (error) { + logger.error('Workload identity previous key is set but unusable', { error }) + return null + } +} + +/** + * AWS and Databricks fetch discovery and keys from the issuer over https on the + * default port, and discovery appends paths to it, so any other issuer (or one + * with a query or fragment) can never be trusted; keyless stays off + * rather than advertising an issuer no relying party accepts. + */ +function assertPublicIssuer(issuer: string): void { + const url = new URL(issuer) + if (url.protocol !== 'https:' || url.port || url.search || url.hash) { + throw new Error( + 'Workload identity issuer must be an https URL without a port, query, or fragment' + ) + } + if (!env.WORKLOAD_IDENTITY_ISSUER?.trim()) { + logger.warn( + 'Workload identity issuer follows the app URL; set WORKLOAD_IDENTITY_ISSUER so customer trust policies survive a URL change', + { issuer } + ) + } +} + +let signingMaterial: Promise | undefined + +/** + * Loads the signing key once per process. The key is immutable config (it only + * changes on redeploy), so this is a memoized derivation rather than a cache, + * and a malformed key is parsed and reported once rather than per request. + */ +function loadSigningMaterial(): Promise { + const signingKey = env.WORKLOAD_IDENTITY_SIGNING_KEY + if (!signingKey) return Promise.reject(new WorkloadIdentityNotConfiguredError()) + if (!signingMaterial) { + signingMaterial = (async () => { + assertPublicIssuer(getWorkloadIdentityIssuer()) + const privateKey = createPrivateKey(normalizePem(signingKey)) + const current = await toPublicJwk(privateKey) + const keys = [current] + const previous = await loadPreviousPublicJwk() + if (previous && previous.kid !== current.kid) keys.push(previous) + return { privateKey, kid: current.kid as string, jwks: { keys } } + })() + signingMaterial.catch((error: unknown) => { + logger.error( + 'Workload identity is configured but unusable (check the signing key and issuer)', + { + error, + } + ) + }) + } + return signingMaterial +} + +/** + * Whether this deployment can mint workload-identity tokens: a signing key is + * set and parses, so a malformed key never advertises an issuer that can't sign. + */ +export async function isWorkloadIdentityConfigured(): Promise { + if (!env.WORKLOAD_IDENTITY_SIGNING_KEY) return false + return loadSigningMaterial().then( + () => true, + () => false + ) +} + +/** The issuer URL customers register as an OIDC identity provider. Stable for the deployment's life. */ +export function getWorkloadIdentityIssuer(): string { + const configured = env.WORKLOAD_IDENTITY_ISSUER?.trim() + return (configured || `${getBaseUrl()}${WORKLOAD_IDENTITY_PATH}`).replace(/\/+$/, '') +} + +/** OpenID Connect discovery document for the issuer. */ +export function getWorkloadIdentityDiscoveryDocument() { + const issuer = getWorkloadIdentityIssuer() + return { + issuer, + jwks_uri: `${issuer}/jwks`, + response_types_supported: ['id_token'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: [WORKLOAD_IDENTITY_ALGORITHM], + scopes_supported: ['openid'], + claims_supported: [ + 'iss', + 'sub', + 'aud', + 'exp', + 'iat', + 'nbf', + 'jti', + 'sim_workspace_id', + 'sim_organization_id', + ], + } +} + +/** Public signing keys, including a retiring key during rotation. */ +export async function getWorkloadIdentityJwks(): Promise<{ keys: JWK[] }> { + return (await loadSigningMaterial()).jwks +} + +export interface WorkloadIdentityOwner { + workspaceId?: string + organizationId?: string +} + +export interface MintWorkloadIdentityTokenInput { + /** Fully-qualified subject; see `buildWorkloadIdentitySubject`. */ + subject: string + /** Relying party, e.g. `sts.amazonaws.com`. */ + audience: string + owner: WorkloadIdentityOwner + /** + * Lifetime in seconds, for a relying party whose own token inherits it + * (Databricks). Defaults to {@link WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS}. + */ + ttlSeconds?: number +} + +/** Signs one short-lived token for a single credential resolution. */ +export async function mintWorkloadIdentityToken( + input: MintWorkloadIdentityTokenInput +): Promise { + assertWorkloadIdentitySubjectOwner(input.subject, input.owner) + const ttl = input.ttlSeconds ?? WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS + if (!Number.isInteger(ttl) || ttl <= 0 || ttl > MAX_WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS) { + throw new Error('Workload identity token lifetime must be between 1 second and an hour') + } + const { privateKey, kid } = await loadSigningMaterial() + const claims: Record = {} + if (input.owner.workspaceId) claims.sim_workspace_id = input.owner.workspaceId + if (input.owner.organizationId) claims.sim_organization_id = input.owner.organizationId + const now = Math.floor(Date.now() / 1000) + return new SignJWT(claims) + .setProtectedHeader({ alg: WORKLOAD_IDENTITY_ALGORITHM, kid, typ: 'JWT' }) + .setIssuer(getWorkloadIdentityIssuer()) + .setSubject(input.subject) + .setAudience(input.audience) + .setIssuedAt(now) + .setNotBefore(now - 30) + .setExpirationTime(now + ttl) + .setJti(generateId()) + .sign(privateKey) +} diff --git a/apps/sim/lib/credentials/workload-identity/responses.ts b/apps/sim/lib/credentials/workload-identity/responses.ts new file mode 100644 index 00000000000..6dbc7138154 --- /dev/null +++ b/apps/sim/lib/credentials/workload-identity/responses.ts @@ -0,0 +1,41 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { + getWorkloadIdentityDiscoveryDocument, + getWorkloadIdentityJwks, + isWorkloadIdentityConfigured, +} from '@/lib/credentials/workload-identity/issuer' + +const logger = createLogger('WorkloadIdentityDiscovery') + +/** + * Cloud providers fetch these anonymously (AWS STS, Databricks) and + * cache them; five minutes keeps a key rotation visible quickly. "No issuer + * here" is a 404 rather than an empty document, so a self-hosted deployment + * without a signing key never advertises one. + */ +const DISCOVERY_HEADERS = { + 'Cache-Control': 'public, max-age=300', + 'Access-Control-Allow-Origin': '*', +} as const + +function notConfigured() { + return new NextResponse(null, { status: 404, headers: { 'Cache-Control': 'no-store' } }) +} + +/** The issuer's OpenID discovery document, or 404 when this deployment has no usable signing key. */ +export async function workloadIdentityDiscoveryResponse() { + if (!(await isWorkloadIdentityConfigured())) return notConfigured() + return NextResponse.json(getWorkloadIdentityDiscoveryDocument(), { headers: DISCOVERY_HEADERS }) +} + +/** The issuer's public signing keys (JWKS), or 404 when this deployment has no usable signing key. */ +export async function workloadIdentityJwksResponse() { + const jwks = await getWorkloadIdentityJwks().catch((error: unknown) => { + // Still a 404 to the provider, but a broken signing key must not look like an unconfigured one. + logger.warn('Could not build the workload identity JWKS', { error }) + return null + }) + if (!jwks) return notConfigured() + return NextResponse.json(jwks, { headers: DISCOVERY_HEADERS }) +} diff --git a/apps/sim/lib/credentials/workload-identity/subject.test.ts b/apps/sim/lib/credentials/workload-identity/subject.test.ts new file mode 100644 index 00000000000..079ebf8fbff --- /dev/null +++ b/apps/sim/lib/credentials/workload-identity/subject.test.ts @@ -0,0 +1,288 @@ +/** + * @vitest-environment node + */ +import { flattenMockConditions } from '@sim/testing' +import { encryptionMock, encryptionMockFns } from '@sim/testing/mocks/encryption.mock' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/encryption', () => encryptionMock) + +import { + assertWorkloadIdentitySubjectUnclaimed, + bindWorkloadIdentitySubject, + issueWorkloadIdentitySubject, + verifyWorkloadIdentitySubject, + WorkloadIdentitySubjectError, +} from '@/lib/credentials/workload-identity/subject' +import type { DbTransaction } from '@/lib/db/types' + +const owner = { workspaceId: 'ws-1' } +const now = Date.UTC(2026, 10, 6) + +describe('workload identity subjects', () => { + it('accepts the subject and proof Sim issued to this person for this workspace', () => { + const grant = issueWorkloadIdentitySubject('aws', owner, 'user-1', now) + + expect(grant.subject).toMatch(/^sim:workspace:ws-1:credential:[A-Za-z0-9_-]+$/) + expect(() => + verifyWorkloadIdentitySubject('aws', owner, 'user-1', grant.subject, grant.subjectProof, now) + ).not.toThrow() + }) + + it('refuses another member reusing a subject they learned from a trust policy', () => { + const victim = issueWorkloadIdentitySubject('aws', owner, 'user-1', now) + const attacker = issueWorkloadIdentitySubject('aws', owner, 'user-2', now) + + expect(() => + verifyWorkloadIdentitySubject( + 'aws', + owner, + 'user-2', + victim.subject, + attacker.subjectProof, + now + ) + ).toThrow(WorkloadIdentitySubjectError) + expect(() => + verifyWorkloadIdentitySubject( + 'aws', + owner, + 'user-2', + victim.subject, + victim.subjectProof, + now + ) + ).toThrow(WorkloadIdentitySubjectError) + }) + + it('refuses a proof presented in another workspace', () => { + const grant = issueWorkloadIdentitySubject('aws', owner, 'user-1', now) + + expect(() => + verifyWorkloadIdentitySubject( + 'aws', + { workspaceId: 'ws-2' }, + 'user-1', + grant.subject, + grant.subjectProof, + now + ) + ).toThrow(WorkloadIdentitySubjectError) + }) + + it('refuses a proof issued for another provider', () => { + const grant = issueWorkloadIdentitySubject('aws', owner, 'user-1', now) + + expect(() => + verifyWorkloadIdentitySubject( + 'databricks', + owner, + 'user-1', + grant.subject, + grant.subjectProof, + now + ) + ).toThrow(WorkloadIdentitySubjectError) + }) + + it('refuses an expired proof with a code that says to request a new subject', () => { + const grant = issueWorkloadIdentitySubject('aws', owner, 'user-1', now) + const later = now + 24 * 60 * 60 * 1000 + 1 + + expect(() => + verifyWorkloadIdentitySubject( + 'aws', + owner, + 'user-1', + grant.subject, + grant.subjectProof, + later + ) + ).toThrow(expect.objectContaining({ detailCode: 'WORKLOAD_IDENTITY_PROOF_EXPIRED' })) + }) + + it('refuses a tampered or malformed proof', () => { + const grant = issueWorkloadIdentitySubject('aws', owner, 'user-1', now) + const [expiresAt] = grant.subjectProof.split('.') + + for (const proof of [ + `${Number(expiresAt) + 1000}.${grant.subjectProof.split('.')[1]}`, + `${expiresAt}.forged`, + 'no-separator', + '', + ]) { + expect(() => + verifyWorkloadIdentitySubject('aws', owner, 'user-1', grant.subject, proof, now) + ).toThrow(expect.objectContaining({ detailCode: 'WORKLOAD_IDENTITY_PROOF_INVALID' })) + } + }) +}) + +describe('binding a subject to a saved credential', () => { + const actor = { ...owner, actorUserId: 'user-1' } + + it('keeps the stored subject when a reconnect submits none', () => { + expect( + bindWorkloadIdentitySubject({ + provider: 'aws', + owner: actor, + storedSubject: 'sim:workspace:ws-1:credential:stored123456', + subject: undefined, + subjectProof: undefined, + }) + ).toBe('sim:workspace:ws-1:credential:stored123456') + }) + + it('refuses a keyless credential with neither a stored nor a proven subject', () => { + expect(() => + bindWorkloadIdentitySubject({ + provider: 'aws', + owner: actor, + storedSubject: undefined, + subject: undefined, + subjectProof: undefined, + }) + ).toThrow(WorkloadIdentitySubjectError) + }) + + it('accepts a new subject only with a proof issued to the person saving it', () => { + const grant = issueWorkloadIdentitySubject('databricks', owner, 'user-1') + + expect( + bindWorkloadIdentitySubject({ + provider: 'databricks', + owner: actor, + storedSubject: 'sim:workspace:ws-1:credential:stored123456', + subject: grant.subject, + subjectProof: grant.subjectProof, + }) + ).toBe(grant.subject) + expect(() => + bindWorkloadIdentitySubject({ + provider: 'databricks', + owner: { ...owner, actorUserId: 'user-2' }, + storedSubject: undefined, + subject: grant.subject, + subjectProof: grant.subjectProof, + }) + ).toThrow(WorkloadIdentitySubjectError) + expect(() => + bindWorkloadIdentitySubject({ + provider: 'databricks', + owner: actor, + storedSubject: undefined, + subject: grant.subject, + subjectProof: undefined, + }) + ).toThrow(WorkloadIdentitySubjectError) + }) +}) + +describe('keyless subject uniqueness across providers', () => { + const SUBJECT = 'sim:workspace:ws-1:credential:abcdef0123456789' + + interface StoredCredential { + id: string + providerId: string + secret: object + } + + /** + * A transaction whose credential query returns only the rows its provider + * filter admits, so a provider missing from the scan is never checked. + */ + function transactionWith(rows: StoredCredential[]): DbTransaction { + encryptionMockFns.mockDecryptSecret.mockImplementation(async (encrypted: string) => ({ + decrypted: JSON.stringify(rows.find((row) => row.id === encrypted)?.secret), + })) + const tx = { + execute: async () => [], + select: () => ({ + from: () => ({ + where: async (condition: unknown) => { + const providerFilter = flattenMockConditions(condition).find( + (node) => node.type === 'inArray' + ) + const admitted = Array.isArray(providerFilter?.values) ? providerFilter.values : [] + return rows + .filter((row) => admitted.includes(row.providerId)) + .map((row) => ({ + id: row.id, + displayName: row.id, + encryptedServiceAccountKey: row.id, + })) + }, + }), + }), + } + return tx as unknown as DbTransaction + } + + function claim(rows: StoredCredential[]) { + return assertWorkloadIdentitySubjectUnclaimed(transactionWith(rows), { + subject: SUBJECT, + scope: { kind: 'workspace', workspaceId: 'ws-1' }, + }) + } + + beforeEach(() => { + encryptionMockFns.mockDecryptSecret.mockReset() + }) + + it.each([ + { + providerId: 'snowflake-service-account', + secret: { + type: 'snowflake_workload_identity_v1', + config: { + authType: 'workload_identity', + host: 'org-acct.snowflakecomputing.com', + subject: SUBJECT, + }, + }, + }, + { + providerId: 'microsoft-app-service-account', + secret: { + type: 'microsoft_app_credential_v1', + config: { authType: 'federated', tenantId: 't', clientId: 'c', subject: SUBJECT }, + }, + }, + { + providerId: 'google-service-account', + secret: { + type: 'google_workload_identity_v1', + config: { projectNumber: '1', poolId: 'pool', providerId: 'prov', subject: SUBJECT }, + }, + }, + ])( + 'refuses a subject a live $providerId credential already holds', + async ({ providerId, secret }) => { + await expect(claim([{ id: 'held', providerId, secret }])).rejects.toThrow( + /already uses this trust setup/ + ) + } + ) + + it('is not blocked by key-based Google and Snowflake credentials, which hold no subject', async () => { + await expect( + claim([ + { + id: 'json-key', + providerId: 'google-service-account', + secret: { type: 'service_account', client_email: 'sa@p.iam.gserviceaccount.com' }, + }, + { + id: 'pat', + providerId: 'snowflake-service-account', + secret: { type: 'token_service_account', apiToken: 'pat', domain: 'x' }, + }, + { + id: 'client-secret', + providerId: 'microsoft-app-service-account', + secret: { type: 'microsoft_app_credential_v1', config: { authType: 'client_secret' } }, + }, + ]) + ).resolves.toBeUndefined() + }) +}) diff --git a/apps/sim/lib/credentials/workload-identity/subject.ts b/apps/sim/lib/credentials/workload-identity/subject.ts new file mode 100644 index 00000000000..8ca02e20176 --- /dev/null +++ b/apps/sim/lib/credentials/workload-identity/subject.ts @@ -0,0 +1,394 @@ +import { createHmac } from 'node:crypto' +import { credential } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray, isNull, ne } from 'drizzle-orm' +import { z } from 'zod' +import { env } from '@/lib/core/config/env' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import type { ResourceScope } from '@/lib/core/resource-scope' +import { resourceScopeCondition } from '@/lib/core/resource-scope.server' +import { decryptSecret } from '@/lib/core/security/encryption' +import { TOKEN_SERVICE_ACCOUNT_SECRET_TYPE } from '@/lib/credentials/token-service-accounts/descriptors' +import { + WORKLOAD_IDENTITY_SERVICE_ACCOUNT_PROVIDER_ID_LIST, + type WorkloadIdentityProvider, +} from '@/lib/credentials/workload-identity/audiences' +import type { WorkloadIdentityOwner } from '@/lib/credentials/workload-identity/issuer' +import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks' +import type { DbTransaction } from '@/lib/db/types' + +const logger = createLogger('WorkloadIdentitySubject') + +/** The `type` of a Google JSON key, stored verbatim. */ +const GOOGLE_SERVICE_ACCOUNT_KEY_TYPE = 'service_account' + +/** + * Credential-scoped workload-identity subjects. + * + * A customer's trust policy pins one subject, so the subject — not the + * workspace — is what a cloud role trusts. Sim generates the random part and + * signs it for the preparing user and owner: a member who learns another + * credential's subject (it is visible in that customer's trust policy) cannot + * attach it to a credential of their own, because only Sim can produce a proof + * for it, and only for the person it was issued to. + */ + +const SUBJECT_PROOF_TTL_MS = 24 * 60 * 60 * 1000 +const SUBJECT_ID_PATTERN = /^[A-Za-z0-9_-]{8,64}$/ + +/** Who owns a credential with a bound subject, and who is saving it. */ +export interface WorkloadIdentityCredentialOwner extends WorkloadIdentityOwner { + /** The person creating or reconnecting the credential. */ + actorUserId: string + /** Decrypted secret of the credential being reconnected, if any. */ + storedSecret?: Record | null +} + +/** Why a keyless credential was refused; every surface reports it as `error.details.code`. */ +type KeylessCredentialErrorCode = + /** This deployment has no workload-identity signing key; use a key-based credential. */ + | 'WORKLOAD_IDENTITY_NOT_CONFIGURED' + /** The proof is more than a day old; request a new subject. */ + | 'WORKLOAD_IDENTITY_PROOF_EXPIRED' + /** The proof was issued to another user, workspace, or provider, or was altered. */ + | 'WORKLOAD_IDENTITY_PROOF_INVALID' + /** No subject was submitted and the credential holds none to keep. */ + | 'WORKLOAD_IDENTITY_SUBJECT_REQUIRED' + /** Another live credential already holds the subject; request a new one. */ + | 'WORKLOAD_IDENTITY_SUBJECT_IN_USE' + /** A sibling credential could not be read, so the subject's uniqueness is unknown. */ + | 'WORKLOAD_IDENTITY_SUBJECT_UNVERIFIABLE' + +/** Refusals caused by another credential's state rather than by the request itself. */ +const KEYLESS_CONFLICT_CODES: ReadonlySet = new Set([ + 'WORKLOAD_IDENTITY_SUBJECT_IN_USE', + 'WORKLOAD_IDENTITY_SUBJECT_UNVERIFIABLE', +]) + +/** + * A keyless credential Sim refused. It travels as an orchestration failure, so + * every surface maps its status the usual way (`409` for a conflict with + * another credential, otherwise `400`) and adds `detailCode` as `details.code`. + */ +export class KeylessCredentialRefusedError extends OrchestrationError { + constructor( + message: string, + readonly detailCode: KeylessCredentialErrorCode + ) { + super(KEYLESS_CONFLICT_CODES.has(detailCode) ? 'conflict' : 'validation', message) + this.name = 'KeylessCredentialRefusedError' + } +} + +/** A submitted or stored subject that cannot be bound to the credential being saved. */ +export class WorkloadIdentitySubjectError extends KeylessCredentialRefusedError { + constructor( + message: string, + detailCode: Exclude< + KeylessCredentialErrorCode, + 'WORKLOAD_IDENTITY_NOT_CONFIGURED' + > = 'WORKLOAD_IDENTITY_PROOF_INVALID' + ) { + super(message, detailCode) + this.name = 'WorkloadIdentitySubjectError' + } +} + +/** + * This deployment has no usable signing key, so it cannot issue or verify + * keyless credentials. + * + * @param remedy The provider's key-based alternative, as one sentence. + */ +export class WorkloadIdentityNotConfiguredError extends KeylessCredentialRefusedError { + constructor(remedy = 'Use a key- or token-based credential.') { + super( + `Keyless connections are not available on this deployment. ${remedy}`, + 'WORKLOAD_IDENTITY_NOT_CONFIGURED' + ) + this.name = 'WorkloadIdentityNotConfiguredError' + } +} + +function ownerSegment(owner: WorkloadIdentityOwner): string { + if (owner.workspaceId && !owner.organizationId) return `workspace:${owner.workspaceId}` + if (owner.organizationId && !owner.workspaceId) return `organization:${owner.organizationId}` + throw new WorkloadIdentitySubjectError('A workload identity subject needs exactly one owner') +} + +function subjectPrefix(owner: WorkloadIdentityOwner): string { + return `sim:${ownerSegment(owner)}:credential:` +} + +/** Throws unless the subject was built for this owner. */ +export function assertWorkloadIdentitySubjectOwner( + subject: string, + owner: WorkloadIdentityOwner +): void { + if (!subject.startsWith(subjectPrefix(owner))) { + throw new WorkloadIdentitySubjectError('Workload identity subject belongs to another owner') + } +} + +/** `sim:workspace::credential:` (or `sim:organization:…`). */ +function buildWorkloadIdentitySubject(owner: WorkloadIdentityOwner, subjectId: string): string { + if (!SUBJECT_ID_PATTERN.test(subjectId)) { + throw new WorkloadIdentitySubjectError('Invalid workload identity subject') + } + return `${subjectPrefix(owner)}${subjectId}` +} + +function proofKey(): Buffer { + return createHmac('sha256', env.BETTER_AUTH_SECRET) + .update('sim/workload-identity-subject/v1') + .digest() +} + +function sign( + provider: WorkloadIdentityProvider, + owner: WorkloadIdentityOwner, + userId: string, + subject: string, + expiresAt: number +) { + return createHmac('sha256', proofKey()) + .update(`${provider}|${ownerSegment(owner)}|${userId}|${subject}|${expiresAt}`, 'utf8') + .digest('base64url') +} + +/** A fresh subject for this owner that no credential holds yet. */ +export function newWorkloadIdentitySubject(owner: WorkloadIdentityOwner): string { + return buildWorkloadIdentitySubject(owner, generateId().replace(/-/g, '')) +} + +export interface WorkloadIdentitySubjectGrant { + subject: string + subjectProof: string + expiresAt: string +} + +/** Issues a fresh subject the given user can attach to a new credential of one provider. */ +export function issueWorkloadIdentitySubject( + provider: WorkloadIdentityProvider, + owner: WorkloadIdentityOwner, + userId: string, + now: number = Date.now() +): WorkloadIdentitySubjectGrant { + const subject = newWorkloadIdentitySubject(owner) + const expiresAt = now + SUBJECT_PROOF_TTL_MS + return { + subject, + subjectProof: `${expiresAt}.${sign(provider, owner, userId, subject, expiresAt)}`, + expiresAt: new Date(expiresAt).toISOString(), + } +} + +/** + * Throws unless the proof was issued by Sim for this provider, owner, user, and + * subject and has not expired. + */ +export function verifyWorkloadIdentitySubject( + provider: WorkloadIdentityProvider, + owner: WorkloadIdentityOwner, + userId: string, + subject: string, + subjectProof: string, + now: number = Date.now() +): void { + const invalid = () => + new WorkloadIdentitySubjectError( + 'This trust setup has expired or belongs to someone else. Start the keyless connection again.' + ) + const separator = subjectProof.indexOf('.') + if (separator <= 0) throw invalid() + const expiresAt = Number(subjectProof.slice(0, separator)) + const signature = subjectProof.slice(separator + 1) + if (!Number.isSafeInteger(expiresAt)) throw invalid() + if (expiresAt <= now) { + throw new WorkloadIdentitySubjectError( + 'This trust setup has expired. Start the keyless connection again to get a new subject.', + 'WORKLOAD_IDENTITY_PROOF_EXPIRED' + ) + } + if (!subject.startsWith(subjectPrefix(owner))) throw invalid() + if (!safeCompare(signature, sign(provider, owner, userId, subject, expiresAt))) throw invalid() +} + +/** + * Decides which subject a keyless credential is saved with. + * + * - No subject submitted: a reconnect keeping the trust the customer already + * configured — the stored subject is reused, and there must be one. + * - The stored subject resubmitted: kept as-is. + * - Any other subject: must carry a fresh proof Sim issued to this person, for + * this owner and provider. + */ +export function bindWorkloadIdentitySubject(input: { + provider: WorkloadIdentityProvider + owner: WorkloadIdentityCredentialOwner + storedSubject: string | undefined + subject: string | undefined + subjectProof: string | undefined +}): string { + const { provider, owner, storedSubject, subject, subjectProof } = input + if (!subject || subject === storedSubject) { + if (storedSubject) return storedSubject + throw new WorkloadIdentitySubjectError( + 'Start a keyless connection to get a trust setup first.', + 'WORKLOAD_IDENTITY_SUBJECT_REQUIRED' + ) + } + if (!subjectProof) { + throw new WorkloadIdentitySubjectError( + 'Start a keyless connection to get a trust setup first.', + 'WORKLOAD_IDENTITY_SUBJECT_REQUIRED' + ) + } + verifyWorkloadIdentitySubject(provider, owner, owner.actorUserId, subject, subjectProof) + return subject +} + +/** The part of a keyless provider's stored secret that can hold a bound subject. */ +const storedSubjectSchema = z.object({ + config: z.object({ subject: z.string().optional() }), +}) + +/** + * Keyed secrets of those providers that have no `config` at all: a Google JSON + * key and a Snowflake programmatic access token. They hold no subject. + */ +const keyedSecretSchema = z.object({ + type: z.enum([GOOGLE_SERVICE_ACCOUNT_KEY_TYPE, TOKEN_SERVICE_ACCOUNT_SECRET_TYPE]), +}) + +/** The subject a stored secret holds, `undefined` for a keyed mode, or `null` when unreadable. */ +async function readStoredSubject(encrypted: string): Promise { + try { + const { decrypted } = await decryptSecret(encrypted) + const blob: unknown = JSON.parse(decrypted) + if (keyedSecretSchema.safeParse(blob).success) return undefined + const parsed = storedSubjectSchema.safeParse(blob) + return parsed.success ? parsed.data.config.subject : null + } catch { + return null + } +} + +function lockSubject(tx: DbTransaction, subject: string): Promise { + return acquireAdvisoryXactLock( + tx, + 'workload_identity_subject', + `workload-identity-subject:${subject}` + ) +} + +/** Names the owner without naming a credential, which the caller may not be able to see. */ +function scopeNoun(scope: ResourceScope): string { + return scope.kind === 'workspace' ? 'this workspace' : 'this organization' +} + +/** Throws unless no live keyless credential of the owner other than `credentialId` holds `subject`. */ +async function assertNoOtherCredentialHolds( + tx: DbTransaction, + input: { subject: string; scope: ResourceScope; credentialId?: string } +): Promise { + const rows = await tx + .select({ + id: credential.id, + encryptedServiceAccountKey: credential.encryptedServiceAccountKey, + }) + .from(credential) + .where( + and( + resourceScopeCondition(credential, input.scope), + inArray(credential.providerId, WORKLOAD_IDENTITY_SERVICE_ACCOUNT_PROVIDER_ID_LIST), + isNull(credential.revokedAt), + input.credentialId ? ne(credential.id, input.credentialId) : undefined + ) + ) + for (const row of rows) { + if (!row.encryptedServiceAccountKey) continue + const storedSubject = await readStoredSubject(row.encryptedServiceAccountKey) + if (storedSubject === null) { + logger.warn('Unreadable keyless credential blocks a subject uniqueness check', { + credentialId: row.id, + }) + throw new WorkloadIdentitySubjectError( + `Sim could not read another keyless credential in ${scopeNoun(input.scope)}, so it cannot confirm this trust setup is unused. Ask an admin to reconnect or delete the broken keyless credential, then try again.`, + 'WORKLOAD_IDENTITY_SUBJECT_UNVERIFIABLE' + ) + } + if (storedSubject === input.subject) { + throw new WorkloadIdentitySubjectError( + `Another credential in ${scopeNoun(input.scope)} already uses this trust setup. Start the keyless connection again.`, + 'WORKLOAD_IDENTITY_SUBJECT_IN_USE' + ) + } + } +} + +/** + * Refuses a subject that another live credential of the same owner already + * holds. A proof stays valid for a day and is not tied to one credential, so + * without this a member removed from a keyless credential could save a second + * credential with the same subject and keep assuming the customer's role. + * + * The subject lives only inside each credential's encrypted secret, so this + * decrypts the owner's keyless credentials (one owner holds few) rather than + * relying on a unique index. It runs inside the transaction that writes the + * credential: the advisory lock holds concurrent saves of one subject until + * commit, so the later save sees the earlier row. A sibling whose secret cannot + * be read might hold the subject, so it refuses the save rather than skip it. + * + * Caveats: uniqueness holds only within one owner (a subject embeds its owner, + * so another owner's credential cannot hold it), only among live credentials of + * the keyless-capable providers (`WORKLOAD_IDENTITY_SERVICE_ACCOUNT_PROVIDER_ID_LIST`: + * AWS, Databricks, Snowflake, Microsoft, and Google), and only for writers that call this inside + * their write transaction. The refusal never names the sibling, which the + * caller may not be able to see. + */ +export async function assertWorkloadIdentitySubjectUnclaimed( + tx: DbTransaction, + input: { subject: string; scope: ResourceScope } +): Promise { + await lockSubject(tx, input.subject) + await assertNoOtherCredentialHolds(tx, input) +} + +/** + * The reconnect counterpart of {@link assertWorkloadIdentitySubjectUnclaimed}, + * run in the transaction that writes `rotatedSecret` onto `credentialId`. + * + * Whether the reconnect keeps the credential's subject is decided here, from + * the row as it stands under the lock, not from the secret read before + * provider verification: while that verification ran, another reconnect could + * have moved this credential to a different subject and a second credential + * could have claimed the old one, so writing it back unchecked would leave two + * credentials holding it. Locks the subject first, then the credential row; no + * path takes a subject lock while holding a credential row, so the order cannot + * deadlock. + */ +export async function assertReconnectedSubjectUnclaimed( + tx: DbTransaction, + input: { rotatedSecret: string; scope: ResourceScope; credentialId: string } +): Promise { + const subject = await readStoredSubject(input.rotatedSecret) + if (!subject) return + await lockSubject(tx, subject) + const [current] = await tx + .select({ encryptedServiceAccountKey: credential.encryptedServiceAccountKey }) + .from(credential) + .where(eq(credential.id, input.credentialId)) + .for('update') + const currentSubject = current?.encryptedServiceAccountKey + ? await readStoredSubject(current.encryptedServiceAccountKey) + : undefined + if (currentSubject === subject) return + await assertNoOtherCredentialHolds(tx, { + subject, + scope: input.scope, + credentialId: input.credentialId, + }) +} diff --git a/apps/sim/lib/custom-tools/application/use-cases.test.ts b/apps/sim/lib/custom-tools/application/use-cases.test.ts index 9eb3d14594c..c4fe00d83ef 100644 --- a/apps/sim/lib/custom-tools/application/use-cases.test.ts +++ b/apps/sim/lib/custom-tools/application/use-cases.test.ts @@ -80,7 +80,12 @@ describe('custom tool application use cases', () => { it.each([ { kind: 'session' as const, userId: 'reader' }, - { kind: 'personal_api_key' as const, userId: 'reader', keyId: 'key-1' }, + { + kind: 'personal_api_key' as const, + userId: 'reader', + keyId: 'key-1', + access: 'write' as const, + }, ])( 'reads available tools as the $kind actor without billing-owner substitution', async (principal) => { @@ -291,6 +296,7 @@ describe('custom tool application use cases', () => { kind: 'workspace_api_key' as const, workspaceId: workspace.workspaceId, keyId: 'workspace-key-1', + access: 'write' as const, } const result = await createWorkspaceCustomToolUseCase.execute({ diff --git a/apps/sim/lib/data-drains/application/use-cases.test.ts b/apps/sim/lib/data-drains/application/use-cases.test.ts index 39f81292c8f..c5cfb0e06df 100644 --- a/apps/sim/lib/data-drains/application/use-cases.test.ts +++ b/apps/sim/lib/data-drains/application/use-cases.test.ts @@ -118,8 +118,8 @@ beforeEach(() => { describe('organization data drain application boundary', () => { it.each([ - { kind: 'workspace_api_key', workspaceId: 'org', keyId: 'key' }, - { kind: 'personal_api_key', userId: 'actor', keyId: 'key' }, + { kind: 'workspace_api_key', workspaceId: 'org', keyId: 'key', access: 'write' }, + { kind: 'personal_api_key', userId: 'actor', keyId: 'key', access: 'write' }, { ...principal, organizationId: 'elsewhere' }, { ...principal, audience: 'sim:knowledge' }, { ...principal, expiresAt: new Date(0) }, diff --git a/apps/sim/lib/desktop/source-request.integration.ts b/apps/sim/lib/desktop/source-request.integration.ts index 437cecdeb47..390e50aafc3 100644 --- a/apps/sim/lib/desktop/source-request.integration.ts +++ b/apps/sim/lib/desktop/source-request.integration.ts @@ -73,7 +73,12 @@ describe.skipIf(!redisUrl)('desktop source request transport', () => { it('rejects non-session callers and oversized requests', async () => { await expect( createDesktopSourceRequest.execute({ - principal: { kind: 'workspace_api_key', workspaceId: generateId(), keyId: generateId() }, + principal: { + kind: 'workspace_api_key', + workspaceId: generateId(), + keyId: generateId(), + access: 'write', + }, input: { requestId: generateShortId(32), payload: '{}' }, }) ).rejects.toThrow('Session authentication required') diff --git a/apps/sim/lib/execution/files.test.ts b/apps/sim/lib/execution/files.test.ts index d743ef80799..a51937ed304 100644 --- a/apps/sim/lib/execution/files.test.ts +++ b/apps/sim/lib/execution/files.test.ts @@ -535,7 +535,7 @@ describe('workflow input files', () => { } it.each([ { kind: 'session', userId: 'user', sessionId: 'session' }, - { kind: 'personal_api_key', userId: 'user', keyId: 'key' }, + { kind: 'personal_api_key', userId: 'user', keyId: 'key', access: 'write' }, { kind: 'oauth_access_token', userId: 'user', @@ -544,7 +544,7 @@ describe('workflow input files', () => { scopes: [], expiresAt: new Date(), }, - { kind: 'workspace_api_key', workspaceId, keyId: 'key' }, + { kind: 'workspace_api_key', workspaceId, keyId: 'key', access: 'write' }, { kind: 'delegated', serviceId: 'copilot', subjectUserId: 'user', ...delegation }, ])('lets authorized member principal $kind reference workspace files', (principal) => { expect(getStoredFileReferenceScope(principal)).toBe('workspace') diff --git a/apps/sim/lib/integrations/availability.server.oauth-configured.test.ts b/apps/sim/lib/integrations/availability.server.oauth-configured.test.ts index 89498c2d0ad..b350ea85153 100644 --- a/apps/sim/lib/integrations/availability.server.oauth-configured.test.ts +++ b/apps/sim/lib/integrations/availability.server.oauth-configured.test.ts @@ -14,12 +14,12 @@ setEnv({ afterAll(resetEnvMock) describe('OAuth service availability projection', () => { - it('uses the repository App while the workflow block keeps its API-key path', () => { + it('uses the repository App while the workflow block keeps its saved-token path', () => { expect( getOAuthServiceAvailability([{ providerId: 'github-repositories', authType: 'oauth' }]) ).toEqual([{ providerId: 'github-repositories', available: true }]) - expect(getIntegrationAvailability().find((item) => item.type === 'github_v2')).toMatchObject({ - state: 'ready', + expect(getIntegrationAvailability().find((item) => item.type === 'github_v3')).toMatchObject({ + state: 'limited', oauthAvailable: false, }) }) diff --git a/apps/sim/lib/integrations/availability.server.test.ts b/apps/sim/lib/integrations/availability.server.test.ts index c491cb07a33..35568c9d02a 100644 --- a/apps/sim/lib/integrations/availability.server.test.ts +++ b/apps/sim/lib/integrations/availability.server.test.ts @@ -44,8 +44,12 @@ function availabilityFor( } describe('integration availability', () => { - it('does not infer GitHub repository OAuth readiness from its API-key workflow block', () => { - expect(availabilityFor('github_v2')).toMatchObject({ state: 'ready', oauthAvailable: false }) + it('does not infer GitHub repository OAuth readiness from its saved-token workflow block', () => { + expect(availabilityFor('github_v3')).toMatchObject({ + state: 'limited', + oauthAvailable: false, + serviceAccountAvailable: true, + }) expect( getOAuthServiceAvailability([{ providerId: 'github-repositories', authType: 'oauth' }]) ).toEqual([{ providerId: 'github-repositories', available: false }]) diff --git a/apps/sim/lib/integrations/credential-display.test.ts b/apps/sim/lib/integrations/credential-display.test.ts index b05aae0889f..73ea26193ba 100644 --- a/apps/sim/lib/integrations/credential-display.test.ts +++ b/apps/sim/lib/integrations/credential-display.test.ts @@ -40,8 +40,11 @@ const EXPECTED_COVERAGE: Record = { 'claude-platform-service-account': ['claude-managed-agents'], 'clickup-service-account': ['clickup'], 'coda-service-account': ['coda'], + 'docusign-service-account': ['docusign'], 'fivetran-service-account': ['fivetran'], + 'bitbucket-service-account': ['bitbucket'], 'github-app-installation': ['github'], + 'github-service-account': ['github'], 'google-service-account': [ 'gmail', 'google-bigquery', @@ -99,19 +102,21 @@ const REGISTERED_SERVICE_ACCOUNT_IDS = [ describe('GitHub Search credentials', () => { it('applies GitHub integration policy without changing workflow token authentication', () => { - expect(getIntegrationTypesForOAuthServiceId('github-repositories')).toEqual(['github_v2']) - expect(getIntegrationTypesForOAuthServiceId('GITHUB-REPOSITORIES')).toEqual(['github_v2']) + expect(getIntegrationTypesForOAuthServiceId('github-repositories')).toEqual(['github_v3']) + expect(getIntegrationTypesForOAuthServiceId('GITHUB-REPOSITORIES')).toEqual(['github_v3']) expect(isOAuthServiceAllowedByIntegrationTypes('github-repositories', new Set(['slack']))).toBe( false ) expect( - isOAuthServiceAllowedByIntegrationTypes('github-repositories', new Set(['github_v2'])) + isOAuthServiceAllowedByIntegrationTypes('github-repositories', new Set(['github_v3'])) ).toBe(true) expect(isOAuthServiceAllowedByIntegrationTypes('github-repositories', null)).toBe(true) expect(getIntegrationsForCredentialProvider('github-repositories')).toEqual([ - expect.objectContaining({ slug: 'github', type: 'github_v2', authType: 'api-key' }), + expect.objectContaining({ slug: 'github', type: 'github_v3', authType: 'oauth' }), + ]) + expect(getIntegrationsForCredentialProvider('github-service-account')).toEqual([ + expect.objectContaining({ slug: 'github', type: 'github_v3' }), ]) - expect(getIntegrationsForCredentialProvider('github')).toEqual([]) }) }) @@ -134,6 +139,9 @@ describe('service-account coverage', () => { */ it('agrees with the predicate the Connected list actually filters on', () => { for (const providerId of REGISTERED_SERVICE_ACCOUNT_IDS) { + // GitHub App installations authorize Search, not workflows: they carry GitHub's + // brand and policy but are managed from Search, never the block's Connected list. + if (providerId === 'github-app-installation') continue const covered = new Set(getIntegrationsForCredentialProvider(providerId).map((i) => i.slug)) for (const integration of INTEGRATIONS) { diff --git a/apps/sim/lib/integrations/credential-display.ts b/apps/sim/lib/integrations/credential-display.ts index f65ef1128c0..83d47e54bbc 100644 --- a/apps/sim/lib/integrations/credential-display.ts +++ b/apps/sim/lib/integrations/credential-display.ts @@ -226,7 +226,7 @@ export function resolveCredentialDisplay(credential: DisplayableCredential): Cre return { service: null, integration, - blockType: 'gitlab', + blockType: integration?.type ?? 'gitlab', icon: GitlabIcon, familyName: null, coveredIntegrations: integration ? [integration] : [], diff --git a/apps/sim/lib/integrations/credential-visibility.server.test.ts b/apps/sim/lib/integrations/credential-visibility.server.test.ts index 566ce32c374..4a339ad23d5 100644 --- a/apps/sim/lib/integrations/credential-visibility.server.test.ts +++ b/apps/sim/lib/integrations/credential-visibility.server.test.ts @@ -235,6 +235,21 @@ describe('integration credential visibility', () => { expect(disabled.isCredentialVisible(credential)).toBe(false) }) + it('keeps GitLab personal tokens visible after the GitLab block moves to a new version', () => { + getIntegrationAvailabilityMock.mockReturnValue([ + availability('gitlab_v2', 'ready', { oauthAvailable: false, serviceAccountAvailable: true }), + ]) + const visibility = createIntegrationCredentialVisibility({ + allowedIntegrationTypes: new Set(['gitlab_v2']), + blockVisibility: null, + oauthServices: SERVICES, + }) + + expect(visibility.isCredentialVisible({ providerId: 'gitlab', type: 'personal_token' })).toBe( + true + ) + }) + it('leaves non-integration credentials visible', () => { const visibility = createIntegrationCredentialVisibility({ allowedIntegrationTypes: new Set(), diff --git a/apps/sim/lib/integrations/credential-visibility.server.ts b/apps/sim/lib/integrations/credential-visibility.server.ts index a38ae4bcc05..eb0770d5653 100644 --- a/apps/sim/lib/integrations/credential-visibility.server.ts +++ b/apps/sim/lib/integrations/credential-visibility.server.ts @@ -13,6 +13,7 @@ import { getGitHubInstallationConfiguration } from '@/lib/oauth/github-installat import { GITHUB_INSTALLATION_PROVIDER_ID } from '@/lib/oauth/github-installation-types' import type { OAuthServiceMetadata } from '@/lib/oauth/types' import { getAllOAuthServices } from '@/lib/oauth/utils' +import { resolveAccessControlBlockType } from '@/lib/permission-groups/integration-allowlist' import { getBlock } from '@/blocks/registry' import { isHiddenUnder } from '@/blocks/visibility/context' @@ -157,13 +158,15 @@ export function createIntegrationCredentialVisibility({ return oauthOwnersByProviderId.get(providerId)?.some(isManagedOAuthServiceVisible) ?? false } if (type === 'personal_token') { + // Personal tokens belong to the GitLab integration, whichever block version is current. + const gitlabType = resolveAccessControlBlockType('gitlab') if ( providerId !== 'gitlab' || - (allowedIntegrationTypes && !allowedIntegrationTypes.has('gitlab')) + (allowedIntegrationTypes && !allowedIntegrationTypes.has(gitlabType)) ) return false - const block = getBlock('gitlab') - const availability = availabilityByType.get('gitlab') + const block = getBlock(gitlabType) + const availability = availabilityByType.get(gitlabType) if (!block || block.hideFromToolbar || isHiddenUnder(blockVisibility, block) || !availability) return false const state = resolveIntegrationAvailabilityStateForVisibility(availability, blockVisibility) diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index 4bc77da959f..c9665b70362 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -298,94 +298,140 @@ type IconComponent = ComponentType> export const blockTypeToIconMap: Record = { a2a: A2AIcon, affinity: AffinityIcon, + affinity_v2: AffinityIcon, agentmail: AgentMailIcon, + agentmail_v2: AgentMailIcon, agentphone: AgentPhoneIcon, + agentphone_v2: AgentPhoneIcon, agiloft: AgiloftIcon, ahrefs: AhrefsIcon, + ahrefs_v2: AhrefsIcon, airtable: AirtableIcon, algolia: AlgoliaIcon, + algolia_v2: AlgoliaIcon, amplitude: AmplitudeIcon, apify: ApifyIcon, + apify_v2: ApifyIcon, apollo: ApolloIcon, + apollo_v2: ApolloIcon, appconfig: AppConfigIcon, + appconfig_v2: AppConfigIcon, arxiv: ArxivIcon, asana: AsanaIcon, ashby: AshbyIcon, + ashby_v2: AshbyIcon, athena: AthenaIcon, + athena_v2: AthenaIcon, attio: AttioIcon, azure_data_explorer: AzureDataExplorerIcon, azure_devops: AzureIcon, + azure_devops_v2: AzureIcon, bitbucket: BitbucketIcon, + box: BoxCompanyIcon, box_v2: BoxCompanyIcon, brandfetch: BrandfetchIcon, brex: BrexIcon, + brex_v2: BrexIcon, brightdata: BrightDataIcon, + brightdata_v2: BrightDataIcon, browser_use: BrowserUseIcon, + browser_use_v2: BrowserUseIcon, buffer: BufferIcon, + buffer_v2: BufferIcon, calcom: CalComIcon, calendly: CalendlyIcon, + calendly_v2: CalendlyIcon, cbinsights: CbInsightsIcon, checkr: CheckrIcon, + checkr_v2: CheckrIcon, circleback: CirclebackIcon, + circleback_v2: CirclebackIcon, clay: ClayIcon, clerk: ClerkIcon, + clerk_v2: ClerkIcon, clickhouse: ClickHouseIcon, clickup: ClickUpIcon, cloudflare: CloudflareIcon, + cloudflare_v2: CloudflareIcon, cloudformation: CloudFormationIcon, + cloudformation_v2: CloudFormationIcon, cloudtrail: CloudTrailIcon, + cloudtrail_v2: CloudTrailIcon, cloudwatch: CloudWatchIcon, + cloudwatch_v2: CloudWatchIcon, coda: CodaIcon, codepipeline: CodePipelineIcon, + codepipeline_v2: CodePipelineIcon, confluence: ConfluenceIcon, confluence_v2: ConfluenceIcon, context_dev: ContextDevIcon, convex: ConvexIcon, + convex_v2: ConvexIcon, crowdstrike: CrowdStrikeIcon, crunchbase: CrunchbaseIcon, - cursor_v2: CursorIcon, + crunchbase_v2: CrunchbaseIcon, + cursor: CursorIcon, + cursor_v3: CursorIcon, dagster: DagsterIcon, databricks: DatabricksIcon, + databricks_v2: DatabricksIcon, datadog: DatadogIcon, datagma: DatagmaIcon, daytona: DaytonaIcon, + daytona_v2: DaytonaIcon, deployments: Rocket, devin: DevinIcon, + devin_v2: DevinIcon, discord: DiscordIcon, + discord_v2: DiscordIcon, docusign: DocuSignIcon, downdetector: DowndetectorIcon, + dropbox: DropboxIcon, dropbox_v2: DropboxIcon, dropcontact: DropcontactIcon, dspy: DsPyIcon, - dub_v2: DubIcon, + dub: DubIcon, + dub_v3: DubIcon, duckduckgo: DuckDuckGoIcon, dynamodb: DynamoDBIcon, + dynamodb_v2: DynamoDBIcon, dynatrace: DynatraceIcon, + dynatrace_v2: DynatraceIcon, ec2: EC2Icon, + ec2_v2: EC2Icon, elasticsearch: ElasticsearchIcon, elevenlabs: ElevenLabsIcon, + elevenlabs_v2: ElevenLabsIcon, emailbison: EmailBisonIcon, + emailbison_v2: EmailBisonIcon, embeddings: EmbeddingsIcon, enrich: EnrichSoIcon, + enrich_v2: EnrichSoIcon, enrichment: EnrichmentIcon, enrow: EnrowIcon, exa: ExaAIIcon, - extend_v2: ExtendIcon, + extend: ExtendIcon, + extend_v3: ExtendIcon, fathom: FathomIcon, + fathom_v2: FathomIcon, + file: DocumentIcon, file_v5: DocumentIcon, findymail: FindymailIcon, firecrawl: FirecrawlIcon, fireflies: FirefliesIcon, - fireflies_v2: FirefliesIcon, + fireflies_v3: FirefliesIcon, fivetran: FivetranIcon, flint: FlintIcon, + flint_v2: FlintIcon, flow: FlowIcon, gamma: GammaIcon, + gamma_v2: GammaIcon, gauge: GaugeIcon, generic: WebhookIcon, github: GithubIcon, - github_v2: GithubIcon, + github_v3: GithubIcon, gitlab: GitLabIcon, + gitlab_v2: GitLabIcon, gmail: GmailIcon, gmail_v2: GmailIcon, gong: GongIcon, @@ -393,6 +439,7 @@ export const blockTypeToIconMap: Record = { google_appsheet: GoogleAppsheetIcon, google_bigquery: GoogleBigQueryIcon, google_books: GoogleBooksIcon, + google_calendar: GoogleCalendarIcon, google_calendar_v2: GoogleCalendarIcon, google_contacts: GoogleContactsIcon, google_docs: GoogleDocsIcon, @@ -403,7 +450,9 @@ export const blockTypeToIconMap: Record = { google_meet: GoogleMeetIcon, google_pagespeed: GooglePagespeedIcon, google_search: GoogleIcon, + google_sheets: GoogleSheetsIcon, google_sheets_v2: GoogleSheetsIcon, + google_slides: GoogleSlidesIcon, google_slides_v2: GoogleSlidesIcon, google_tasks: GoogleTasksIcon, google_translate: GoogleTranslateIcon, @@ -412,65 +461,92 @@ export const blockTypeToIconMap: Record = { 'google-drive': GoogleDriveIcon, 'google-sheets': GoogleSheetsIcon, grafana: GrafanaIcon, + grafana_v2: GrafanaIcon, grain: GrainIcon, - grain_v2: GrainIcon, + grain_v3: GrainIcon, granola: GranolaIcon, + granola_v2: GranolaIcon, greenhouse: GreenhouseIcon, greenhouse_v2: GreenhouseIcon, greptile: GreptileIcon, harmonic: HarmonicIcon, helpscout: HelpScoutIcon, hex: HexIcon, + hex_v2: HexIcon, higgsfield: HiggsfieldIcon, hubspot: HubspotIcon, huggingface: HuggingFaceIcon, + huggingface_v2: HuggingFaceIcon, + human_in_the_loop: HumanInTheLoopIcon, human_in_the_loop_v2: HumanInTheLoopIcon, hunter: HunterIOIcon, iam: IAMIcon, + iam_v2: IAMIcon, icypeas: IcypeasIcon, identity_center: IdentityCenterIcon, + identity_center_v2: IdentityCenterIcon, imap: MailServerIcon, incidentio: IncidentioIcon, + incidentio_v2: IncidentioIcon, infisical: InfisicalIcon, + infisical_v2: InfisicalIcon, instantly: InstantlyIcon, + instantly_v2: InstantlyIcon, intercom: IntercomIcon, - intercom_v2: IntercomIcon, + intercom_v3: IntercomIcon, intune: IntuneIcon, jama: JamaIcon, jina: JinaAIIcon, jira: JiraIcon, jira_service_management: JiraServiceManagementIcon, jotform: JotformIcon, + jotform_v2: JotformIcon, jsm: JiraServiceManagementIcon, - jupyter_v2: JupyterIcon, + jupyter: JupyterIcon, + jupyter_v3: JupyterIcon, + kalshi: KalshiIcon, kalshi_v2: KalshiIcon, ketch: KetchIcon, knowledge: PackageSearchIcon, lambda: LambdaIcon, + lambda_v2: LambdaIcon, langsmith: LangsmithIcon, + langsmith_v2: LangsmithIcon, latex: LatexIcon, launchdarkly: LaunchDarklyIcon, + launchdarkly_v2: LaunchDarklyIcon, leadmagic: LeadMagicIcon, lemlist: LemlistIcon, + lemlist_v2: LemlistIcon, linear: LinearIcon, linear_v2: LinearIcon, linkedin: LinkedInIcon, linkup: LinkupIcon, linq: LinqIcon, + linq_v2: LinqIcon, logfire: LogfireIcon, + logfire_v2: LogfireIcon, logrocket: LogRocketIcon, + logs: Library, logs_v2: Library, loops: LoopsIcon, + loops_v2: LoopsIcon, luma: LumaIcon, + luma_v2: LumaIcon, mailchimp: MailchimpIcon, + mailchimp_v2: MailchimpIcon, mailgun: MailgunIcon, + mailgun_v2: MailgunIcon, managed_agent: ClaudeIcon, manageengine_sdp: ManageEngineIcon, mem0: Mem0Icon, + mem0_v2: Mem0Icon, memory: BrainIcon, microsoft_ad: AzureIcon, + microsoft_dataverse: MicrosoftDataverseIcon, microsoft_dataverse_v2: MicrosoftDataverseIcon, microsoft_dynamics_365: MicrosoftDataverseIcon, + microsoft_excel: MicrosoftExcelIcon, microsoft_excel_v2: MicrosoftExcelIcon, microsoft_planner: MicrosoftPlannerIcon, microsoft_teams: MicrosoftTeamsIcon, @@ -478,6 +554,7 @@ export const blockTypeToIconMap: Record = { 'microsoft-teams': MicrosoftTeamsIcon, millionverifier: MillionVerifierIcon, mintlify: MintlifyIcon, + mistral_parse: MistralIcon, mistral_parse_v3: MistralIcon, modal: ModalIcon, monday: MondayIcon, @@ -488,112 +565,162 @@ export const blockTypeToIconMap: Record = { netsuite: NetSuiteIcon, neverbounce: NeverBounceIcon, new_relic: NewRelicIcon, + new_relic_v2: NewRelicIcon, notion: NotionIcon, notion_v2: NotionIcon, obsidian: ObsidianIcon, okta: OktaIcon, + okta_v2: OktaIcon, onedrive: MicrosoftOneDriveIcon, onepassword: OnePasswordIcon, oracle_fusion_financials: OracleIcon, oracledb: OracleIcon, otter: OtterIcon, + otter_v2: OtterIcon, outlook: OutlookIcon, pagerduty: PagerDutyIcon, + pagerduty_v2: PagerDutyIcon, parallel_ai: ParallelIcon, peopledatalabs: PeopleDataLabsIcon, perplexity: PerplexityIcon, persona: PersonaIcon, + persona_v2: PersonaIcon, pinecone: PineconeIcon, pipedrive: PipedriveIcon, pitchbook: PitchBookIcon, plane: PlaneIcon, + plane_v2: PlaneIcon, planetscale: PlanetScaleIcon, polymarket: PolymarketIcon, postgresql: PostgresIcon, posthog: PosthogIcon, powerbi: PowerBIIcon, profound: ProfoundIcon, + profound_v2: ProfoundIcon, prospeo: ProspeoIcon, - pulse_v2: PulseIcon, + pulse: PulseIcon, + pulse_v3: PulseIcon, qdrant: QdrantIcon, quartr: QuartrIcon, + quartr_v2: QuartrIcon, quickbooks: QuickBooksIcon, - quiver_v2: QuiverIcon, + quiver: QuiverIcon, + quiver_v3: QuiverIcon, rabbitmq: RabbitmqIcon, railway: RailwayIcon, ramp: RampIcon, rb2b: RB2BIcon, + rb2b_v2: RB2BIcon, rds: RDSIcon, + rds_v2: RDSIcon, reddit: RedditIcon, redis: RedisIcon, - reducto_v2: ReductoIcon, + reducto: ReductoIcon, + reducto_v3: ReductoIcon, resend: ResendIcon, + resend_v2: ResendIcon, revenuecat: RevenueCatIcon, rippling: RipplingIcon, + rippling_v2: RipplingIcon, rocketlane: RocketlaneIcon, + rocketlane_v2: RocketlaneIcon, rootly: RootlyIcon, + rootly_v2: RootlyIcon, rss: RssIcon, s3: S3Icon, + s3_v2: S3Icon, sailpoint: SailPointIcon, salesforce: SalesforceIcon, sap_concur: SapConcurIcon, sap_s4hana: SapS4HanaIcon, secrets_manager: SecretsManagerIcon, + secrets_manager_v2: SecretsManagerIcon, semrush: SemrushIcon, sendblue: SendblueIcon, sendgrid: SendgridIcon, + sendgrid_v2: SendgridIcon, sentry: SentryIcon, + sentry_v2: SentryIcon, serper: SerperIcon, servicenow: ServiceNowIcon, servicenow_v2: ServiceNowIcon, ses: SESIcon, + ses_v2: SESIcon, + sftp: SftpIcon, sftp_v2: SftpIcon, + sharepoint: MicrosoftSharepointIcon, sharepoint_v2: MicrosoftSharepointIcon, shopify: ShopifyIcon, sim_workspace_event: SimTriggerIcon, similarweb: SimilarwebIcon, + similarweb_v2: SimilarwebIcon, sixtyfour: SixtyfourIcon, + sixtyfour_v2: SixtyfourIcon, slack: SlackIcon, slack_app: SlackIcon, slack_v2: SlackIcon, smartlead: SmartleadIcon, + smartlead_v2: SmartleadIcon, smtp: SmtpIcon, snowflake: SnowflakeIcon, splunk: SplunkIcon, sportmonks: SportmonksIcon, + sportmonks_v2: SportmonksIcon, sqs: SQSIcon, + sqs_v2: SQSIcon, square: SquareIcon, + square_v2: SquareIcon, + ssh: SshIcon, ssh_v2: SshIcon, ssm: SSMIcon, + ssm_v2: SSMIcon, stagehand: StagehandIcon, stripe: StripeIcon, + stripe_v2: StripeIcon, sts: STSIcon, + sts_v2: STSIcon, + stt: STTIcon, stt_v2: STTIcon, supabase: SupabaseIcon, + supabase_v2: SupabaseIcon, table: Table, table_v2: Table, tailscale: TailscaleIcon, + tailscale_v2: TailscaleIcon, tavily: TavilyIcon, + tavily_v2: TavilyIcon, telegram: TelegramIcon, + telegram_v2: TelegramIcon, temporal: TemporalIcon, - textract_v2: TextractIcon, + textract: TextractIcon, + textract_v3: TextractIcon, thrive: ThriveIcon, + thrive_v2: ThriveIcon, tiktok: TikTokIcon, tinybird: TinybirdIcon, + tinybird_v2: TinybirdIcon, tinyfish: TinyFishIcon, trello: TrelloIcon, trigger_dev: TriggerDevIcon, + trigger_dev_v2: TriggerDevIcon, twilio: TwilioIcon, twilio_sms: TwilioIcon, + twilio_sms_v2: TwilioIcon, twilio_voice: TwilioIcon, + twilio_voice_v2: TwilioIcon, typeform: TypeformIcon, + typeform_v2: TypeformIcon, upstash: UpstashIcon, + upstash_v2: UpstashIcon, uptimerobot: UptimeRobotIcon, + uptimerobot_v2: UptimeRobotIcon, vanta: VantaIcon, vercel: VercelIcon, + vercel_v2: VercelIcon, wealthbox: WealthboxIcon, webflow: WebflowIcon, whatsapp: WhatsAppIcon, + whatsapp_v2: WhatsAppIcon, wikipedia: WikipediaIcon, windchill: WindchillIcon, wiza: WizaIcon, @@ -601,9 +728,12 @@ export const blockTypeToIconMap: Record = { workday: WorkdayIcon, x: xIcon, youcom: YouComIcon, + youcom_v2: YouComIcon, youtube: YouTubeIcon, + youtube_v2: YouTubeIcon, zendesk: ZendeskIcon, zep: ZepIcon, + zep_v2: ZepIcon, zerobounce: ZeroBounceIcon, zoho_desk: ZohoDeskIcon, zoom: ZoomIcon, diff --git a/apps/sim/lib/integrations/oauth-service.test.ts b/apps/sim/lib/integrations/oauth-service.test.ts index 0f7e18b5621..e648dd6e776 100644 --- a/apps/sim/lib/integrations/oauth-service.test.ts +++ b/apps/sim/lib/integrations/oauth-service.test.ts @@ -104,7 +104,7 @@ describe('resolveServiceAccountIntegration', () => { it.concurrent('returns null rather than inventing a link for unsupported input', () => { // The handler turns null into "use oauth_get_auth_link instead"; a wrong // match here would send the user to a modal that cannot take their key. - expect(resolveServiceAccountIntegration('github')).toBeNull() + expect(resolveServiceAccountIntegration('spotify')).toBeNull() expect(resolveServiceAccountIntegration('dropbox')).toBeNull() expect(resolveServiceAccountIntegration('')).toBeNull() expect(resolveServiceAccountIntegration(' ')).toBeNull() diff --git a/apps/sim/lib/internal/appconfig/client.ts b/apps/sim/lib/internal/appconfig/client.ts index 38a5e285031..8c456d2e636 100644 --- a/apps/sim/lib/internal/appconfig/client.ts +++ b/apps/sim/lib/internal/appconfig/client.ts @@ -30,25 +30,18 @@ import { GetLatestConfigurationCommand, StartConfigurationSessionCommand, } from '@aws-sdk/client-appconfigdata' +import { awsClientConfig } from '@/lib/internal/aws/credentials' import type { AppConfigConnectionConfig } from '@/tools/appconfig/types' export function createAppConfigClient(config: AppConfigConnectionConfig): AppConfigClient { return new AppConfigClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } export function createAppConfigDataClient(config: AppConfigConnectionConfig): AppConfigDataClient { return new AppConfigDataClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/athena/client.ts b/apps/sim/lib/internal/athena/client.ts index c584509c627..3d6dad493da 100644 --- a/apps/sim/lib/internal/athena/client.ts +++ b/apps/sim/lib/internal/athena/client.ts @@ -1,4 +1,5 @@ import { AthenaClient } from '@aws-sdk/client-athena' +import { awsClientConfig } from '@/lib/internal/aws/credentials' export interface AthenaConnectionConfig { region: string @@ -8,10 +9,6 @@ export interface AthenaConnectionConfig { export function createAthenaClient(config: AthenaConnectionConfig): AthenaClient { return new AthenaClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/aws/credentials.test.ts b/apps/sim/lib/internal/aws/credentials.test.ts new file mode 100644 index 00000000000..72f2c4d9647 --- /dev/null +++ b/apps/sim/lib/internal/aws/credentials.test.ts @@ -0,0 +1,160 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import type { + InternalToolOperationCall, + InternalToolOperationHandler, +} from '@/lib/internal/tool-operations/types' + +const { mockGetAwsCredentialConfig, mockResolveAwsSessionCredentials } = vi.hoisted(() => ({ + mockGetAwsCredentialConfig: vi.fn(), + mockResolveAwsSessionCredentials: vi.fn(), +})) + +vi.mock('@/lib/credentials/aws-service-account.server', () => ({ + buildAwsRoleSessionName: (label?: string) => `sim-${label ?? 'credential'}`, + getAwsCredentialConfig: mockGetAwsCredentialConfig, + resolveAwsSessionCredentials: mockResolveAwsSessionCredentials, +})) + +import { awsClientCredentials, withAwsCredential } from '@/lib/internal/aws/credentials' + +function call(overrides: Partial = {}): InternalToolOperationCall { + return { + toolId: 'dynamodb_get', + input: { + region: 'us-east-1', + accessKeyId: 'TESTINLINEKEYID', + secretAccessKey: 'inline-secret', + }, + headers: new Headers(), + context: { workflowId: 'wf-1', workspaceId: 'ws-1', executionId: 'exec-1' }, + requestId: 'req-1', + ...overrides, + } +} + +describe('withAwsCredential', () => { + it('hands an inline-key call to the family handler untouched', async () => { + const request = call() + const handler = vi.fn(async (received: InternalToolOperationCall) => { + const input = received.input as { accessKeyId: string; secretAccessKey: string } + return Response.json(awsClientCredentials(input.accessKeyId, input.secretAccessKey)) + }) + + const response = await withAwsCredential(handler)(request) + + expect(handler.mock.calls[0][0]).toBe(request) + expect(await (response as Response).json()).toStrictEqual({ + accessKeyId: 'TESTINLINEKEYID', + secretAccessKey: 'inline-secret', + }) + expect(mockGetAwsCredentialConfig).not.toHaveBeenCalled() + }) + + it('substitutes the resolved session keys and signs with their session token', async () => { + mockGetAwsCredentialConfig.mockResolvedValue({ + config: { authType: 'oidc_role' }, + owner: { workspaceId: 'ws-1' }, + }) + mockResolveAwsSessionCredentials.mockResolvedValue({ + accessKeyId: 'ASIASESSION', + secretAccessKey: 'session-secret', + sessionToken: 'session-token', + }) + const handler = vi.fn(async (received: InternalToolOperationCall) => { + const input = received.input as { accessKeyId: string; secretAccessKey: string } + return Response.json(awsClientCredentials(input.accessKeyId, input.secretAccessKey)) + }) + + const response = await withAwsCredential(handler)( + call({ + input: { region: 'us-east-1', tableName: 'Users' }, + context: { + workflowId: 'wf-1', + workspaceId: 'ws-1', + executionId: 'exec-1', + authorizedServiceAccountCredential: { credentialId: 'cred-1', service: 'aws' }, + }, + }) + ) + + expect(mockGetAwsCredentialConfig).toHaveBeenCalledWith('cred-1', { + workspaceId: 'ws-1', + organizationId: undefined, + }) + expect(handler.mock.calls[0][0].input).toMatchObject({ + region: 'us-east-1', + tableName: 'Users', + accessKeyId: 'ASIASESSION', + secretAccessKey: 'session-secret', + }) + expect(await (response as Response).json()).toStrictEqual({ + accessKeyId: 'ASIASESSION', + secretAccessKey: 'session-secret', + sessionToken: 'session-token', + }) + }) + + it('ignores a credential the executor authorized for another service', async () => { + const request = call({ + context: { + workflowId: 'wf-1', + workspaceId: 'ws-1', + authorizedServiceAccountCredential: { credentialId: 'oracle-1', service: 'oracledb' }, + }, + }) + const handler = vi.fn(async () => Response.json({})) + + await withAwsCredential(handler)(request) + + expect(handler.mock.calls[0][0]).toBe(request) + expect(mockGetAwsCredentialConfig).not.toHaveBeenCalled() + }) + + it('never attaches a session token to keys other than the resolved pair', async () => { + mockGetAwsCredentialConfig.mockResolvedValue({ config: {}, owner: { workspaceId: 'ws-1' } }) + mockResolveAwsSessionCredentials.mockResolvedValue({ + accessKeyId: 'ASIASESSION', + secretAccessKey: 'session-secret', + sessionToken: 'session-token', + }) + const handler = vi.fn(async () => + Response.json(awsClientCredentials('TESTOTHERKEYID', 'other-secret')) + ) + + const response = await withAwsCredential(handler)( + call({ + context: { + workflowId: 'wf-1', + workspaceId: 'ws-1', + authorizedServiceAccountCredential: { credentialId: 'c', service: 'aws' }, + }, + }) + ) + + expect(await (response as Response).json()).toStrictEqual({ + accessKeyId: 'TESTOTHERKEYID', + secretAccessKey: 'other-secret', + }) + }) + + it('refuses the call when the credential cannot be resolved, without running the handler', async () => { + mockGetAwsCredentialConfig.mockRejectedValue(new Error('AWS credential is unavailable')) + const handler = vi.fn() + + const response = (await withAwsCredential(handler)( + call({ + context: { + workflowId: 'wf-1', + workspaceId: 'ws-2', + authorizedServiceAccountCredential: { credentialId: 'cred-1', service: 'aws' }, + }, + }) + )) as Response + + expect(response.status).toBe(403) + expect(handler).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/internal/aws/credentials.ts b/apps/sim/lib/internal/aws/credentials.ts new file mode 100644 index 00000000000..8e05d583eef --- /dev/null +++ b/apps/sim/lib/internal/aws/credentials.ts @@ -0,0 +1,129 @@ +import { AsyncLocalStorage } from 'node:async_hooks' +import { getErrorMessage } from '@sim/utils/errors' +import { isRecordLike } from '@sim/utils/object' +import { awsRequestHandler } from '@/lib/core/network/proxy-agent.server' +import { + type AwsSessionCredentials, + buildAwsRoleSessionName, + getAwsCredentialConfig, + resolveAwsSessionCredentials, +} from '@/lib/credentials/aws-service-account.server' +import type { + InternalToolOperationCall, + InternalToolOperationHandler, +} from '@/lib/internal/tool-operations/types' + +/** + * Saved-credential support for every AWS tool family. + * + * An AWS operation was written for inline keys: its contract requires an + * access key pair and its client signs with exactly that pair. A saved + * credential plugs in underneath without touching either: + * + * 1. The executor authorizes the credential and sets + * `context.authorizedServiceAccountCredential` (trusted context, never tool input). + * 2. {@link withAwsCredential} resolves it to session credentials and + * substitutes them for `accessKeyId`/`secretAccessKey` before the contract parses. + * 3. The family's client builds its SDK config with {@link awsClientConfig}, whose + * credentials add the session token for that resolved key pair. + * + * With no saved credential, step 2 calls the handler with the original request + * object and step 3 returns the same `{ accessKeyId, secretAccessKey }` the + * client always built, so inline-key behavior is unchanged. + * + * The session token is scoped to the handler's async context, so a client must + * be built while the handler runs (every family's `create*Client` is). A client + * cached across calls or built after the handler returns would sign without + * the token and AWS would reject it. + */ + +const sessionStorage = new AsyncLocalStorage() + +/** SDK credentials for a client built from an operation's key fields. */ +export function awsClientCredentials( + accessKeyId: string, + secretAccessKey: string +): { accessKeyId: string; secretAccessKey: string; sessionToken?: string } { + const session = sessionStorage.getStore() + if (session?.sessionToken && session.accessKeyId === accessKeyId) { + return { accessKeyId, secretAccessKey, sessionToken: session.sessionToken } + } + return { accessKeyId, secretAccessKey } +} + +/** + * SDK client settings for a client built from an operation's key fields: its region, + * {@link awsClientCredentials}, and the request handler that honors `HTTPS_PROXY`. + */ +export function awsClientConfig(region: string, accessKeyId: string, secretAccessKey: string) { + return { + region, + credentials: awsClientCredentials(accessKeyId, secretAccessKey), + requestHandler: awsRequestHandler(), + } +} + +/** + * Runs `operation` inside a resolved session, so clients built during it sign + * with the session token. For callers outside a tool handler, such as selectors. + */ +export function runWithAwsSession( + session: AwsSessionCredentials, + operation: () => Promise +): Promise { + return sessionStorage.run(session, operation) +} + +/** + * Resolves an executor-authorized AWS credential for the run's owner scope. + * Also used directly by AWS tools that sign their own HTTP requests. + */ +export async function resolveAuthorizedAwsCredential( + credentialId: string, + context: { workspaceId?: string; organizationId?: string; executionId?: string }, + signal?: AbortSignal +): Promise { + const { config, owner } = await getAwsCredentialConfig(credentialId, { + workspaceId: context.workspaceId, + organizationId: context.organizationId, + }) + signal?.throwIfAborted() + return resolveAwsSessionCredentials(config, owner, { + roleSessionName: buildAwsRoleSessionName(context.executionId), + signal, + }) +} + +/** Wraps a family's operation handler so it also accepts a saved AWS credential. */ +export function withAwsCredential( + handler: InternalToolOperationHandler +): InternalToolOperationHandler { + return async (request: InternalToolOperationCall) => { + const authorized = request.context.authorizedServiceAccountCredential + if (authorized?.service !== 'aws') return handler(request) + const { credentialId } = authorized + + if (!isRecordLike(request.input)) { + return Response.json({ error: 'Invalid request data' }, { status: 400 }) + } + + let session: AwsSessionCredentials + try { + session = await resolveAuthorizedAwsCredential(credentialId, request.context, request.signal) + } catch (error) { + request.signal?.throwIfAborted() + return Response.json( + { error: `Could not use the AWS credential: ${getErrorMessage(error, 'Unknown error')}` }, + { status: 403 } + ) + } + request.signal?.throwIfAborted() + + const input = { + ...request.input, + accessKeyId: session.accessKeyId, + secretAccessKey: session.secretAccessKey, + } + return runWithAwsSession(session, () => handler({ ...request, input })) + } +} diff --git a/apps/sim/lib/internal/cloudformation/client.ts b/apps/sim/lib/internal/cloudformation/client.ts index fb2eb8bca66..82055571cd1 100644 --- a/apps/sim/lib/internal/cloudformation/client.ts +++ b/apps/sim/lib/internal/cloudformation/client.ts @@ -4,6 +4,7 @@ import { type Parameter, type Tag, } from '@aws-sdk/client-cloudformation' +import { awsClientConfig } from '@/lib/internal/aws/credentials' export interface CloudFormationConnectionConfig { region: string @@ -15,11 +16,7 @@ export function createCloudFormationClient( config: CloudFormationConnectionConfig ): CloudFormationClient { return new CloudFormationClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/cloudtrail/client.ts b/apps/sim/lib/internal/cloudtrail/client.ts index 3d5b13d7bd9..b66491a376d 100644 --- a/apps/sim/lib/internal/cloudtrail/client.ts +++ b/apps/sim/lib/internal/cloudtrail/client.ts @@ -1,4 +1,5 @@ import { CloudTrailClient } from '@aws-sdk/client-cloudtrail' +import { awsClientConfig } from '@/lib/internal/aws/credentials' export interface CloudTrailConnectionConfig { region: string @@ -24,11 +25,7 @@ export function createCloudTrailClient( options: CreateCloudTrailClientOptions = {} ): CloudTrailClient { return new CloudTrailClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), ...(options.throttleSensitive ? { retryMode: 'adaptive', maxAttempts: THROTTLE_SENSITIVE_MAX_ATTEMPTS } : {}), diff --git a/apps/sim/lib/internal/cloudwatch/client.ts b/apps/sim/lib/internal/cloudwatch/client.ts index f198a8a25c2..ab5e8584cdb 100644 --- a/apps/sim/lib/internal/cloudwatch/client.ts +++ b/apps/sim/lib/internal/cloudwatch/client.ts @@ -10,6 +10,7 @@ import { import { createLogger } from '@sim/logger' import { sleep } from '@sim/utils/helpers' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import { awsClientConfig } from '@/lib/internal/aws/credentials' interface AwsCredentials { region: string @@ -22,22 +23,14 @@ export function createCloudWatchClient( options?: { maxAttempts?: number } ): CloudWatchClient { return new CloudWatchClient({ - region: config.region, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), ...(options?.maxAttempts !== undefined && { maxAttempts: options.maxAttempts }), - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, }) } export function createCloudWatchLogsClient(config: AwsCredentials): CloudWatchLogsClient { return new CloudWatchLogsClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/codepipeline/client.ts b/apps/sim/lib/internal/codepipeline/client.ts index 0061c45688d..f1cec6dcce2 100644 --- a/apps/sim/lib/internal/codepipeline/client.ts +++ b/apps/sim/lib/internal/codepipeline/client.ts @@ -1,4 +1,5 @@ import { CodePipelineClient } from '@aws-sdk/client-codepipeline' +import { awsClientConfig } from '@/lib/internal/aws/credentials' export interface CodePipelineConnectionConfig { region: string @@ -8,10 +9,6 @@ export interface CodePipelineConnectionConfig { export function createCodePipelineClient(config: CodePipelineConnectionConfig): CodePipelineClient { return new CodePipelineClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/docusign/client.test.ts b/apps/sim/lib/internal/docusign/client.test.ts new file mode 100644 index 00000000000..a81ae3e772c --- /dev/null +++ b/apps/sim/lib/internal/docusign/client.test.ts @@ -0,0 +1,36 @@ +import { jsonResponse } from '@sim/testing/helpers/http' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { DocuSignClient } from '@/lib/internal/docusign/client' + +const ACCOUNT_BASE = + 'https://na3.docusign.net/restapi/v2.1/accounts/99999999-0000-0000-0000-000000000001' + +const mockFetch = vi.fn() + +describe('DocuSignClient.create', () => { + beforeEach(() => { + vi.stubGlobal('fetch', mockFetch) + }) + + it('uses a credential-bound account base without asking the deployment host for userinfo', async () => { + mockFetch.mockResolvedValueOnce(jsonResponse({ envelopeId: 'env-1' })) + + const client = await DocuSignClient.create('jwt-token', undefined, ACCOUNT_BASE) + await client.json('/envelopes/env-1', { method: 'GET' }, 'label', 'fallback') + + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(mockFetch.mock.calls[0][0]).toBe(`${ACCOUNT_BASE}/envelopes/env-1`) + }) + + it.each([ + 'https://evil.example.com/restapi/v2.1/accounts/99999999-0000-0000-0000-000000000001', + 'https://na3.docusign.net.evil.example.com/restapi/v2.1/accounts/99999999-0000-0000-0000-000000000001', + 'http://na3.docusign.net/restapi/v2.1/accounts/99999999-0000-0000-0000-000000000001', + 'https://na3.docusign.net/restapi/v2.1/accounts/99999999-0000-0000-0000-000000000001/../x', + ])('never sends the token to an account base of the wrong shape: %s', async (base) => { + await expect(DocuSignClient.create('jwt-token', undefined, base)).rejects.toMatchObject({ + status: 400, + }) + expect(mockFetch).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/internal/docusign/client.ts b/apps/sim/lib/internal/docusign/client.ts index 3e8894c5950..372762da549 100644 --- a/apps/sim/lib/internal/docusign/client.ts +++ b/apps/sim/lib/internal/docusign/client.ts @@ -5,7 +5,7 @@ import { readResponseToBufferWithLimit, } from '@/lib/core/utils/stream-limits' import { DocuSignOperationError } from '@/lib/internal/docusign/errors' -import { getDocusignOAuthUrl } from '@/lib/oauth/docusign' +import { getDocusignOAuthUrl, parseDocuSignAccountApiBase } from '@/lib/oauth/docusign' const MAX_DOCUSIGN_JSON_BYTES = 2 * 1024 * 1024 export const MAX_DOCUSIGN_DOCUMENT_BYTES = 25 * 1024 * 1024 @@ -60,7 +60,23 @@ export class DocuSignClient { private readonly apiBase: string ) {} - static async create(accessToken: string, signal?: AbortSignal): Promise { + /** + * Binds a client to one account. A JWT Grant credential arrives with + * `accountApiBase` already resolved from its own environment at mint time; an + * OAuth token resolves the user's default account through `/oauth/userinfo` + * on the deployment's DocuSign host. + */ + static async create( + accessToken: string, + signal?: AbortSignal, + accountApiBase?: string + ): Promise { + if (accountApiBase) { + if (!parseDocuSignAccountApiBase(accountApiBase)) { + throw new DocuSignOperationError('DocuSign credential account URL is invalid', 400) + } + return new DocuSignClient(accessToken, accountApiBase) + } const response = await fetchDocusign( getDocusignOAuthUrl('/oauth/userinfo'), { headers: { Authorization: `Bearer ${accessToken}` } }, diff --git a/apps/sim/lib/internal/docusign/execute-tool.ts b/apps/sim/lib/internal/docusign/execute-tool.ts index 5ed0366b6dd..efb75859e48 100644 --- a/apps/sim/lib/internal/docusign/execute-tool.ts +++ b/apps/sim/lib/internal/docusign/execute-tool.ts @@ -19,7 +19,10 @@ import type { InternalToolOperationHandler, } from '@/lib/internal/tool-operations/types' -const auth = { accessToken: z.string().min(1, 'Access token is required') } +const auth = { + accessToken: z.string().min(1, 'Access token is required'), + instanceUrl: z.string().optional(), +} const schemas = { docusign_create_from_template: z.object({ ...auth, diff --git a/apps/sim/lib/internal/docusign/operations.ts b/apps/sim/lib/internal/docusign/operations.ts index 156a92bbf86..e525b66da99 100644 --- a/apps/sim/lib/internal/docusign/operations.ts +++ b/apps/sim/lib/internal/docusign/operations.ts @@ -37,8 +37,11 @@ function jsonBody(data: Record): RequestInit { return { method: 'POST', body: JSON.stringify(data) } } -async function client(accessToken: string, signal?: AbortSignal): Promise { - return DocuSignClient.create(accessToken, signal) +async function client( + input: { accessToken: string; instanceUrl?: string }, + signal?: AbortSignal +): Promise { + return DocuSignClient.create(input.accessToken, signal, input.instanceUrl) } export async function executeDocuSignCreateFromTemplate( @@ -61,7 +64,7 @@ export async function executeDocuSignCreateFromTemplate( } if (input.emailSubject) body.emailSubject = input.emailSubject if (input.emailBody) body.emailBlurb = input.emailBody - const provider = await client(input.accessToken, context.signal) + const provider = await client(input, context.signal) return provider.json( '/envelopes', jsonBody(body), @@ -174,7 +177,7 @@ export async function executeDocuSignSendEnvelope( }, ] } - const provider = await client(input.accessToken, context.signal) + const provider = await client(input, context.signal) return provider.json( '/envelopes', jsonBody(envelope), @@ -188,7 +191,7 @@ export async function executeDocuSignGetEnvelope( input: DocuSignGetEnvelopeParams, context: DocuSignOperationContext ) { - const provider = await client(input.accessToken, context.signal) + const provider = await client(input, context.signal) return provider.json( `/envelopes/${input.envelopeId.trim()}?include=recipients,documents`, {}, @@ -213,7 +216,7 @@ export async function executeDocuSignListEnvelopes( if (input.envelopeStatus) query.append('status', input.envelopeStatus) if (input.searchText) query.append('search_text', input.searchText) if (input.count) query.append('count', input.count) - const provider = await client(input.accessToken, context.signal) + const provider = await client(input, context.signal) return provider.json( `/envelopes?${query}`, {}, @@ -227,7 +230,7 @@ export async function executeDocuSignVoidEnvelope( input: DocuSignVoidEnvelopeParams, context: DocuSignOperationContext ) { - const provider = await client(input.accessToken, context.signal) + const provider = await client(input, context.signal) await provider.json( `/envelopes/${input.envelopeId.trim()}`, { method: 'PUT', body: JSON.stringify({ status: 'voided', voidedReason: input.voidedReason }) }, @@ -243,7 +246,7 @@ export async function executeDocuSignDownloadDocument( context: DocuSignOperationContext ) { const documentId = input.documentId || 'combined' - const provider = await client(input.accessToken, context.signal) + const provider = await client(input, context.signal) const { buffer, contentType, fileName } = await provider.document( input.envelopeId.trim(), documentId, @@ -279,7 +282,7 @@ export async function executeDocuSignListTemplates( if (input.searchText) query.append('search_text', input.searchText) if (input.count) query.append('count', input.count) const suffix = query.size ? `?${query}` : '' - const provider = await client(input.accessToken, context.signal) + const provider = await client(input, context.signal) return provider.json( `/templates${suffix}`, {}, @@ -293,7 +296,7 @@ export async function executeDocuSignListRecipients( input: DocuSignListRecipientsParams, context: DocuSignOperationContext ) { - const provider = await client(input.accessToken, context.signal) + const provider = await client(input, context.signal) return provider.json( `/envelopes/${input.envelopeId.trim()}/recipients`, {}, diff --git a/apps/sim/lib/internal/dynamodb/client.ts b/apps/sim/lib/internal/dynamodb/client.ts index 76ee3ceef9c..71bcb38bfcc 100644 --- a/apps/sim/lib/internal/dynamodb/client.ts +++ b/apps/sim/lib/internal/dynamodb/client.ts @@ -8,15 +8,12 @@ import { ScanCommand, UpdateCommand, } from '@aws-sdk/lib-dynamodb' +import { awsClientConfig } from '@/lib/internal/aws/credentials' import type { DynamoDBConnectionConfig, DynamoDBTableSchema } from '@/tools/dynamodb/types' export function createDynamoDBClient(config: DynamoDBConnectionConfig): DynamoDBDocumentClient { const client = new DynamoDBClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) return DynamoDBDocumentClient.from(client, { @@ -222,11 +219,7 @@ export async function deleteItem( */ export function createRawDynamoDBClient(config: DynamoDBConnectionConfig): DynamoDBClient { return new DynamoDBClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/ec2/client.ts b/apps/sim/lib/internal/ec2/client.ts index 300bed5922f..21b522d6859 100644 --- a/apps/sim/lib/internal/ec2/client.ts +++ b/apps/sim/lib/internal/ec2/client.ts @@ -1,4 +1,5 @@ import { EC2Client, type Tag } from '@aws-sdk/client-ec2' +import { awsClientConfig } from '@/lib/internal/aws/credentials' interface Ec2ConnectionConfig { region: string @@ -11,12 +12,8 @@ export function createEc2Client( options?: { maxAttempts?: number } ): EC2Client { return new EC2Client({ - region: config.region, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), ...(options?.maxAttempts !== undefined && { maxAttempts: options.maxAttempts }), - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, }) } diff --git a/apps/sim/lib/internal/file/execute-tool.test.ts b/apps/sim/lib/internal/file/execute-tool.test.ts index 0aa42882fc0..f0cfa920dd5 100644 --- a/apps/sim/lib/internal/file/execute-tool.test.ts +++ b/apps/sim/lib/internal/file/execute-tool.test.ts @@ -252,6 +252,7 @@ describe('executeFileTool', () => { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1', + access: 'write', } const call = request('file_read', MANAGE_INPUTS.file_read) call.context.callerPrincipal = callerPrincipal @@ -267,7 +268,12 @@ describe('executeFileTool', () => { it('never falls back to a direct caller after invalid executor delegation', async () => { const call = request('file_read', MANAGE_INPUTS.file_read) - call.context.callerPrincipal = { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' } + call.context.callerPrincipal = { + kind: 'personal_api_key', + userId: 'user-1', + keyId: 'key-1', + access: 'write', + } mockCreateExecutorPrincipalFromExecutionContext.mockRejectedValueOnce( new InvalidInternalDelegationBindingError() ) @@ -282,7 +288,12 @@ describe('executeFileTool', () => { request('file_read', MANAGE_INPUTS.file_read, { context: { workflowId: '', - callerPrincipal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' }, + callerPrincipal: { + kind: 'personal_api_key', + userId: 'user-1', + keyId: 'key-1', + access: 'write', + }, }, }) ) diff --git a/apps/sim/lib/internal/iam/client.ts b/apps/sim/lib/internal/iam/client.ts index 76a2a610692..8cc18c193bd 100644 --- a/apps/sim/lib/internal/iam/client.ts +++ b/apps/sim/lib/internal/iam/client.ts @@ -38,15 +38,12 @@ import { SimulatePrincipalPolicyCommand, UpdateAccessKeyCommand, } from '@aws-sdk/client-iam' +import { awsClientConfig } from '@/lib/internal/aws/credentials' import type { IAMConnectionConfig, IAMSimulateContextEntry } from '@/tools/iam/types' export function createIAMClient(config: IAMConnectionConfig): IAMClient { return new IAMClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/identity-center/client.ts b/apps/sim/lib/internal/identity-center/client.ts index 02c5cbeef68..ca751a9e29f 100644 --- a/apps/sim/lib/internal/identity-center/client.ts +++ b/apps/sim/lib/internal/identity-center/client.ts @@ -27,6 +27,7 @@ import { SSOAdminClient, type TargetType, } from '@aws-sdk/client-sso-admin' +import { awsClientConfig } from '@/lib/internal/aws/credentials' import { AWS_FANOUT_CONCURRENCY, mapWithConcurrency, @@ -42,11 +43,7 @@ interface IdentityCenterConnectionConfig { export function createSSOAdminClient(config: IdentityCenterConnectionConfig): SSOAdminClient { return new SSOAdminClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } @@ -54,11 +51,7 @@ export function createIdentityStoreClient( config: IdentityCenterConnectionConfig ): IdentitystoreClient { return new IdentitystoreClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } @@ -68,11 +61,11 @@ export function createIdentityStoreClient( */ export function createOrganizationsClient(config: IdentityCenterConnectionConfig) { return new OrganizationsClient({ - region: resolveOrganizationsRegion(config.region), - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig( + resolveOrganizationsRegion(config.region), + config.accessKeyId, + config.secretAccessKey + ), }) } diff --git a/apps/sim/lib/internal/lambda/client.ts b/apps/sim/lib/internal/lambda/client.ts index 9c689061687..2f83944c4d9 100644 --- a/apps/sim/lib/internal/lambda/client.ts +++ b/apps/sim/lib/internal/lambda/client.ts @@ -8,6 +8,7 @@ import { type LayersListItem, type LayerVersionsListItem, } from '@aws-sdk/client-lambda' +import { awsClientConfig } from '@/lib/internal/aws/credentials' export interface LambdaConnectionConfig { region: string @@ -17,11 +18,7 @@ export interface LambdaConnectionConfig { export function createLambdaClient(config: LambdaConnectionConfig): LambdaClient { return new LambdaClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/llm/credentials.ts b/apps/sim/lib/internal/llm/credentials.ts index 4c450190600..a6b5f09a7d4 100644 --- a/apps/sim/lib/internal/llm/credentials.ts +++ b/apps/sim/lib/internal/llm/credentials.ts @@ -21,9 +21,12 @@ export async function resolveVertexAccessToken( if (!resolved) throw new Error(`Vertex AI credential not found: ${credentialId}`) if (resolved.credentialType === 'service_account' && resolved.credentialId) { - const accessToken = await getServiceAccountToken(resolved.credentialId, [ - 'https://www.googleapis.com/auth/cloud-platform', - ]) + const accessToken = await getServiceAccountToken( + resolved.credentialId, + ['https://www.googleapis.com/auth/cloud-platform'], + undefined, + { service: 'vertex-ai' } + ) logger.info('Resolved Vertex AI service account credential', { requestId }) return accessToken } diff --git a/apps/sim/lib/internal/mistral/execute-tool.ts b/apps/sim/lib/internal/mistral/execute-tool.ts index c2db35d1ab2..baa76a4ba7b 100644 --- a/apps/sim/lib/internal/mistral/execute-tool.ts +++ b/apps/sim/lib/internal/mistral/execute-tool.ts @@ -9,8 +9,12 @@ import { mistralParseInputSchema, } from '@/lib/internal/mistral/input' import { isMistralInputWithinLimit } from '@/lib/internal/mistral/input-size' -import { executeMistralParse } from '@/lib/internal/mistral/operations' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { executeMistralToolParse } from '@/lib/internal/mistral/operations' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' import { docNotReadyMessage, isDocNotReadyError } from '@/lib/uploads/utils/doc-not-ready' const logger = createLogger('MistralToolExecution') @@ -26,7 +30,9 @@ function validationResponse(error: z.ZodError): Response { ) } -export const executeMistralTool: InternalToolOperationHandler = async (request) => { +export const executeMistralTool: InternalToolOperationHandler = async ( + request +) => { request.signal?.throwIfAborted() if (!['mistral_parser', 'mistral_parser_v2', 'mistral_parser_v3'].includes(request.toolId)) { return Response.json( @@ -51,14 +57,14 @@ export const executeMistralTool: InternalToolOperationHandler = async (request) if (!parsed.success) return validationResponse(parsed.error) try { - const result = await executeMistralParse(parsed.data, { + const result = await executeMistralToolParse(request.toolId, parsed.data, { headers: request.headers, requestId: request.requestId, signal: request.signal, userId: request.context.userId, }) request.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof ProviderCapacityDeferredError) { diff --git a/apps/sim/lib/internal/mistral/images.test.ts b/apps/sim/lib/internal/mistral/images.test.ts new file mode 100644 index 00000000000..e972533d38c --- /dev/null +++ b/apps/sim/lib/internal/mistral/images.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' +import { extractOcrImageFiles } from '@/lib/internal/mistral/images' +import { MAX_OCR_EXTRACTED_IMAGES } from '@/tools/mistral/types' + +const JPEG = Buffer.concat([Buffer.from([0xff, 0xd8, 0xff]), Buffer.from('synthetic jpeg body')]) +const dataUri = (bytes: Buffer) => `data:image/jpeg;base64,${bytes.toString('base64')}` + +function image(id: string, imageBase64: string | undefined) { + return { + id, + top_left_x: 0, + top_left_y: 0, + bottom_right_x: 10, + bottom_right_y: 10, + image_base64: imageBase64, + } +} + +describe('extractOcrImageFiles', () => { + it('moves image bytes into files named as storage will name them, with no base64 left in the page output', () => { + const { output, files } = extractOcrImageFiles({ + pages: [ + { + index: 2, + markdown: '![img-0.jpeg](img-0.jpeg)', + images: [image('img-0.jpeg', dataUri(JPEG))], + }, + ], + }) + expect(files).toEqual([{ buffer: JPEG, name: 'page-2-img-0.jpg', mimeType: 'image/jpeg' }]) + expect(JSON.stringify(output)).not.toContain(JPEG.toString('base64')) + expect(output.pages).toEqual([ + expect.objectContaining({ + images: [expect.objectContaining({ id: 'img-0.jpeg', file_name: 'page-2-img-0.jpg' })], + }), + ]) + }) + + it('stores at most the image cap and keeps the bounding boxes of the rest without a file', () => { + const images = Array.from({ length: MAX_OCR_EXTRACTED_IMAGES + 1 }, (_, i) => + image(`img-${i}.jpeg`, JPEG.toString('base64')) + ) + const { output, files } = extractOcrImageFiles({ pages: [{ index: 0, images }] }) + expect(files).toHaveLength(MAX_OCR_EXTRACTED_IMAGES) + const pageImages = (output.pages as Array<{ images: Array> }>)[0].images + expect(pageImages).toHaveLength(MAX_OCR_EXTRACTED_IMAGES + 1) + expect(pageImages.at(-1)).not.toHaveProperty('file_name') + expect(pageImages.at(-1)).not.toHaveProperty('image_base64') + }) + + it('skips an image over the per-image cap instead of failing the parse', () => { + const huge = Buffer.alloc(21 * 1024 * 1024) + const { output, files } = extractOcrImageFiles({ + pages: [ + { + index: 0, + images: [image('img-0.png', dataUri(huge)), image('img-1.jpg', dataUri(JPEG))], + }, + ], + }) + expect(files.map((file) => file.name)).toEqual(['page-0-img-1.jpg']) + expect(JSON.stringify(output)).not.toContain('image_base64') + }) +}) diff --git a/apps/sim/lib/internal/mistral/images.ts b/apps/sim/lib/internal/mistral/images.ts new file mode 100644 index 00000000000..58706e4b47e --- /dev/null +++ b/apps/sim/lib/internal/mistral/images.ts @@ -0,0 +1,66 @@ +import { isRecordLike } from '@sim/utils/object' +import { decodeDataUriWithinLimit } from '@/lib/file-parsers/data-uri' +import { isFileParserError } from '@/lib/file-parsers/errors' +import type { InternalToolFile } from '@/lib/internal/tool-operations/file-result' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' +import { resolveEffectiveMimeType } from '@/lib/uploads/utils/file' +import { resolveStoredFileMetadata } from '@/lib/uploads/utils/stored-file-metadata' +import { MAX_OCR_EXTRACTED_IMAGES } from '@/tools/mistral/types' + +/** Largest single image kept; a bigger one is a page scan, not a figure. */ +const MAX_OCR_IMAGE_BYTES = 20 * 1024 * 1024 + +interface OcrImageFiles { + /** The OCR output with every `image_base64` removed and each kept image named. */ + output: Record + files: InternalToolFile[] +} + +/** The decoded image, or null when it is over the per-image cap. */ +function decodeImage(imageBase64: string): { buffer: Buffer; mediaType: string | null } | null { + const dataUri = imageBase64.startsWith('data:') ? imageBase64 : `data:;base64,${imageBase64}` + try { + return decodeDataUriWithinLimit(dataUri, MAX_OCR_IMAGE_BYTES) + } catch (error) { + if (isFileParserError(error) && error.code === 'complexity_limit') return null + throw error + } +} + +/** + * Turns the base64 images in a Mistral OCR response into files the executor + * stores, so image bytes never travel through block outputs. Each kept image + * gets a `file_name` matching its stored file; an image past the count, the + * per-image cap, or the total byte budget keeps its bounding box but no file. + */ +export function extractOcrImageFiles(output: unknown): OcrImageFiles { + if (!isRecordLike(output)) return { output: {}, files: [] } + if (!Array.isArray(output.pages)) return { output, files: [] } + const files: InternalToolFile[] = [] + let totalBytes = 0 + const pages = output.pages.map((page: unknown) => { + if (!isRecordLike(page) || !Array.isArray(page.images)) return page + const pageIndex = typeof page.index === 'number' ? page.index : 0 + const images = page.images.map((image: unknown) => { + if (!isRecordLike(image)) return image + const { image_base64: imageBase64, ...rest } = image + if (typeof imageBase64 !== 'string' || !imageBase64) return rest + if (files.length >= MAX_OCR_EXTRACTED_IMAGES) return rest + const decoded = decodeImage(imageBase64) + if (!decoded || totalBytes + decoded.buffer.length > MAX_BUFFERED_TRANSFER_BYTES) return rest + totalBytes += decoded.buffer.length + const id = typeof image.id === 'string' && image.id ? image.id : `img-${files.length}.jpeg` + /** Storage renames an image from its bytes; naming it the same way keeps `file_name` matching. */ + const draftName = `page-${pageIndex}-${id}` + const { fileName, mimeType } = resolveStoredFileMetadata( + draftName, + resolveEffectiveMimeType(decoded.mediaType, draftName), + decoded.buffer + ) + files.push({ buffer: decoded.buffer, name: fileName, mimeType }) + return { ...rest, file_name: fileName } + }) + return { ...page, images } + }) + return { output: { ...output, pages }, files } +} diff --git a/apps/sim/lib/internal/mistral/input.ts b/apps/sim/lib/internal/mistral/input.ts index c5aefac72c0..1fe2a43a651 100644 --- a/apps/sim/lib/internal/mistral/input.ts +++ b/apps/sim/lib/internal/mistral/input.ts @@ -12,8 +12,9 @@ export const mistralParseInputSchema = z.object({ resultType: z.string().optional(), pages: z.array(z.number()).max(MISTRAL_OCR_REQUEST_POLICY.maxPages).optional(), includeImageBase64: z.boolean().optional(), - imageLimit: z.number().optional(), - imageMinSize: z.number().optional(), + imageLimit: z.number().int().min(0).optional(), + imageMinSize: z.number().int().min(0).optional(), + tableFormat: z.enum(['markdown', 'html']).optional(), [RESOLVED_SECRET_PROVENANCE_FIELD]: resolvedSecretTraceProvenanceSchema.optional(), }) diff --git a/apps/sim/lib/internal/mistral/operations.test.ts b/apps/sim/lib/internal/mistral/operations.test.ts index d9271893695..5139ee96dfc 100644 --- a/apps/sim/lib/internal/mistral/operations.test.ts +++ b/apps/sim/lib/internal/mistral/operations.test.ts @@ -39,8 +39,10 @@ import { import type { MistralParseInput } from '@/lib/internal/mistral/input' import { executeMistralParse, + executeMistralToolParse, type MistralOperationContext, } from '@/lib/internal/mistral/operations' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const authorizeFile = filesAuthorizationMockFns.mockAssertToolFileAccess const { mockDownloadServableFileFromStorage: downloadFile, mockDownloadFileFromUrl: downloadUrl } = @@ -233,4 +235,59 @@ describe('Mistral ingestion authorization', () => { expect(countPages).not.toHaveBeenCalled() expect(submit).not.toHaveBeenCalled() }) + + it.each([ + { case: 'a limit above the cap', imageLimit: 5000 }, + { case: 'no limit', imageLimit: undefined }, + { case: 'a zero limit', imageLimit: 0 }, + ])( + 'never asks Mistral for more images than one parse may store, given $case', + async ({ imageLimit }) => { + await executeMistralToolParse( + 'mistral_parser_v3', + { ...input, includeImageBase64: true, imageLimit }, + context + ) + expect(submit.mock.calls[0][1]).toMatchObject({ + include_image_base64: true, + image_limit: 100, + }) + } + ) + + it('returns extracted images as files, never as base64 in the output', async () => { + const jpeg = Buffer.concat([ + Buffer.from([0xff, 0xd8, 0xff]), + Buffer.from('synthetic jpeg body'), + ]) + submit.mockResolvedValue({ + pages: [ + { + index: 0, + images: [ + { id: 'img-0.png', image_base64: `data:image/png;base64,${jpeg.toString('base64')}` }, + ], + }, + ], + }) + const result = await executeMistralToolParse( + 'mistral_parser_v3', + { ...input, includeImageBase64: true }, + context + ) + expect(isInternalToolFileResult(result)).toBe(true) + if (!isInternalToolFileResult(result)) return + expect(result.files).toHaveLength(1) + expect(JSON.stringify(result.present([]))).not.toContain(jpeg.toString('base64')) + }) + + it('does not request images for tool versions that have no image output', async () => { + const result = await executeMistralToolParse( + 'mistral_parser', + { ...input, includeImageBase64: true }, + context + ) + expect(isInternalToolFileResult(result)).toBe(false) + expect(submit.mock.calls[0][1]).not.toHaveProperty('include_image_base64') + }) }) diff --git a/apps/sim/lib/internal/mistral/operations.ts b/apps/sim/lib/internal/mistral/operations.ts index 6f25359adeb..d353c5057b5 100644 --- a/apps/sim/lib/internal/mistral/operations.ts +++ b/apps/sim/lib/internal/mistral/operations.ts @@ -7,8 +7,13 @@ import { decodeDataUriWithinLimit } from '@/lib/file-parsers/data-uri' import { isFileParserError } from '@/lib/file-parsers/errors' import { submitMistralOcr } from '@/lib/internal/mistral/client' import { MistralOperationError } from '@/lib/internal/mistral/errors' +import { extractOcrImageFiles } from '@/lib/internal/mistral/images' import type { MistralParseInput } from '@/lib/internal/mistral/input' import { countMistralPdfPages } from '@/lib/internal/mistral/page-count' +import { + createInternalToolFilesResult, + type InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' import { MISTRAL_OCR_REQUEST_POLICY } from '@/lib/knowledge/documents/ocr-request-policy' import { isModelSafeWorkspaceFileKey, @@ -26,6 +31,7 @@ import { type ServableFile, } from '@/lib/uploads/utils/file.server' import { assertToolFileAccess } from '@/app/api/files/authorization' +import { MAX_OCR_EXTRACTED_IMAGES } from '@/tools/mistral/types' const logger = createLogger('MistralOperations') const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif'] as const @@ -258,6 +264,33 @@ export async function executeMistralParse( } } +/** + * The workflow tool's parse. Only `mistral_parser_v3` extracts images, and it + * returns them as stored files rather than base64 in the output; older tool + * versions declare no image output, so they never ask Mistral for images. + */ +export async function executeMistralToolParse( + toolId: string, + input: MistralParseInput, + context: MistralOperationContext +): Promise<{ success: true; output: unknown } | InternalToolFileResult> { + if (toolId !== 'mistral_parser_v3' || !input.includeImageBase64) { + return executeMistralParse({ ...input, includeImageBase64: undefined }, context) + } + const result = await executeMistralParse( + { + ...input, + imageLimit: Math.min(input.imageLimit || MAX_OCR_EXTRACTED_IMAGES, MAX_OCR_EXTRACTED_IMAGES), + }, + context + ) + const { output, files } = extractOcrImageFiles(result.output) + return createInternalToolFilesResult(files, (images) => ({ + success: true, + output: { ...output, images }, + })) +} + async function executeMistralParseWithinDeadline( input: MistralParseInput, context: MistralOperationContext @@ -302,6 +335,7 @@ async function executeMistralParseWithinDeadline( } if (input.imageLimit) body.image_limit = input.imageLimit if (input.imageMinSize) body.image_min_size = input.imageMinSize + if (input.tableFormat) body.table_format = input.tableFormat const output = await submitMistralOcr( input.apiKey, diff --git a/apps/sim/lib/internal/principals/executor.test.ts b/apps/sim/lib/internal/principals/executor.test.ts index 3d8ab01b840..87d77a7cd54 100644 --- a/apps/sim/lib/internal/principals/executor.test.ts +++ b/apps/sim/lib/internal/principals/executor.test.ts @@ -144,6 +144,7 @@ describe('createExecutorPrincipalFromExecutionContext', () => { kind: 'workspace_api_key' as const, workspaceId: 'workspace-canonical', keyId: 'workspace-key-1', + access: 'write' as const, }, }, { diff --git a/apps/sim/lib/internal/rds/client.ts b/apps/sim/lib/internal/rds/client.ts index 683e9a1aee4..5739a82988e 100644 --- a/apps/sim/lib/internal/rds/client.ts +++ b/apps/sim/lib/internal/rds/client.ts @@ -5,15 +5,12 @@ import { RDSDataClient, type SqlParameter, } from '@aws-sdk/client-rds-data' +import { awsClientConfig } from '@/lib/internal/aws/credentials' import type { RdsConnectionConfig } from '@/tools/rds/types' export function createRdsClient(config: RdsConnectionConfig): RDSDataClient { return new RDSDataClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/s3/client.ts b/apps/sim/lib/internal/s3/client.ts index 95d336d332a..f7ee626afd5 100644 --- a/apps/sim/lib/internal/s3/client.ts +++ b/apps/sim/lib/internal/s3/client.ts @@ -1,4 +1,5 @@ import { S3Client } from '@aws-sdk/client-s3' +import { awsClientConfig } from '@/lib/internal/aws/credentials' export interface S3ConnectionConfig { region: string @@ -8,10 +9,6 @@ export interface S3ConnectionConfig { export function createS3Client(config: S3ConnectionConfig): S3Client { return new S3Client({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/secrets-manager/client.ts b/apps/sim/lib/internal/secrets-manager/client.ts index 7e3808dc776..b3606ba4294 100644 --- a/apps/sim/lib/internal/secrets-manager/client.ts +++ b/apps/sim/lib/internal/secrets-manager/client.ts @@ -12,6 +12,7 @@ import { UntagResourceCommand, UpdateSecretCommand, } from '@aws-sdk/client-secrets-manager' +import { awsClientConfig } from '@/lib/internal/aws/credentials' interface SecretsManagerConnectionConfig { region: string @@ -32,11 +33,7 @@ export function createSecretsManagerClient( config: SecretsManagerConnectionConfig ): SecretsManagerClient { return new SecretsManagerClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/ses/client.ts b/apps/sim/lib/internal/ses/client.ts index 8ab4c75bdfa..f5de62a80fa 100644 --- a/apps/sim/lib/internal/ses/client.ts +++ b/apps/sim/lib/internal/ses/client.ts @@ -22,6 +22,7 @@ import { UpdateEmailTemplateCommand, } from '@aws-sdk/client-sesv2' import { z } from 'zod' +import { awsClientConfig } from '@/lib/internal/aws/credentials' import type { SESConnectionConfig } from '@/tools/ses/types' const SesBulkEmailDestinationSchema = z.object({ @@ -33,11 +34,7 @@ type SesBulkEmailDestination = z.infer export function createSESClient(config: SESConnectionConfig): SESv2Client { return new SESv2Client({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/sqs/client.ts b/apps/sim/lib/internal/sqs/client.ts index 9c762c0439d..0910e64dc27 100644 --- a/apps/sim/lib/internal/sqs/client.ts +++ b/apps/sim/lib/internal/sqs/client.ts @@ -1,12 +1,9 @@ import { SQSClient } from '@aws-sdk/client-sqs' +import { awsClientConfig } from '@/lib/internal/aws/credentials' import type { SqsConnectionConfig } from '@/tools/sqs/types' export function createSqsClient(config: SqsConnectionConfig): SQSClient { return new SQSClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/ssm/client.ts b/apps/sim/lib/internal/ssm/client.ts index 584708bad35..1136505669c 100644 --- a/apps/sim/lib/internal/ssm/client.ts +++ b/apps/sim/lib/internal/ssm/client.ts @@ -63,6 +63,7 @@ import type { AwsSsmPutParameterBody } from '@/lib/api/contracts/tools/aws/ssm-p import type { AwsSsmSendCommandBody } from '@/lib/api/contracts/tools/aws/ssm-send-command' import type { AwsSsmStartAutomationExecutionBody } from '@/lib/api/contracts/tools/aws/ssm-start-automation-execution' import type { AwsSsmStopAutomationExecutionBody } from '@/lib/api/contracts/tools/aws/ssm-stop-automation-execution' +import { awsClientConfig } from '@/lib/internal/aws/credentials' interface SsmConnectionConfig { region: string @@ -72,11 +73,7 @@ interface SsmConnectionConfig { export function createSsmClient(config: SsmConnectionConfig): SSMClient { return new SSMClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/sts/client.ts b/apps/sim/lib/internal/sts/client.ts index d5fa70efed9..6ec39d21825 100644 --- a/apps/sim/lib/internal/sts/client.ts +++ b/apps/sim/lib/internal/sts/client.ts @@ -9,15 +9,13 @@ import { STSClient, type Tag, } from '@aws-sdk/client-sts' +import { awsRequestHandler } from '@/lib/core/network/proxy-agent.server' +import { awsClientConfig } from '@/lib/internal/aws/credentials' import type { STSConnectionConfig } from '@/tools/sts/types' export function createSTSClient(config: STSConnectionConfig): STSClient { return new STSClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, + ...awsClientConfig(config.region, config.accessKeyId, config.secretAccessKey), }) } @@ -37,6 +35,7 @@ export function createUnauthenticatedSTSClient(region: string): STSClient { return new STSClient({ region, credentials: { accessKeyId: 'anonymous', secretAccessKey: 'anonymous' }, + requestHandler: awsRequestHandler(), }) } diff --git a/apps/sim/lib/internal/textract/operations.ts b/apps/sim/lib/internal/textract/operations.ts index 84b20566792..2551ab46a29 100644 --- a/apps/sim/lib/internal/textract/operations.ts +++ b/apps/sim/lib/internal/textract/operations.ts @@ -22,6 +22,7 @@ import type { textractParseContract, } from '@/lib/api/contracts/tools/media/document-parse' import { validateOpaqueModelInputProvenance } from '@/lib/execution/model-input-provenance' +import { awsClientConfig } from '@/lib/internal/aws/credentials' import { parseS3Uri, resolveDocumentInput } from '@/lib/internal/textract/document-input' import { mapTextractSdkError, textractErrorResponse } from '@/lib/internal/textract/errors' import { @@ -83,11 +84,7 @@ function createTextractClient(input: { secretAccessKey: string }): TextractClient { return new TextractClient({ - region: input.region, - credentials: { - accessKeyId: input.accessKeyId, - secretAccessKey: input.secretAccessKey, - }, + ...awsClientConfig(input.region, input.accessKeyId, input.secretAccessKey), }) } diff --git a/apps/sim/lib/internal/tool-operations/registry.server.test.ts b/apps/sim/lib/internal/tool-operations/registry.server.test.ts index 56cc49449ba..bc2a5ded6ab 100644 --- a/apps/sim/lib/internal/tool-operations/registry.server.test.ts +++ b/apps/sim/lib/internal/tool-operations/registry.server.test.ts @@ -1,11 +1,17 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { + AWS_CREDENTIAL_OPERATION_TOOL_IDS, getInternalToolOperationHandler, getRegisteredInternalToolOperationIds, isInternalToolOperationRegistered, } from '@/lib/internal/tool-operations/registry.server' +import { getToolMetadata } from '@/tools/metadata' +import { AWS_SESSION_SIGNED_TOOL_IDS } from '@/tools/shared/saved-credentials' import { getToolIds } from '@/tools/tool-ids' +// The AWS routing check reads real tool policies. +vi.unmock('@/tools/metadata') + /** * Registration is checked against the generated tool ids rather than the * executable registry, whose import costs more than every handler load below @@ -35,4 +41,26 @@ describe('internal tool operation registry', () => { expect(isInternalToolOperationRegistered('mcp-server-id-tool-name')).toBe(true) expect(await getInternalToolOperationHandler('mcp-server-id-tool-name')).toBeTypeOf('function') }) + + /** + * A tool that authorizes saved AWS credentials but neither runs in a wrapped + * family nor signs with the session token would receive no keys and fail at + * run time with AWS's signature error. + */ + it('routes every AWS-credential tool through a path that resolves the credential', () => { + const awsTools = getToolIds().filter( + (toolId) => getToolMetadata(toolId)?.oauth?.provider === 'aws' + ) + + expect(awsTools.length).toBeGreaterThan(0) + for (const toolId of awsTools) { + expect( + AWS_CREDENTIAL_OPERATION_TOOL_IDS.has(toolId) || AWS_SESSION_SIGNED_TOOL_IDS.has(toolId), + toolId + ).toBe(true) + } + for (const toolId of AWS_SESSION_SIGNED_TOOL_IDS) { + expect(isInternalToolOperationRegistered(toolId), toolId).toBe(false) + } + }) }) diff --git a/apps/sim/lib/internal/tool-operations/registry.server.ts b/apps/sim/lib/internal/tool-operations/registry.server.ts index 9ea7a6bc921..76f84db5dd7 100644 --- a/apps/sim/lib/internal/tool-operations/registry.server.ts +++ b/apps/sim/lib/internal/tool-operations/registry.server.ts @@ -1492,75 +1492,121 @@ function registerFamily( } } +/** + * AWS families also accept a saved AWS credential. The wrapper is a no-op for + * calls without an executor-authorized credential, so inline-key calls reach + * the family handler unchanged. + */ const handlerLoaders = new Map() +/** Tool ids whose operation accepts a saved AWS credential (see `registerAwsFamily`). */ +export const AWS_CREDENTIAL_OPERATION_TOOL_IDS = new Set() + +/** + * Registers an AWS family wrapped so it also accepts a saved AWS credential, + * and records its tool ids so the wiring can be checked. + */ +function registerAwsFamily( + toolIds: readonly string[], + load: () => Promise> +): void { + registerFamily(handlerLoaders, toolIds, async () => { + const [{ withAwsCredential }, handler] = await Promise.all([ + import('@/lib/internal/aws/credentials'), + load(), + ]) + return withAwsCredential(handler) + }) + for (const toolId of toolIds) AWS_CREDENTIAL_OPERATION_TOOL_IDS.add(toolId) +} + registerFamily(handlerLoaders, ASHBY_TOOL_IDS, async () => { return (await import('@/lib/internal/ashby/execute-tool')).executeAshbyTool }) -registerFamily(handlerLoaders, STS_TOOL_IDS, async () => { - return (await import('@/lib/internal/sts/execute-tool')).executeStsTool -}) -registerFamily(handlerLoaders, EC2_TOOL_IDS, async () => { - return (await import('@/lib/internal/ec2/execute-tool')).executeEc2Tool -}) -registerFamily(handlerLoaders, APPCONFIG_TOOL_IDS, async () => { - return (await import('@/lib/internal/appconfig/execute-tool')).executeAppConfigTool -}) -registerFamily(handlerLoaders, IAM_TOOL_IDS, async () => { - return (await import('@/lib/internal/iam/execute-tool')).executeIamTool -}) -registerFamily(handlerLoaders, IDENTITY_CENTER_TOOL_IDS, async () => { - return (await import('@/lib/internal/identity-center/execute-tool')).executeIdentityCenterTool -}) -registerFamily(handlerLoaders, SECRETS_MANAGER_TOOL_IDS, async () => { - return (await import('@/lib/internal/secrets-manager/execute-tool')).executeSecretsManagerTool -}) -registerFamily(handlerLoaders, SSM_TOOL_IDS, async () => { - return (await import('@/lib/internal/ssm/execute-tool')).executeSsmTool -}) -registerFamily(handlerLoaders, DYNAMODB_TOOL_IDS, async () => { - return (await import('@/lib/internal/dynamodb/execute-tool')).executeDynamodbTool -}) -registerFamily(handlerLoaders, SES_TOOL_IDS, async () => { - return (await import('@/lib/internal/ses/execute-tool')).executeSesTool -}) -registerFamily(handlerLoaders, SQS_TOOL_IDS, async () => { - return (await import('@/lib/internal/sqs/execute-tool')).executeSqsTool -}) -registerFamily(handlerLoaders, RDS_TOOL_IDS, async () => { - return (await import('@/lib/internal/rds/execute-tool')).executeRdsTool -}) -registerFamily(handlerLoaders, TEXTRACT_TOOL_IDS, async () => { - return (await import('@/lib/internal/textract/execute-tool')).executeTextractTool -}) -registerFamily(handlerLoaders, CLOUDWATCH_TOOL_IDS, async () => { - return (await import('@/lib/internal/cloudwatch/execute-tool')).executeCloudwatchTool -}) -registerFamily(handlerLoaders, CLOUDTRAIL_TOOL_IDS, async () => { - return (await import('@/lib/internal/cloudtrail/execute-tool')).executeCloudtrailTool -}) +registerAwsFamily( + STS_TOOL_IDS, + async () => (await import('@/lib/internal/sts/execute-tool')).executeStsTool +) +registerAwsFamily( + EC2_TOOL_IDS, + async () => (await import('@/lib/internal/ec2/execute-tool')).executeEc2Tool +) +registerAwsFamily( + APPCONFIG_TOOL_IDS, + async () => (await import('@/lib/internal/appconfig/execute-tool')).executeAppConfigTool +) +registerAwsFamily( + IAM_TOOL_IDS, + async () => (await import('@/lib/internal/iam/execute-tool')).executeIamTool +) +registerAwsFamily( + IDENTITY_CENTER_TOOL_IDS, + async () => + (await import('@/lib/internal/identity-center/execute-tool')).executeIdentityCenterTool +) +registerAwsFamily( + SECRETS_MANAGER_TOOL_IDS, + async () => + (await import('@/lib/internal/secrets-manager/execute-tool')).executeSecretsManagerTool +) +registerAwsFamily( + SSM_TOOL_IDS, + async () => (await import('@/lib/internal/ssm/execute-tool')).executeSsmTool +) +registerAwsFamily( + DYNAMODB_TOOL_IDS, + async () => (await import('@/lib/internal/dynamodb/execute-tool')).executeDynamodbTool +) +registerAwsFamily( + SES_TOOL_IDS, + async () => (await import('@/lib/internal/ses/execute-tool')).executeSesTool +) +registerAwsFamily( + SQS_TOOL_IDS, + async () => (await import('@/lib/internal/sqs/execute-tool')).executeSqsTool +) +registerAwsFamily( + RDS_TOOL_IDS, + async () => (await import('@/lib/internal/rds/execute-tool')).executeRdsTool +) +registerAwsFamily( + TEXTRACT_TOOL_IDS, + async () => (await import('@/lib/internal/textract/execute-tool')).executeTextractTool +) +registerAwsFamily( + CLOUDWATCH_TOOL_IDS, + async () => (await import('@/lib/internal/cloudwatch/execute-tool')).executeCloudwatchTool +) +registerAwsFamily( + CLOUDTRAIL_TOOL_IDS, + async () => (await import('@/lib/internal/cloudtrail/execute-tool')).executeCloudtrailTool +) registerFamily(handlerLoaders, POSTGRESQL_TOOL_IDS, async () => { return (await import('@/lib/internal/postgresql/execute-tool')).executePostgresqlTool }) registerFamily(handlerLoaders, REDIS_TOOL_IDS, async () => { return (await import('@/lib/internal/redis/execute-tool')).executeRedisTool }) -registerFamily(handlerLoaders, CLOUDFORMATION_TOOL_IDS, async () => { - return (await import('@/lib/internal/cloudformation/execute-tool')).executeCloudformationTool -}) -registerFamily(handlerLoaders, LAMBDA_TOOL_IDS, async () => { - return (await import('@/lib/internal/lambda/execute-tool')).executeLambdaTool -}) -registerFamily(handlerLoaders, CODEPIPELINE_TOOL_IDS, async () => { - return (await import('@/lib/internal/codepipeline/execute-tool')).executeCodepipelineTool -}) +registerAwsFamily( + CLOUDFORMATION_TOOL_IDS, + async () => (await import('@/lib/internal/cloudformation/execute-tool')).executeCloudformationTool +) +registerAwsFamily( + LAMBDA_TOOL_IDS, + async () => (await import('@/lib/internal/lambda/execute-tool')).executeLambdaTool +) +registerAwsFamily( + CODEPIPELINE_TOOL_IDS, + async () => (await import('@/lib/internal/codepipeline/execute-tool')).executeCodepipelineTool +) registerFamily(handlerLoaders, MYSQL_TOOL_IDS, async () => { return (await import('@/lib/internal/mysql/execute-tool')).executeMysqlTool }) -registerFamily(handlerLoaders, ATHENA_TOOL_IDS, async () => { - return (await import('@/lib/internal/athena/execute-tool')).executeAthenaTool -}) +registerAwsFamily( + ATHENA_TOOL_IDS, + async () => (await import('@/lib/internal/athena/execute-tool')).executeAthenaTool +) registerFamily(handlerLoaders, CLICKHOUSE_TOOL_IDS, async () => { return (await import('@/lib/internal/clickhouse/execute-tool')).executeClickHouseTool }) @@ -1570,9 +1616,10 @@ registerFamily(handlerLoaders, MONGODB_TOOL_IDS, async () => { registerFamily(handlerLoaders, NEO4J_TOOL_IDS, async () => { return (await import('@/lib/internal/neo4j/execute-tool')).executeNeo4jTool }) -registerFamily(handlerLoaders, S3_TOOL_IDS, async () => { - return (await import('@/lib/internal/s3/execute-tool')).executeS3Tool -}) +registerAwsFamily( + S3_TOOL_IDS, + async () => (await import('@/lib/internal/s3/execute-tool')).executeS3Tool +) registerFamily(handlerLoaders, JUPYTER_TOOL_IDS, async () => { return (await import('@/lib/internal/jupyter/execute-tool')).executeJupyterTool }) diff --git a/apps/sim/lib/internal/tool-operations/types.ts b/apps/sim/lib/internal/tool-operations/types.ts index 9a6db19cab7..4e85821dfae 100644 --- a/apps/sim/lib/internal/tool-operations/types.ts +++ b/apps/sim/lib/internal/tool-operations/types.ts @@ -12,6 +12,12 @@ export type InternalToolOperationImplementation

= ( context?: InternalToolOperationContext ) => Promise | ToolResponse +/** A service-account credential the executor authorized, and the tool service it was bound to. */ +export interface AuthorizedServiceAccountCredential { + credentialId: string + service: string +} + /** Trusted runtime scope shared by every in-process tool operation. */ export interface InternalToolOperationContext { workflowId: string @@ -20,6 +26,12 @@ export interface InternalToolOperationContext { executionId?: string userId?: string executorDelegationOrigin?: ExecutorDelegationOrigin + /** + * The saved service-account credential the executor authorized for this call, + * set only from its own credential resolution, never from tool input. + * Operations that resolve a credential server-side (AWS session keys) read it. + */ + authorizedServiceAccountCredential?: AuthorizedServiceAccountCredential /** Trusted source block for saved MCP operation restrictions. */ mcpBlockId?: string /** diff --git a/apps/sim/lib/invitations/application/send-invitation-batch.test.ts b/apps/sim/lib/invitations/application/send-invitation-batch.test.ts index ab95519db6a..ebf4092eb5e 100644 --- a/apps/sim/lib/invitations/application/send-invitation-batch.test.ts +++ b/apps/sim/lib/invitations/application/send-invitation-batch.test.ts @@ -168,7 +168,12 @@ describe('invitation batch application boundary', () => { it('rejects workspace API keys before any user or scope lookup', async () => { await expect( useCase.execute({ - principal: { kind: 'workspace_api_key', workspaceId: 'workspace', keyId: 'key' }, + principal: { + kind: 'workspace_api_key', + workspaceId: 'workspace', + keyId: 'key', + access: 'write', + }, input: orgInput, }) ).rejects.toMatchObject({ detailCode: 'WORKSPACE_KEY_OPERATION_NOT_PERMITTED' }) diff --git a/apps/sim/lib/knowledge/__integration__/application-acl.integration.ts b/apps/sim/lib/knowledge/__integration__/application-acl.integration.ts index 3d1fe53b622..4213fcf034f 100644 --- a/apps/sim/lib/knowledge/__integration__/application-acl.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/application-acl.integration.ts @@ -91,11 +91,17 @@ describe('indexed source content through real application access', () => { const ids = createKnowledgeAclFixtureIds() const { aliceId, bobId, workspaceId, knowledgeBaseId, connectorId, lockId, groups } = ids const alice: Principal = { kind: 'session', userId: aliceId, sessionId: 'fixture-alice' } - const bob: Principal = { kind: 'personal_api_key', userId: bobId, keyId: 'fixture-bob' } + const bob: Principal = { + kind: 'personal_api_key', + userId: bobId, + keyId: 'fixture-bob', + access: 'write', + } const workspaceKey: Principal = { kind: 'workspace_api_key', workspaceId, keyId: 'fixture-workspace', + access: 'write', } const content = 'Orion project: the release readiness checklist is complete. Engineers approved the customer migration plan and documented every operational dependency.' diff --git a/apps/sim/lib/knowledge/__integration__/coda-live.integration.ts b/apps/sim/lib/knowledge/__integration__/coda-live.integration.ts index a2d2b9cc993..b5e52ed2d7c 100644 --- a/apps/sim/lib/knowledge/__integration__/coda-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/coda-live.integration.ts @@ -264,6 +264,7 @@ describe.skipIf(!tokenPath || !fixturePath || !secondEmail)( kind: 'workspace_api_key', workspaceId: ids.workspaceId, keyId: 'fixture', + access: 'write', }) expect(await keySearch).not.toContain(documentId) const chunks = await listKnowledgeChunks.execute({ diff --git a/apps/sim/lib/knowledge/__integration__/confluence-live.integration.ts b/apps/sim/lib/knowledge/__integration__/confluence-live.integration.ts index e58d8f8a52c..ab388c0bdf3 100644 --- a/apps/sim/lib/knowledge/__integration__/confluence-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/confluence-live.integration.ts @@ -343,6 +343,7 @@ describe.skipIf(!fixtureFile)('live Confluence ingestion and permission parity', kind: 'workspace_api_key', workspaceId: input.workspaceId, keyId: 'confluence-live', + access: 'write', } expect( ( diff --git a/apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts b/apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts index e54b80a29c1..c80c160e85c 100644 --- a/apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts @@ -33,11 +33,17 @@ describe.each([384, 768, 1024, 1536, 3072] as const)( const secondBaseId = generateId() const embeddingModel = dimensions === 384 ? 'ollama/all-minilm' : 'text-embedding-3-large' const alice: Principal = { kind: 'session', userId: ids.aliceId, sessionId: 'fixture-alice' } - const bob: Principal = { kind: 'personal_api_key', userId: ids.bobId, keyId: 'fixture-bob' } + const bob: Principal = { + kind: 'personal_api_key', + userId: ids.bobId, + keyId: 'fixture-bob', + access: 'write', + } const workspaceKey: Principal = { kind: 'workspace_api_key', workspaceId: ids.workspaceId, keyId: 'fixture-key', + access: 'write', } const restrictedAcl = confluencePageAcl({ providerId: 'google-drive', diff --git a/apps/sim/lib/knowledge/__integration__/gitlab-federated.integration.ts b/apps/sim/lib/knowledge/__integration__/gitlab-federated.integration.ts index 183e4328089..c5ecb8894b3 100644 --- a/apps/sim/lib/knowledge/__integration__/gitlab-federated.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/gitlab-federated.integration.ts @@ -68,6 +68,7 @@ describe.skipIf(!fixtureFile)('federated self-hosted GitLab Search', () => { kind: 'personal_api_key', userId: person.simId, keyId: 'gitlab-federated-fixture', + access: 'write', }) async function response( diff --git a/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts b/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts index 03cfab42c74..d5ad059856d 100644 --- a/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts @@ -125,11 +125,13 @@ describe.skipIf(!fixtureFile)('live self-hosted GitLab ingestion and permission kind: 'personal_api_key', userId: person.simId, keyId: 'gitlab-live-fixture', + access: 'write', }) const workspaceKey = (): Principal => ({ kind: 'workspace_api_key', workspaceId: ids.workspaceId, keyId: 'gitlab-live-fixture', + access: 'write', }) async function waitForProjectAccess(person: GitLabPerson, expectedStatus: number) { diff --git a/apps/sim/lib/knowledge/__integration__/google-drive-live.integration.ts b/apps/sim/lib/knowledge/__integration__/google-drive-live.integration.ts index 99e6aa32199..100492de60f 100644 --- a/apps/sim/lib/knowledge/__integration__/google-drive-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/google-drive-live.integration.ts @@ -92,11 +92,13 @@ describe.skipIf(!keyPath || !folderId || !ownerEmail)( kind: 'personal_api_key', userId: ids.bobId, keyId: 'live-drive', + access: 'write', }) const workspaceKey = (): Principal => ({ kind: 'workspace_api_key', workspaceId: ids.workspaceId, keyId: 'live-drive', + access: 'write', }) async function search(principal: Principal) { diff --git a/apps/sim/lib/knowledge/__integration__/google-drive-member-live.integration.ts b/apps/sim/lib/knowledge/__integration__/google-drive-member-live.integration.ts index 66b304cf182..84f4c34fde3 100644 --- a/apps/sim/lib/knowledge/__integration__/google-drive-member-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/google-drive-member-live.integration.ts @@ -124,6 +124,7 @@ describe.skipIf(!fixtureFile || !serviceAccountFile)( kind: 'workspace_api_key', workspaceId: input.workspaceId, keyId: 'drive-member-live', + access: 'write', }) const sourceConfig = () => ({ folderId: [sharedFolderId, privateFolderId], maxFiles: 0 }) diff --git a/apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts b/apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts index 294ee0ff1c4..ce6331e8847 100644 --- a/apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts @@ -26,6 +26,7 @@ describe('API-key KB block fan-out', () => { kind: 'workspace_api_key', workspaceId: ids.workspaceId, keyId: 'fixture-key', + access: 'write', } const vector = [1, ...Array(1535).fill(0)] const queryVector = { diff --git a/apps/sim/lib/knowledge/__integration__/keyword-ranking.integration.ts b/apps/sim/lib/knowledge/__integration__/keyword-ranking.integration.ts index 94cf2553cc0..7951f4c7368 100644 --- a/apps/sim/lib/knowledge/__integration__/keyword-ranking.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/keyword-ranking.integration.ts @@ -159,6 +159,7 @@ describe('unauthenticated keyword ranking', () => { kind: 'workspace_api_key', workspaceId: ids.workspaceId, keyId: 'fixture-key', + access: 'write', } const alice: Principal = { kind: 'session', userId: ids.aliceId, sessionId: 'fixture-alice' } const restrictedAcl = confluencePageAcl({ diff --git a/apps/sim/lib/knowledge/__integration__/providers-live.integration.ts b/apps/sim/lib/knowledge/__integration__/providers-live.integration.ts index efdc7e4c37d..a95651ab98e 100644 --- a/apps/sim/lib/knowledge/__integration__/providers-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/providers-live.integration.ts @@ -168,6 +168,7 @@ describe.skipIf(!credentialsFile)('real embedding and scanned PDF providers', () kind: 'workspace_api_key', workspaceId: ids.workspaceId, keyId: 'synthetic-provider-fixture', + access: 'write', }, input: { workspaceId: ids.workspaceId, @@ -615,7 +616,12 @@ describe.skipIf(!credentialsFile)('real embedding and scanned PDF providers', () expect(result.results[0]?.documentId).toBe(created.documentId) for (const deniedPrincipal of [ { ...principal, userId: ids.bobId }, - { kind: 'workspace_api_key' as const, workspaceId: ids.workspaceId, keyId: 'fixture' }, + { + kind: 'workspace_api_key' as const, + workspaceId: ids.workspaceId, + keyId: 'fixture', + access: 'write' as const, + }, ]) { await expect( readKnowledgeDocument.execute({ principal: deniedPrincipal, input }) diff --git a/apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts b/apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts index 0a286b3d43d..62c86edae97 100644 --- a/apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts @@ -36,6 +36,7 @@ const workspaceKey: Principal = { kind: 'workspace_api_key', workspaceId: ids.workspaceId, keyId: 'fixture-key', + access: 'write', } const privateId = generateId() const sharedId = generateId() @@ -176,7 +177,7 @@ describe('canonical search knowledge-base policy', () => { ) for (const principal of [ writer, - { kind: 'personal_api_key', userId: ids.bobId, keyId: 'fixture-personal' }, + { kind: 'personal_api_key', userId: ids.bobId, keyId: 'fixture-personal', access: 'write' }, workspaceKey, copilot(ids.bobId), ] as Principal[]) { @@ -308,7 +309,12 @@ describe('canonical search knowledge-base policy', () => { .deletedAt ).not.toBeNull() await deleteKnowledgeBaseOperation.execute({ - principal: { kind: 'personal_api_key', userId: ids.aliceId, keyId: 'fixture-admin-key' }, + principal: { + kind: 'personal_api_key', + userId: ids.aliceId, + keyId: 'fixture-admin-key', + access: 'write', + }, input: { knowledgeBaseId: replacementId }, }) const restored = await restoreKnowledgeBase.execute({ diff --git a/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts b/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts index 8d19f607c59..af1c3f72cee 100644 --- a/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts @@ -345,6 +345,7 @@ async function searchWorkspaceKb( kind: 'workspace_api_key', workspaceId: fixture.workspaceId, keyId: 'fixture-search-key', + access: 'write', }, input: { workspaceId: fixture.workspaceId, diff --git a/apps/sim/lib/knowledge/__integration__/slack-live.integration.ts b/apps/sim/lib/knowledge/__integration__/slack-live.integration.ts index b9e90eb68b2..c2b46f617f4 100644 --- a/apps/sim/lib/knowledge/__integration__/slack-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/slack-live.integration.ts @@ -104,6 +104,7 @@ describe.skipIf(!fixtureFile)('live Slack content and human access', () => { kind: 'workspace_api_key', workspaceId: input.workspaceId, keyId: 'slack-live', + access: 'write', }) async function api(method: string, token: string, body: Record = {}) { diff --git a/apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts b/apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts index 4033a2f9219..ac563bda13c 100644 --- a/apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts @@ -73,6 +73,7 @@ describe.each([ kind: 'workspace_api_key', workspaceId: ids.workspaceId, keyId: 'fixture-key', + access: 'write', } beforeAll(async () => { diff --git a/apps/sim/lib/knowledge/application/connectors.test.ts b/apps/sim/lib/knowledge/application/connectors.test.ts index 630893c5e51..2156453d5a2 100644 --- a/apps/sim/lib/knowledge/application/connectors.test.ts +++ b/apps/sim/lib/knowledge/application/connectors.test.ts @@ -388,7 +388,8 @@ describe('knowledge connector application use cases', () => { 'https://www.googleapis.com/auth/admin.directory.domain.readonly', ] : ['https://www.googleapis.com/auth/drive.readonly'], - 'admin@corp.com' + 'admin@corp.com', + { service: 'google-drive' } ) } ) @@ -993,7 +994,8 @@ describe('organization connector credential authorization', () => { googleDriveConnectorMeta.auth.mode === 'oauth' ? googleDriveConnectorMeta.auth.adminServiceAccountScopes : undefined, - 'admin@corp.com' + 'admin@corp.com', + { service: 'google-drive' } ) }) diff --git a/apps/sim/lib/knowledge/application/documents.test.ts b/apps/sim/lib/knowledge/application/documents.test.ts index 92fe406159a..76f1bf59ec5 100644 --- a/apps/sim/lib/knowledge/application/documents.test.ts +++ b/apps/sim/lib/knowledge/application/documents.test.ts @@ -1,3 +1,4 @@ +import { toPrincipalActor } from '@sim/auth/principal' import { queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' import { createPersonalApiKeyPrincipal, @@ -379,7 +380,7 @@ describe('knowledge document application use cases', () => { actorName: 'Workspace API key', metadata: expect.objectContaining({ operation: 'knowledge.documents.upload', - actor: createWorkspaceApiKeyPrincipal(), + actor: toPrincipalActor(createWorkspaceApiKeyPrincipal()), }), }) ) diff --git a/apps/sim/lib/knowledge/application/folders.test.ts b/apps/sim/lib/knowledge/application/folders.test.ts index 262bf5fe8ef..0642e307483 100644 --- a/apps/sim/lib/knowledge/application/folders.test.ts +++ b/apps/sim/lib/knowledge/application/folders.test.ts @@ -1,3 +1,4 @@ +import { toPrincipalActor } from '@sim/auth/principal' import { createWorkspaceApiKeyPrincipal } from '@sim/testing/factories/principal.factory' import { auditMock, auditMockFns } from '@sim/testing/mocks/audit.mock' import { folderQueriesMock, folderQueriesMockFns } from '@sim/testing/mocks/folder-queries.mock' @@ -114,7 +115,7 @@ describe('knowledge folder application use cases', () => { actorName: 'Workspace API key', metadata: expect.objectContaining({ operation: 'knowledge.folders.create', - actor: createWorkspaceApiKeyPrincipal(), + actor: toPrincipalActor(createWorkspaceApiKeyPrincipal()), }), }) ) diff --git a/apps/sim/lib/knowledge/application/knowledge-bases.test.ts b/apps/sim/lib/knowledge/application/knowledge-bases.test.ts index 31c8267a3e0..3d65d45acdf 100644 --- a/apps/sim/lib/knowledge/application/knowledge-bases.test.ts +++ b/apps/sim/lib/knowledge/application/knowledge-bases.test.ts @@ -1,3 +1,4 @@ +import { toPrincipalActor } from '@sim/auth/principal' import { createSessionPrincipal, createWorkspaceApiKeyPrincipal, @@ -235,7 +236,7 @@ describe('knowledge base application use cases', () => { actorName: 'Workspace API key', metadata: expect.objectContaining({ operation: 'knowledge.create', - actor: createWorkspaceApiKeyPrincipal({ keyId: 'workspace-key-1' }), + actor: toPrincipalActor(createWorkspaceApiKeyPrincipal({ keyId: 'workspace-key-1' })), }), }) ) diff --git a/apps/sim/lib/knowledge/application/operations.ts b/apps/sim/lib/knowledge/application/operations.ts index d15f9f60fca..32d98788516 100644 --- a/apps/sim/lib/knowledge/application/operations.ts +++ b/apps/sim/lib/knowledge/application/operations.ts @@ -1031,6 +1031,23 @@ function defineKnowledgeSessionOperation( return Object.freeze(operation) } +export const searchSourcePermissionOperations = { + read: defineOrganizationOperation({ + minimumRole: 'admin', + principalKinds: ['personal_api_key', 'oauth_access_token'], + capability: 'knowledge.use', + id: 'knowledge.search.source_permissions.read', + oauthScope: 'api:read', + }), + replace: defineOrganizationOperation({ + minimumRole: 'admin', + principalKinds: ['personal_api_key', 'oauth_access_token'], + capability: 'knowledge.use', + id: 'knowledge.search.source_permissions.replace', + oauthScope: 'api:write', + }), +} as const + export const knowledgeSessionOperations = { // permission-group-exempt: delegates to knowledgeOperations.list, which carries knowledge.use list: defineKnowledgeSessionOperation({ id: 'knowledge.session.list', capability: 'none' }), diff --git a/apps/sim/lib/knowledge/application/source-permissions.ts b/apps/sim/lib/knowledge/application/source-permissions.ts new file mode 100644 index 00000000000..4c82b5031e7 --- /dev/null +++ b/apps/sim/lib/knowledge/application/source-permissions.ts @@ -0,0 +1,146 @@ +import { AuditAction, AuditResourceType } from '@sim/audit' +import { db } from '@sim/db' +import { + knowledgeBase, + knowledgeConnector, + knowledgeConnectorPermissionSnapshot, +} from '@sim/db/schema' +import { and, eq, inArray, isNull } from 'drizzle-orm' +import type { ConnectorPermissionGrants } from '@/lib/api/contracts/knowledge/connector-permissions' +import { + defineAuthorizedOrganizationUseCase, + type OrganizationUseCaseContext, +} from '@/lib/core/application/authorized-organization-use-case' +import { authorizeOrganizationOperation } from '@/lib/core/application/organization-authorization' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { resourceScopeCondition } from '@/lib/core/resource-scope.server' +import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability' +import { searchSourcePermissionOperations } from '@/lib/knowledge/application/operations' +import { writeConnectorPermissions } from '@/lib/knowledge/connectors/permission-store' +import { connectorIsLive } from '@/lib/knowledge/connectors/sync-lock' +import { + lockOrganizationSearchApproval, + searchIntegrationAccessCondition, +} from '@/lib/knowledge/search/integration-policy' + +interface SourceInput { + organizationId: string + sourceId: string +} +interface ReplacementInput extends SourceInput { + expectedRevision: number + permissions: ConnectorPermissionGrants +} + +function sourceNotFound(): never { + throw new OrchestrationError( + 'not_found', + 'Search source with externally managed permissions not found' + ) +} + +function searchIndex(organizationId: string) { + return and( + resourceScopeCondition(knowledgeBase, { kind: 'organization', organizationId }), + eq(knowledgeBase.isSearchIndex, true), + isNull(knowledgeBase.deletedAt) + ) +} + +function searchSource(sourceId: string) { + return and( + eq(knowledgeConnector.id, sourceId), + eq(knowledgeConnector.accessMode, 'admin'), + inArray(knowledgeConnector.status, ['active', 'paused', 'pending', 'syncing', 'error']), + connectorIsLive(), + searchIntegrationAccessCondition() + ) +} + +async function readPermissionSource({ organizationId, sourceId }: SourceInput) { + if (!(await isKnowledgeMemberAccessAvailable({ organizationId }))) return sourceNotFound() + const snapshot = knowledgeConnectorPermissionSnapshot + const [source] = await db + .select({ + knowledgeBaseId: knowledgeBase.id, + connectorType: knowledgeConnector.connectorType, + revision: snapshot.revision, + metadata: snapshot.metadata, + }) + .from(knowledgeConnector) + .innerJoin(knowledgeBase, eq(knowledgeBase.id, knowledgeConnector.knowledgeBaseId)) + .innerJoin(snapshot, eq(snapshot.connectorId, knowledgeConnector.id)) + .where(and(searchIndex(organizationId), searchSource(sourceId))) + .limit(1) + if (!source) return sourceNotFound() + const { CONNECTOR_REGISTRY } = await import('@/connectors/registry.server') + const replacement = CONNECTOR_REGISTRY[source.connectorType]?.permissionConfig?.grantReplacement + if (!replacement?.supports(source.metadata)) return sourceNotFound() + return { ...source, replacement } +} + +/** Allows CI to pin the revision without disclosing uploaded identities or project memberships. */ +export const readSearchSourcePermissions = defineAuthorizedOrganizationUseCase({ + operation: searchSourcePermissionOperations.read, + async execute({ input }: OrganizationUseCaseContext) { + const { revision } = await readPermissionSource(input) + return { revision } + }, +}) + +/** Replaces the configured provider’s grants in one revision. */ +export const replaceSearchSourcePermissions = defineAuthorizedOrganizationUseCase({ + operation: searchSourcePermissionOperations.replace, + async execute({ principal, input }: OrganizationUseCaseContext) { + const { organizationId, sourceId, expectedRevision, permissions } = input + const source = await readPermissionSource(input) + if (permissions.provider !== source.connectorType) { + throw new OrchestrationError( + 'validation', + 'The permissions must match the configured source provider.' + ) + } + const prepared = source.replacement.prepare(source, expectedRevision, permissions) + const revision = await db.transaction(async (tx) => { + await lockOrganizationSearchApproval(tx, organizationId) + await authorizeOrganizationOperation( + principal, + searchSourcePermissionOperations.replace, + input, + { + executor: tx, + forShare: true, + } + ) + /** Preserve the parent-then-source lock order shared with source removal. */ + const [index] = await tx + .select({ id: knowledgeBase.id }) + .from(knowledgeBase) + .where(and(eq(knowledgeBase.id, source.knowledgeBaseId), searchIndex(organizationId))) + .for('share') + if (!index) return sourceNotFound() + const [locked] = await tx + .select({ id: knowledgeConnector.id }) + .from(knowledgeConnector) + .where( + and( + eq(knowledgeConnector.knowledgeBaseId, index.id), + eq(knowledgeConnector.connectorType, source.connectorType), + searchSource(sourceId) + ) + ) + .for('update') + if (!locked) return sourceNotFound() + return writeConnectorPermissions(tx, locked.id, { ...prepared, expectedRevision }) + }) + return { revision } + }, + projectAudit: ({ input, result }) => ({ + action: AuditAction.CONNECTOR_UPDATED, + resourceType: AuditResourceType.CONNECTOR, + resourceId: input.sourceId, + resourceName: input.permissions.provider, + description: 'Replaced Search source permissions', + metadata: { revision: result.revision }, + }), +}) diff --git a/apps/sim/lib/knowledge/connectors/access-token.test.ts b/apps/sim/lib/knowledge/connectors/access-token.test.ts index 00414a3dcd1..0674996d174 100644 --- a/apps/sim/lib/knowledge/connectors/access-token.test.ts +++ b/apps/sim/lib/knowledge/connectors/access-token.test.ts @@ -53,6 +53,29 @@ describe('resolveConnectorAccessToken', () => { mockResolveTokenBundle.mockResolvedValue({ accessToken: 'access-token' }) }) + it("seeds a minted credential's bound account into the sync context, and nothing for OAuth", async () => { + const bound = + 'https://na3.docusign.net/restapi/v2.1/accounts/99999999-0000-0000-0000-000000000002' + const resolveDocuSign = () => + resolveConnectorAccessToken({ + auth: { mode: 'oauth', provider: 'docusign', requiredScopes: ['signature'] }, + connector: credentialConnector('docusign-credential'), + userId: 'user-1', + requestId: 'req-1', + sourceConfig: {}, + }) + + mockResolveTokenBundle.mockResolvedValueOnce({ accessToken: 'jwt-token', instanceUrl: bound }) + const jwtToken = await resolveDocuSign() + if (!jwtToken) throw new Error('Expected a JWT Grant connector token') + expect(syncContextForToken(jwtToken)).toEqual({ credentialInstanceUrl: bound }) + + mockResolveTokenBundle.mockResolvedValueOnce({ accessToken: 'oauth-token' }) + const oauthToken = await resolveDocuSign() + if (!oauthToken) throw new Error('Expected an OAuth connector token') + expect(syncContextForToken(oauthToken)).toEqual({}) + }) + it('does not accept an undeclared key alternative on other OAuth connectors', async () => { await expect( resolveConnectorAccessToken({ @@ -164,7 +187,8 @@ describe('delegated connector access', () => { 'actor', 'request', delegationAuth.adminServiceAccountScopes, - 'admin@example.com' + 'admin@example.com', + { service: 'google-drive' } ) await expect(token?.getDelegatedAccessToken?.(' Employee@Example.com ')).resolves.toBe( 'user-drive-token' @@ -273,7 +297,8 @@ describe.each([ 'actor', 'request', [directoryScope], - 'directory.admin@example.com' + 'directory.admin@example.com', + { service: auth.mode === 'oauth' ? auth.provider : undefined } ) expect(token?.getDelegatedAccessToken).toBeTypeOf('function') await expect(token?.getDelegatedAccessToken?.(' Employee@Example.com ')).resolves.toBe( @@ -332,7 +357,8 @@ describe('impersonation on the connector path', () => { 'credential-owner', 'req-1', OAUTH_AUTH.requiredScopes, - 'admin@corp.com' + 'admin@corp.com', + { service: 'google-drive' } ) }) }) diff --git a/apps/sim/lib/knowledge/connectors/access-token.ts b/apps/sim/lib/knowledge/connectors/access-token.ts index 219303a9321..0464a520749 100644 --- a/apps/sim/lib/knowledge/connectors/access-token.ts +++ b/apps/sim/lib/knowledge/connectors/access-token.ts @@ -29,6 +29,11 @@ export interface ConnectorAccessToken { cloudId?: string /** The trusted site domain belonging to the credential's cloud id. */ domain?: string + /** + * The provider API base a minted service-account token is bound to (a + * DocuSign JWT Grant credential's account). Never present for OAuth. + */ + instanceUrl?: string /** Server-only capability bound to this credential and the connector's declared scopes. */ getDelegatedAccessToken?: (subject: string, signal?: AbortSignal) => Promise } @@ -139,14 +144,11 @@ export async function resolveConnectorAccessToken(params: { requestId, connectorServiceAccountScopes(auth, params.accessMode), subject, - ...(githubRepositoryScope || params.signal - ? [ - { - ...(githubRepositoryScope ? { githubRepositoryScope } : {}), - ...(params.signal ? { signal: params.signal } : {}), - }, - ] - : []) + { + service: auth.provider, + ...(githubRepositoryScope ? { githubRepositoryScope } : {}), + ...(params.signal ? { signal: params.signal } : {}), + } ) if (!bundle?.accessToken) return null @@ -190,6 +192,7 @@ export async function resolveConnectorAccessToken(params: { ...(bundle.cloudId ? { cloudId: bundle.cloudId, ...(bundle.domain ? { domain: bundle.domain } : {}) } : {}), + ...(bundle.instanceUrl ? { instanceUrl: bundle.instanceUrl } : {}), } } @@ -232,6 +235,7 @@ export function syncContextForToken(token: ConnectorAccessToken): Record, + expectedRevision: number, + permissions: ConnectorPermissionGrants + ): ConnectorPermissionData + } prepare( input: PrepareConnectorPermissionsInput ): Promise diff --git a/apps/sim/lib/knowledge/connectors/permission-store.ts b/apps/sim/lib/knowledge/connectors/permission-store.ts index 83f1b4b9da9..3979a617dc3 100644 --- a/apps/sim/lib/knowledge/connectors/permission-store.ts +++ b/apps/sim/lib/knowledge/connectors/permission-store.ts @@ -9,6 +9,13 @@ import type { DbTransaction } from '@/lib/db/types' export type ConnectorPermissionSnapshot = typeof knowledgeConnectorPermissionSnapshot.$inferSelect +/** A complete replacement. Metadata can be read without loading the private payload or subject grants. */ +export interface ConnectorPermissionData { + metadata: Record + payload: Record + groups: readonly { groupKey: string; subjects: readonly string[] }[] +} + /** The application has authorized this canonical source and verified the reader's subject. */ export async function hasConnectorPermissionGrant( connectorId: string, @@ -64,12 +71,7 @@ export async function readConnectorPermissionMetadata(connectorIds: readonly str export async function writeConnectorPermissions( tx: DbTransaction, connectorId: string, - input: { - expectedRevision: number - metadata: Record - payload: Record - groups: readonly { groupKey: string; subjects: readonly string[] }[] - } + input: ConnectorPermissionData & { expectedRevision: number } ) { const table = knowledgeConnectorPermissionSnapshot const [current] = await tx @@ -114,4 +116,5 @@ export async function writeConnectorPermissions( 'Could not save permissions. The previous configuration was preserved.' ) } + return values.revision } diff --git a/apps/sim/lib/knowledge/mcp/server.protocol.test.ts b/apps/sim/lib/knowledge/mcp/server.protocol.test.ts index 62c34aa3dfc..e035aa87035 100644 --- a/apps/sim/lib/knowledge/mcp/server.protocol.test.ts +++ b/apps/sim/lib/knowledge/mcp/server.protocol.test.ts @@ -93,11 +93,27 @@ describe('Search MCP protocol', () => { expect(JSON.parse(searchContent.text)).toMatchObject({ results: [{ documentId, queryIndexes: [0] }], }) - const read = await client.callTool({ name: 'read_document', arguments: { documentId } }) + const read = await client.callTool({ + name: 'read_document', + arguments: { documentId, limit: 8 }, + }) expect(read.isError).not.toBe(true) + expect(mocks.liveRead.mock.lastCall?.[0].input).toMatchObject({ + limit: 8, + scaledWindow: { documentsInCall: 1 }, + }) expect(read.content).toEqual([ { type: 'text', text: expect.stringContaining('Read evidence') }, ]) + const batchRead = await client.callTool({ + name: 'read_document', + arguments: { documentIds: [documentId, `${documentId}b`], limit: 8 }, + }) + expect(batchRead.isError).not.toBe(true) + expect(mocks.liveRead.mock.lastCall?.[0].input).toMatchObject({ + limit: 8, + scaledWindow: { documentsInCall: 2 }, + }) const offsetless = await client.callTool({ name: 'search', arguments: { diff --git a/apps/sim/lib/knowledge/mcp/server.ts b/apps/sim/lib/knowledge/mcp/server.ts index facc4b8a0d7..69f9074c401 100644 --- a/apps/sim/lib/knowledge/mcp/server.ts +++ b/apps/sim/lib/knowledge/mcp/server.ts @@ -298,6 +298,7 @@ export function createKnowledgeMcpServer(context: KnowledgeMcpContext): McpServe ...options, documentId: id, organizationId, + scaledWindow: { documentsInCall: documentId ? 1 : batch.length }, resultSecretRegistry: registry, signal, }, diff --git a/apps/sim/lib/messaging/email/providers/ses.ts b/apps/sim/lib/messaging/email/providers/ses.ts index 93efc679c06..2de3c739204 100644 --- a/apps/sim/lib/messaging/email/providers/ses.ts +++ b/apps/sim/lib/messaging/email/providers/ses.ts @@ -3,6 +3,7 @@ import nodemailer from 'nodemailer' import type SESTransport from 'nodemailer/lib/ses-transport' import { getAwsCredentials } from '@/lib/core/config/aws' import { env } from '@/lib/core/config/env' +import { awsRequestHandler } from '@/lib/core/network/proxy-agent.server' import { sendViaNodemailer } from '@/lib/messaging/email/providers/send-via-nodemailer' import type { MailProvider } from '@/lib/messaging/email/types' @@ -16,7 +17,11 @@ export function createSesProvider(): MailProvider | null { const region = env.AWS_SES_REGION if (!region) return null - const sesClient = new SESv2Client({ region, credentials: getAwsCredentials() }) + const sesClient = new SESv2Client({ + region, + credentials: getAwsCredentials(), + requestHandler: awsRequestHandler(), + }) const sesOptions: SESTransport.Options = { SES: { sesClient, SendEmailCommand }, } diff --git a/apps/sim/lib/monitoring/metrics.ts b/apps/sim/lib/monitoring/metrics.ts index 9562fe609db..36b84749950 100644 --- a/apps/sim/lib/monitoring/metrics.ts +++ b/apps/sim/lib/monitoring/metrics.ts @@ -32,6 +32,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { getAwsCredentials } from '@/lib/core/config/aws' import { isHosted } from '@/lib/core/config/env-flags' +import { awsRequestHandler } from '@/lib/core/network/proxy-agent.server' const logger = createLogger('HostedKeyMetrics') @@ -63,6 +64,7 @@ function getClient(): CloudWatchClient { client = new CloudWatchClient({ region: process.env.AWS_REGION || 'us-east-1', credentials: getAwsCredentials(), + requestHandler: awsRequestHandler(), }) } return client diff --git a/apps/sim/lib/mothership/__integration__/content-delivery-compat.baseline.json b/apps/sim/lib/mothership/__integration__/content-delivery-compat.baseline.json index 2f85e7c1452..a9dcd23f9db 100644 --- a/apps/sim/lib/mothership/__integration__/content-delivery-compat.baseline.json +++ b/apps/sim/lib/mothership/__integration__/content-delivery-compat.baseline.json @@ -853,6 +853,7 @@ "startedAt": "