# Authentication, Environments, Rate Limits and Events

From **all 479 shipped main connector files — 612 auth blocks**, `oauth2` 308 · `custom` 304.

> **The installed CLI is the authority, not this file.** This is a corpus snapshot; the
> schema `stackone validate` enforces can move ahead of it. When validate rejects a shape
> shown here, trust the CLI's error message and fix to it — do **not** spend turns grepping
> `node_modules` bundles or `.d.mts` files to reverse-engineer the schema. Fix the field the
> error names, re-validate, and note the drift in the run's gap ledger so this file gets
> corrected. A measured azureblobstorage run lost its single biggest time block to exactly
> that archaeology after 12 validate errors on an auth block written from these docs.

> Counts in an earlier draft of this file came from 215 files, because the miner silently
> skipped 264 connectors whose `actions:` block repeats the `$ref` key — strict YAML rejects
> duplicate mapping keys and the parser threw. Everything below is re-derived from the full
> corpus with that fixed. Several shapes appear only in the previously-skipped 264.

---

## Main file — all 11 keys

```yaml
StackOne: 1.0.0                    # 479 — always
info:                              # 479
  title: 1Password
  key: onepassword                 # slug, may differ from the folder name
  version: 1.1.0
  description: Secrets management platform for secure credential access and vault operations.
  assets:                          # 479 — always present
    icon: https://stackone-logos.com/api/1password/filled/svg
  changelog:                       # 191
    summary: Fixed identity actions failing to authenticate against the SCIM bridge.
    labels:
      - bug-fix
baseUrl: ${credentials.serverUrl}/v1   # 479 — templates allowed
releaseStage: preview              # 473
resources: https://developer.1password.com/docs/connect/api-reference/   # 445
rateLimit:                         # 248
  mainRatelimit: 10                # requests per second — the only key in the corpus
scopeDefinitions: {...}            # 209
events: {...}                      # 84
documentation:                     # 2 — extra reference links
  references:
    - title: MeisterTask API reference
      url: https://developers.meistertask.com/
      description: Full REST API reference.
actions:                           # 479
  $ref: 1password.activity
  $ref: 1password.files
```

> **The `actions` block repeats the `$ref` key.** That is invalid strict YAML — parsers must
> allow duplicate keys, and any tool reading refs must scan the raw text, because a
> duplicate-tolerant parse keeps only the last one. 264 of 479 connectors use this form.

---

## Auth block anatomy

Four keys on all 612 blocks: `type` · `label` · `authorization` · `environments`.

| Key | Count | |
|---|---|---|
| `type` `label` `authorization` `environments` | 612 | always |
| `support` | 611 | description + setup/config guides |
| `configFields` | 486 | end-user credentials |
| `testActions` | 368 | read-only actions proving the connection |
| `refreshAuthentication` | 309 | token refresh |
| `setupFields` | 217 | developer-app credentials |
| `grantType` | 184 | `client_credentials` 110 · `authorization_code` 74 |
| `requiredScopes` 3 · `configNotices` 2 · `certificates` 1 | | rare |

**`authorization.type`:** `bearer` 229 · `oauth2` 199 · `none` 124 · `basic` 60

---

## `authorization` — all 20 shapes

### No auth — 120
```yaml
authorization:
  type: none
```

### Bearer — 152 with `includeBearer`, 77 without
```yaml
authorization:
  type: bearer
  token: $.credentials.accessToken
  includeBearer: true          # prepend "Bearer " — omit if the provider wants the raw token
```

### Basic — 43
```yaml
authorization:
  type: basic
  username: $.credentials.apiId
  password: $.credentials.apiToken
```

**With explicit encoding** — 11
```yaml
  encoding: base64
```

**Username only** — 5. API key in the username field, password blank:
```yaml
authorization:
  type: basic
  username: '{{credentials.apiKey}}'
```

**Password only** — token as the password, no username.

### Signed requests — 4
```yaml
authorization:
  type: none
  signing:
    strategy: hmac
```

