# Third-party OAuth apps

Use OAuth when your application must operate a workspace owned by someone else. The person authorizes the scopes they see in Meteor; your app receives a short-lived access token. **Never** ask for or capture their API key.

Meteor implements OAuth 2.0 Authorization Code with **mandatory PKCE S256**. The access token is a Bearer token for the same public API and lasts one hour. The refresh token lasts 30 days and rotates on every use.

## Before you redirect

In the workspace that owns your integration, open **Settings → Developers → OAuth apps** and register the app. Store its `client_id`. If you choose a confidential app, also store the `client_secret`: Meteor shows it only on creation or rotation.

- Register every full `redirect_uri`. Meteor compares the return URL **exactly**; wildcards and fragments (`#`) are not accepted.
- The callback must use HTTPS, except `localhost`, `127.0.0.1`, or `::1` during development.
- Declare only the scopes you need. Partner scopes cannot be assigned to an OAuth app.
- Choose **public** for a SPA or native app that cannot protect a secret. Choose **confidential** only when the exchange runs on your server.

## Redirect to consent

Generate an unpredictable `state` and a PKCE `code_verifier` between 43 and 128 characters for each user. Store both in your app session. Compute `code_challenge = base64url(SHA-256(code_verifier))`, then redirect the browser to Meteor's consent screen:

```ts
import { createHash, randomBytes } from 'node:crypto';
import { OAuth } from '@meteor.ia/sdk';

const verifier = randomBytes(48).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');
const state = randomBytes(24).toString('base64url');

// Store verifier and state in the session before redirecting.
const oauth = new OAuth();
const url = oauth.authorizeUrl({
  clientId: process.env.MET_OAUTH_CLIENT_ID!,
  redirectUri: 'https://your-app.example.com/oauth/callback',
  scope: ['runs:execute', 'contacts:read'],
  state,
  codeChallenge: challenge,
});
res.redirect(url);
```

The person signs in to Meteor if needed and sees your app name, website, and requested scopes. If they accept, Meteor redirects to your `redirect_uri` with `code` and the same `state`. If they deny, it returns with `error=access_denied`.

## Exchange the code on your server

In the callback, validate `state` against what you stored, then exchange the code. Always send the identical `redirect_uri` and the original `code_verifier`. The code is single use and expires after 60 seconds.

```ts
const { access_token, refresh_token, expires_in, scope } = await oauth.exchangeCode({
  clientId: process.env.MET_OAUTH_CLIENT_ID!,
  clientSecret: process.env.MET_OAUTH_CLIENT_SECRET, // confidential apps only
  code: String(req.query.code),
  redirectUri: 'https://your-app.example.com/oauth/callback',
  codeVerifier: session.pkceVerifier,
});

// Encrypt refresh_token when storing it. access_token is a short-lived met_ key.
```

Do not send the `client_secret`, `code_verifier`, authorization code, or refresh token to a browser, log, or analytics provider. The token does not include a `workspace_id`: preserve the workspace your integration operates in your own installation record if the API route needs it.

## Call the API and refresh

`access_token` is a Bearer token for the approved scopes. Use it with the REST API or SDK; set the installation's `workspaceId` for routes that require it.

```ts
import Met from '@meteor.ia/sdk';

const met = new Met(access_token, { workspaceId: installation.workspaceId });
const run = await met.runs.create({ input: 'Summarize today\'s leads' });
```

Before it expires, or after an expired-token response, refresh from your server. Every refresh issues **a new refresh token**: replace the previous one atomically. Reusing a consumed refresh token revokes the whole grant for safety.

```ts
const renewed = await oauth.refresh({
  clientId: process.env.MET_OAUTH_CLIENT_ID!,
  clientSecret: process.env.MET_OAUTH_CLIENT_SECRET,
  refreshToken: installation.refreshToken,
});
// Store renewed.access_token and renewed.refresh_token together.
```

## Revoke and rotate

A person can revoke your app from **Settings → Developers → OAuth apps**. Your app can also revoke an access or refresh token; revoking a refresh token invalidates the whole grant:

```ts
await oauth.revoke(installation.refreshToken, {
  clientId: process.env.MET_OAUTH_CLIENT_ID!,
  clientSecret: process.env.MET_OAUTH_CLIENT_SECRET,
});
```

For confidential apps, rotate the `client_secret` from the same screen. A normal rotation keeps the previous secret valid for 24 hours while you deploy the new one; use immediate invalidation only after a leak.

If your owning workspace already uses [outbound webhooks](webhooks.html), you can subscribe to `app.authorized` and `app.revoked` with `integrations:read`. Those events go to the workspace that registered the app and include `client_id`, `workspace_id`, scopes, and `grant_id`, without personal data.

## Current limits

The authorization server does not yet publish OAuth metadata at `/.well-known/oauth-authorization-server` or dynamic client registration. Configure the URLs documented in this guide manually; do not attempt to discover them from the domain.
