Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/docs/content/docs/contributing/building.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -639,3 +639,19 @@ import {
cardToFallbackText, // Convert card to plain text
} from "@chat-adapter/shared";
```

### Token encryption

If your adapter persists OAuth tokens to a `StateAdapter`, encrypt them at rest with the shared AES-256-GCM helpers instead of rolling your own:

```typescript
import {
encryptToken, // Encrypt a string into an EncryptedTokenData envelope
decryptToken, // Decrypt an envelope back to the original string
decodeKey, // Decode a hex-64 or base64-44 32-byte key (throws on wrong length)
isEncryptedTokenData, // Type guard for tolerating legacy plaintext records
type EncryptedTokenData,
} from "@chat-adapter/shared";
```

Accept the key as an optional `encryptionKey` config field (auto-detected from a `*_ENCRYPTION_KEY` env var), encrypt on `setInstallation()`, decrypt on `getInstallation()`, and use `isEncryptedTokenData` to keep accepting plaintext records so operators can roll the key in without flushing existing installs. See `@chat-adapter/slack` and `@chat-adapter/linear` for reference implementations.
18 changes: 14 additions & 4 deletions packages/adapter-gchat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,18 @@ All options are auto-detected from environment variables when not provided.
| `credentials` | No* | Service account credentials JSON. Auto-detected from `GOOGLE_CHAT_CREDENTIALS` |
| `useApplicationDefaultCredentials` | No | Use Application Default Credentials. Auto-detected from `GOOGLE_CHAT_USE_ADC` |
| `pubsubTopic` | No | Pub/Sub topic for Workspace Events. Auto-detected from `GOOGLE_CHAT_PUBSUB_TOPIC` |
| `pubsubAudience` | No | Expected JWT audience for Pub/Sub webhook verification. Auto-detected from `GOOGLE_CHAT_PUBSUB_AUDIENCE` |
| `googleChatProjectNumber` | No | GCP project number for direct webhook JWT verification. Auto-detected from `GOOGLE_CHAT_PROJECT_NUMBER` |
| `pubsubAudience` | No† | Expected JWT audience for Pub/Sub webhook verification. Auto-detected from `GOOGLE_CHAT_PUBSUB_AUDIENCE` |
| `googleChatProjectNumber` | No† | GCP project number for direct webhook JWT verification. Auto-detected from `GOOGLE_CHAT_PROJECT_NUMBER` |
| `disableSignatureVerification` | No† | Disable JWT verification entirely (development only). Auto-detected from `GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION=true` |
| `impersonateUser` | No | User email for domain-wide delegation. Auto-detected from `GOOGLE_CHAT_IMPERSONATE_USER` |
| `auth` | No | Custom auth object (advanced) |
| `apiUrl` | No | Override the Google Chat API base URL. Auto-detected from `GOOGLE_CHAT_API_URL` |
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |

*Either `credentials`, `GOOGLE_CHAT_CREDENTIALS` env var, `useApplicationDefaultCredentials`, or `GOOGLE_CHAT_USE_ADC=true` is required.

†One of `googleChatProjectNumber`, `pubsubAudience`, or `disableSignatureVerification: true` is required — the constructor throws otherwise. Configure the verifier(s) for each transport you actually receive; requests of a shape whose verifier is unconfigured are rejected with HTTP 401.

## Environment variables

```bash
Expand All @@ -179,9 +182,10 @@ GOOGLE_CHAT_CREDENTIALS={"type":"service_account",...}
GOOGLE_CHAT_PUBSUB_TOPIC=projects/your-project/topics/chat-events
GOOGLE_CHAT_IMPERSONATE_USER=admin@yourdomain.com

# Optional: webhook verification (recommended for production)
# Webhook verification — at least one of the three is required
GOOGLE_CHAT_PROJECT_NUMBER=123456789 # For direct webhook JWT verification
GOOGLE_CHAT_PUBSUB_AUDIENCE=https://your-domain.com/api/webhooks/gchat # For Pub/Sub JWT verification
# GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION=true # Escape hatch for local dev only

# Optional: override the Google Chat API base URL
GOOGLE_CHAT_API_URL=...
Expand All @@ -191,7 +195,13 @@ GOOGLE_CHAT_API_URL=...

The adapter supports JWT verification for both webhook types. When configured, the adapter validates the `Authorization: Bearer <JWT>` header on incoming requests using Google's public keys. Requests with missing or invalid tokens are rejected with HTTP 401.