### OAuth2 — authorization code, 97
```yaml
authorization:
  type: oauth2
  authorizationUrl: https://github.com/login/oauth/authorize
  authorizationParams:
    client_id: $.credentials.clientId
    redirect_uri: ${apiHostUri}/connect/oauth2/github/callback
  tokenUrl: https://github.com/login/oauth/access_token
  token: $.credentials.accessToken
  includeBearer: true
```

`${apiHostUri}` is platform-supplied — never hardcode a redirect host.

### OAuth2 variants, by frequency

| Extra keys | Count | Purpose |
|---|---|---|
| `issuer` | 27 | OIDC discovery |
| `tokenParams` | 15+3+2 | credentials in the token-request **body**, not a header |
| `tokenExpiresIn` + `tokenRefreshExpiresIn` | 15 | provider omits `expires_in` |
| `tokenExpiresIn` alone | 9 | |
| `customHeaders` | 6+5+2 | HTTP Basic client auth at the token endpoint |
| `pkce: true` | 6+5+3 | S256 challenge |
| `tokenResponse` | 2 | select which token to keep |

```yaml
  tokenParams:
    grant_type: authorization_code
    client_id: $.credentials.clientId
    client_secret: $.credentials.clientSecret
    redirect_uri: ${apiHostUri}/connect/oauth2/bazaarvoice/callback

  customHeaders:
    Authorization: Basic ${encodeBase64(credentials.clientId + ":" + credentials.clientSecret)}
    Content-Type: application/x-www-form-urlencoded

  tokenResponse:               # Slack returns both; keep the bot token
    acceptTokenTypes:
      - bot
      - user
```

### Static refresh token — no `authorizationUrl`
```yaml
authorization:
  type: oauth2
  tokenUrl: https://${credentials.workday_host}/ccx/oauth2/${credentials.tenant}/token
  token: $.credentials.accessToken
  includeBearer: true
```

For providers where the user pastes a long-lived token instead of a browser redirect.

### Dynamic URLs

```yaml
authorizationUrl: https://${credentials.accountIdentifier}.snowflakecomputing.com/oauth/authorize

authorizationUrl: >-
  {{context.environment == "sandbox"
  ? "https://hcm-demo.paycor.com/AppActivation/Authorize"
  : "https://hcm.paycor.com/AppActivation/Authorize"}}
```

`context.environment` lets one block serve sandbox and production.

### Scopes

```yaml
scope: '{{$.credentials.scopes ?? "all-apis offline_access"}}'
```

Let the user override, falling back to a documented default. **`offline_access` is what yields
a refresh token** on most OIDC providers — omitting it is why refresh silently fails.

### Client certificates — 1
```yaml
certificates:
  certs:
    - $.credentials.certificate
  keys:
    - $.credentials.privateKey
```

For mTLS providers.

---

## `refreshAuthentication` — 309

`action` 309 · `schedule` 4.

```yaml
refreshAuthentication:
  action:
    actionId: refresh_token_adobelearningmanager
    actionType: refresh_token
    categories:
      - internal
    label: Refresh OAuth2 Token
    description: Refresh the access token using the stored refresh token.
    steps: [...]
  schedule: '*/30 * * * *'      # 4 — cron, for providers whose tokens expire on a clock
```

`actionType: refresh_token` and `categories: [internal]` keep it out of the customer-facing
action list.

`actionType: refresh_token` also changes how a failure runs. The `map_fields` step still runs
after a fatal step, over only the steps that succeeded, so a token the grant already rotated is
saved rather than lost. And every request after the first successful provider call is capped at
30s. So where the refresh makes a grant call, shape the steps as grant first, `map_fields` last,
named fields only, and `result` pointing at the `map_fields` output. The why is under **When a step fails** in
`build-connector/references/auth-oauth2-code-refresh.md`.

---

## `environments` — 612, always the same shape

```yaml
environments:
  - key: production
    name: Production
  - key: test
    name: Test
```

Read in expressions as `context.environment`.

---

## `setupFields` and `configFields` — 1,624 fields

**These use `key`. Action `inputs` use `name`.** Mixing them is the most common structural
error in this file.