Verification is opt-in — when the config options are not set, webhooks are accepted without signature checks (for backward compatibility and development).
Verification is required. The constructor throws `ValidationError` unless one of the following is set:

- `googleChatProjectNumber` (or `GOOGLE_CHAT_PROJECT_NUMBER`) — direct webhooks
- `pubsubAudience` (or `GOOGLE_CHAT_PUBSUB_AUDIENCE`) — Pub/Sub push deliveries
- `disableSignatureVerification: true` (or `GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION=true`) — explicit opt-out, intended for local development only

The two transports share one HTTP endpoint, so each verifier only covers its own request shape. If you only configure `googleChatProjectNumber`, incoming Pub/Sub-shaped requests are rejected with HTTP 401, and vice versa — configure both if you receive both.

### Direct webhooks (Google Chat API)

Expand Down
13 changes: 13 additions & 0 deletions packages/adapter-linear/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ createLinearAdapter({
});
```

### Token encryption

For multi-tenant OAuth installs, pass a base64-encoded 32-byte key as `encryptionKey` (or set `LINEAR_ENCRYPTION_KEY`) to encrypt stored access and refresh tokens at rest using AES-256-GCM:

```bash
openssl rand -base64 32
```

When `encryptionKey` is set, `setInstallation()` encrypts tokens before writing them to the configured state adapter and `getInstallation()` decrypts them transparently. Existing plaintext records continue to work, so you can roll the key in without flushing installs. Without an `encryptionKey`, tokens are stored in plaintext (the previous default).

### Making the bot @-mentionable (optional)

To make the bot appear in Linear's `@` mention dropdown as an Agent:
Expand Down Expand Up @@ -207,6 +217,7 @@ All options are auto-detected from environment variables when not provided.
| `accessToken` | No* | Pre-obtained OAuth access token. Auto-detected from `LINEAR_ACCESS_TOKEN` |
| `clientId` | No* | Multi-tenant OAuth app client ID. Auto-detected from `LINEAR_CLIENT_ID` |
| `clientSecret` | No* | Multi-tenant OAuth app client secret. Auto-detected from `LINEAR_CLIENT_SECRET` |
| `encryptionKey` | No | AES-256-GCM key for encrypting stored OAuth tokens. Auto-detected from `LINEAR_ENCRYPTION_KEY` |
| `clientCredentials` | No* | Single-tenant client credentials config |
| `clientCredentials.scopes` | No | Scopes for client credentials auth. Defaults to `["read", "write", "comments:create", "issues:create"]` |
| `mode` | No | Inbound webhook handling mode. `"comments"` by default, or `"agent-sessions"` for app-actor installs |
Expand Down Expand Up @@ -238,6 +249,8 @@ LINEAR_CLIENT_CREDENTIALS_SCOPES=read,write,comments:create,issues:create
LINEAR_CLIENT_ID=your-client-id
LINEAR_CLIENT_SECRET=your-client-secret
LINEAR_REDIRECT_URI=https://your-domain.com/api/linear/install/callback
# Optional: encrypt stored OAuth tokens at rest
LINEAR_ENCRYPTION_KEY=...

# Optional: inbound webhook mode
# comments | agent-sessions
Expand Down
10 changes: 10 additions & 0 deletions packages/adapter-shared/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ pnpm add @chat-adapter/shared
- `renderGfmTable(headers, rows)` - render a GitHub Flavored Markdown table
- `escapeTableCell(text)` - escape pipe characters in table cells

### Token encryption

AES-256-GCM helpers for encrypting OAuth tokens at rest before writing them to a state adapter. Used by `@chat-adapter/slack` and `@chat-adapter/linear`; available to any adapter that persists credentials.

- `encryptToken(plaintext, key)` - encrypt a string and return an `EncryptedTokenData` envelope (random 12-byte IV per call)
- `decryptToken(data, key)` - decrypt an envelope back to the original string
- `decodeKey(encoded)` - decode a hex-64 or base64-44 encoded 32-byte key; throws on wrong length
- `isEncryptedTokenData(value)` - type guard for distinguishing envelopes from legacy plaintext records
- `EncryptedTokenData` - the envelope type (`{ ciphertext, iv, authTag }`, all base64)
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated

### Error classes

Standardized errors for adapter implementations:
Expand Down
Loading