| Field | Count |
|---|---|
| `key` `label` `type` `required` | 1,624 each |
| `description` | 1,623 |
| `secret` | 1,586 |
| `placeholder` | 1,498 |
| `tooltip` | 1,125 |
| `options` | 57 — `select` only |
| `readOnly` `computed` `value` | 49 each |
| `externalSystemAccountIdentifier` | 2 |

**Types:** `text` 913 · `password` 653 · `select` 57 · `text_area` 1

```yaml
configFields:
  - key: accessToken
    label: Access Token
    type: password
    required: true
    secret: true
    placeholder: eyJhbGciOiJFUzI1NiIsImtpZCI6...
    description: JWT bearer token for authenticating with the Connect API.
    tooltip: Generate this when creating your Secrets Automation workflow.
```

**`select` with options** — regional hosts and similar fixed choices:
```yaml
  - key: apiHost
    label: API Region
    type: select
    required: true
    options:
      - label: Global (cc.absolute.com)
        value: api.absolute.com
      - label: EU (cc.eu2.absolute.com)
        value: api.eu2.absolute.com
```

Then `baseUrl: https://${credentials.apiHost}`.

**Computed read-only field** — the platform fills it, the user copies it out:
```yaml
  - key: nativeWebhookUrl
    label: Native Webhook URL
    type: text
    readOnly: true
    computed: true
    required: false
    secret: false
    value: >-
      {{webhookEventsUrl(webhooks_url, orgId, projectSecureId, connectorProfileId,
      external_trigger_token)}}
```

This is how a user gets the per-connection webhook URL to paste into the provider.

### `configNotices` — 2

```yaml
configNotices:
  - key: installAppFirst
    type: warning
    description: >-
      For access to private repositories, ensure the GitHub App is installed on your account.
      Without installation, only public repositories and user profile data are available.
```

A banner shown at connection time for a prerequisite the fields cannot express.

---

## Derived credentials — `targetFieldKey`

Not every credential is typed by the user. A step can **write** one onto the connection, and
that is how a value resolved during or after the auth exchange becomes readable as
`$.credentials.*` from then on:

```yaml
- stepId: map_credentials
  stepFunction:
    functionName: map_fields
    parameters:
      fields:
        - targetFieldKey: cloudId
          expression: $.steps.extract_cloud_id.output.data.cloudId
        - targetFieldKey: refreshToken
          expression: $.steps.refresh_request.output.data.refresh_token
```

Atlassian resolves `cloudId` after OAuth this way; Bullhorn persists `bhRestToken`. These
credentials have **no `configField` or `setupField`** — declaring one would ask the user for
a value the flow computes.

> **Match the case exactly.** The platform credential is `refreshToken`; a `map_fields`
> expression reading `$.credentials.refresh_token` silently yields nothing, so the refresh
> token is stored empty and every connection dies at the first token expiry. This is a live
> bug in a shipped connector — the two spellings sit 20 lines apart and neither the parser
> nor `stackone validate` can see it.

---

## `testActions` — 368

Two forms ship. The short one names an action defined elsewhere in the connector:

```yaml
testActions:
  - action: list_vaults
```

The long one **defines the action inline**, so it exists nowhere else — used when the test
needs a request no customer-facing action makes:

```yaml
testActions:
  - action:
      actionId: test_connection_clickup_custom
      categories:
        - internal
      label: Test Connection
      description: Validate the token and workspace ID by listing spaces in the workspace.
      steps:
        - stepId: test_workspace_request
          stepFunction:
            functionName: request
            parameters: {...}
```

**Read-only only** — these run on every connection attempt, so a test action that writes
creates provider data each time someone opens the connection screen.

---

## `support` — 611

`description` 600 · `guides` 539 · `link` 23.

```yaml
support:
  description: Authenticate using a JWT bearer token generated for your Connect server.
  guides:
    config:                        # explains configFields (end-user credentials)
      sections:
        - title: Create a Connect server environment
          content: |
            StackOne connects through a self-hosted Connect server.
          steps:
            - title: Open the Developer section
              content: Sign in as an owner or administrator, then select **Developer**.
              list:
                - The environment can only read items from the vaults you select here.
              image:
                src: images/custom-config-developer-sidebar.png
                alt: Dashboard with the Developer item highlighted in the sidebar
    setup: {...}                   # explains setupFields (developer app registration)
  link: https://docs.provider.com/api/auth
```

Guide structure is `sections[] → steps[] → {title, content, list[], image{src, alt}}`. Images
live in the connector's `images/` folder and are referenced by relative path.

Match the guide to the field set it explains: `config` for `configFields`, `setup` for
`setupFields`.

---

## `scopeDefinitions` — 209 connectors

```yaml
scopeDefinitions:
  admin:read:
    description: Read access to admin API for users, learning objects, catalogs and groups.
  admin:write:
    description: Write access for creating, updating and deleting resources.
    includes: admin:read       # implies the read scope
```

`includes` expresses implication, so an action needing `admin:write` need not also list
`admin:read`.

---

## `events` — 84 connectors

`actions` 84 · `router` 84 · `guides` 56 · `setup` 50 · `externalAccountIdExtractor` 14.

```yaml
events:
  actions:
    - $ref: auvik.events              # partial holding the event actions
  router:
    rules:
      - when: present(body.alertId)   # first match wins
        then:
          action: auvik_webhook_alert
          input:
            event: body
  externalAccountIdExtractor:         # map an inbound payload back to a connection
    actionId: extract_external_account_id
    actionType: custom
    label: Extract Account
    description: Resolves the connected account from the Organization ID in the webhook meta.
    steps: []
    result: 'body.meta && body.meta.orgID ? "" + body.meta.orgID : null'
  guides:
    setup:
      warning: >-
        Webhooks are configured manually — there is no subscription API, so StackOne cannot
        create or remove them.
      sections: [...]
```

`externalAccountIdExtractor` has **empty `steps` and a bare expression `result`** — it computes
a value from the payload without making a request.

Event actions use `actionType: event` (1,613 actions), `emit_event` steps, and
`providerEvents` (1,468) naming the provider's event types.

## Auth hardening rules (gates A13–A16)

These are the auth defects that recur across generated connectors and are now gated. Write
to them up front rather than fixing on gate failure.

**A13 — never hardcode `state`.** A constant `state` in `authorizationParams` is a common
generated defect. `state` is the OAuth CSRF binding; a fixed value defeats it for every
connection. Omit it — the platform injects a per-request value.

**A14 — declare a `scopes` field on scoped OAuth.** Defining `scopeDefinitions` without a
`scopes` setup/config field means no connection can ever be scope-customised. An
authorization-code block alongside `scopeDefinitions` needs a `scopes` field
(client-credentials flows are exempt — their scopes are typically fixed at app level).

**A15 — use the corpus credential keys.** Builds invent synonyms the platform and guides do
not recognise: `companyDomain` (census: 2 connectors, against `subdomain` 24 · `domain` 15)
and snake_case variants of platform keys (census: zero declarations). Canonical spellings:
`subdomain`, `domain`, `apiKey`, `apiToken`, `accessToken`, `refreshToken`, `clientId`,
`clientSecret`, `email`, `username`, `password`, `scopes`. The census lives in
`corpus-stats.json` beside this file — regenerate it with `scripts/corpus/mine.js` before
adding to the synonym table, so a legitimate minority spelling is never declared a defect
(`apiToken` is used by 38 connectors).

**A16 — shape refresh actions to survive a failed step.** A rotating provider kills the old
refresh token on the grant, so the new one has to be saved even when a later step fails. The
runtime keeps running `map_fields` for that, and four shapes undo it: no `map_fields` step at
all on an action that calls the provider (blocking), `map_fields` not being the last step
(blocking), `result` reading a request step instead of the `map_fields` output (blocking), and a
whole-output mapping like `accessToken: $.data` or `{{$.data}}` (major, 4 shipped connectors
still do it). See `refreshAuthentication` above.

**testActions — pick a self-identifying endpoint.** Prefer `/me`-style endpoints and their
equivalents over customer-data listings: they validate the credential without touching
tenant data, and they fail more legibly — an empty list is ambiguous where a rejected
identity call is not.
