Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion docs/agent-evaluation/hookdeck-outpost-agent-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Use judgment; when two paths seem possible, prefer **Quick path** unless they cl
- **Go** → **Go quickstart** + official Go SDK as that doc shows.
- **curl**, **HTTP only**, or **REST** without a language SDK → **curl quickstart** + OpenAPI.

Do **not** mix argument styles across languages (e.g. do not apply TypeScript `publish.event({ ... })` shapes to Python).
Do **not** mix argument styles across languages (e.g. do not apply TypeScript `outpost.publish({ ... })` shapes to Python).

### Quick path — how to deliver

Expand Down
2 changes: 1 addition & 1 deletion docs/agent-evaluation/scenarios/02-basics-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Paste the **## Template** block from `[hookdeck-outpost-agent-prompt.mdoc](../..
**Measurement:** Heuristic `scoreScenario02` in [`src/score-transcript.ts`](../src/score-transcript.ts); LLM judge maps the bullets below ([README.md § Measuring scenarios](../README.md#measuring-scenarios)). Execution row is manual.

- Depends on `@hookdeck/outpost-sdk`; uses `Outpost` client with `apiKey` from `process.env.OUTPOST_API_KEY`.
- Calls `tenants.upsert`, `destinations.create` (webhook), `publish.event`.
- Calls `tenants.upsert`, `destinations.create` (webhook), `outpost.publish`.
- Uses a topic that matches the dashboard list from the prompt (or asks which topic if ambiguous).
- Webhook URL from `OUTPOST_TEST_WEBHOOK_URL` (or clearly documented env).
- No API key in source; fails fast if env missing.
Expand Down
10 changes: 10 additions & 0 deletions docs/agent-evaluation/src/run-agent-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,11 @@ Agent cwd is usually the run directory. Scenarios may define ## Eval harness (JS
);
if (report.overallTranscriptPass === false) {
anyScoreFailure = true;
for (const c of report.transcript.checks) {
if (!c.pass) {
console.error(` [FAIL] ${c.id}: ${c.detail}`);
}
}
}
}

Expand All @@ -1032,6 +1037,11 @@ Agent cwd is usually the run directory. Scenarios may define ## Eval harness (JS
);
if (!llmReport.overall_transcript_pass) {
anyScoreFailure = true;
for (const c of llmReport.criteria) {
if (!c.pass) {
console.error(` [FAIL] ${c.criterion}: ${c.evidence}`);
}
}
}
}

Expand Down
6 changes: 4 additions & 2 deletions docs/agent-evaluation/src/score-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,13 @@ function scoreScenario02(corpus: string, assistant: string): TranscriptScore {
detail: dest ? "Calls destinations.create" : "Expected destinations.create",
});

const pub = /publish\.event|publish\?\.event/.test(t);
// TS SDK >=1.3.0 exposes `outpost.publish(...)` directly. Keep `publish.event`
// as a fallback so older transcripts (and stray references in comments) still match.
const pub = /\.publish\s*\(|publish\.event|publish\?\.event/.test(t);
checks.push({
id: "publish_event",
pass: pub,
detail: pub ? "Calls publish.event" : "Expected publish.event",
detail: pub ? "Calls outpost.publish(…) or publish.event" : "Expected outpost.publish(…) or publish.event",
});

const hookUrl = /OUTPOST_TEST_WEBHOOK_URL/.test(t);
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-evaluation/src/transcript-trajectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ function toolInputWritePath(
}

const SDK_HINT_PATTERNS: ReadonlyArray<{ id: string; re: RegExp }> = [
{ id: "ts_publish.event", re: /\bpublish\.event\b/i },
{ id: "ts_publish", re: /\.publish\s*\(/ },
{ id: "ts_tenants.upsert", re: /\btenants\.upsert\b/i },
{ id: "ts_destinations.create", re: /\bdestinations\.create\b/i },
{ id: "py_publish", re: /\bpublish\.event\b/i },
Expand Down
95 changes: 87 additions & 8 deletions docs/content/destinations/webhook.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,82 @@ In **Standard Webhooks** mode, the same value is sent as the **`webhook-id`** he

### Default Mode

The signature is computed over the timestamp and request body:
The signature is computed over the request body:

```
HMAC-SHA256(secret, "${body}")
```

The `x-outpost-signature` header value follows the format: `v0=${signature}`

If the destination has an unexpired `previous_secret` during secret rotation, Outpost includes one signature for each valid secret in the same header. The current secret's signature is first, followed by the previous secret's signature:
`v0=<signature-from-current-secret>,<signature-from-previous-secret>`

To verify:
1. Extract the timestamp and signature from the header
2. Compute the expected signature using your secret
3. Compare signatures using a constant-time comparison
4. Optionally reject requests with old timestamps to prevent replay attacks
1. Extract the signature header
2. Split the `v0=` value on commas
3. Compute the expected signature using each active secret you accept
4. Accept the request if any signature matches using a constant-time comparison
5. Optionally reject requests with old timestamps to prevent replay attacks

#### Custom signature templates

In default mode, operators can customize both the signed content and the signature header value:

- `DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE` controls the string passed to HMAC.
- `DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE` controls the value written to the signature header.

These use the same configuration key names in both deployment models, but they are applied differently: Managed Outpost stores them through the Config API or Hookdeck dashboard, while self-hosted Outpost reads them from environment variables or YAML configuration.

Templates use Go template syntax with helper functions such as `join`. The signature content template can use:

| Field | Description |
|-------|-------------|
| `.EventID` | Event id used for the delivery |
| `.Topic` | Event topic |
| `.Timestamp` | Delivery timestamp |
| `.Body` | Raw request body |

The signature header template can use the same metadata plus `.Signatures`, which is the list of generated signatures for all valid secrets. During secret rotation, `.Signatures` contains the current secret's signature first and the previous secret's signature second.

For example, to include a Unix timestamp in the signed content and header:

{% tabs tabGroup="deployment" %}
{% tab label="Managed" %}
Set the values in the [Config API](/docs/outpost/api#configuration) or in [Hookdeck Destinations settings](https://dashboard.hookdeck.com/settings/project/destinations). For the Config API, send the configuration keys in the JSON body:

```json
{
"DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE": "{{.Timestamp.Unix}}.{{.Body}}",
"DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE": "t={{.Timestamp.Unix}},v0={{.Signatures | join \",\"}}"
}
```
{% /tab %}
{% tab label="Self-Hosted" %}
Set the values as environment variables:

```sh
DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE="{{.Timestamp.Unix}}.{{.Body}}"
DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE="t={{.Timestamp.Unix}},v0={{.Signatures | join \",\"}}"
```

You can also set `signature_content_template` and `signature_header_template` in YAML under `destinations.webhook`.
{% /tab %}
{% /tabs %}

With one secret, the header looks like:

```
x-outpost-signature: t=1717249416,v0=<signature-from-current-secret>
```

During rotation, the same template preserves both signatures:

```
x-outpost-signature: t=1717249416,v0=<signature-from-current-secret>,<signature-from-previous-secret>
```

If you customize `DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE`, keep `.Signatures` in the output so receivers can verify requests during secret rotation.

### Standard Webhooks Mode

Expand All @@ -106,6 +169,12 @@ base64(HMAC-SHA256(secret, "${webhook-id}.${timestamp}.${body}"))

Use the official [Standard Webhooks SDK](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries) to verify signatures. Secrets use the `whsec_<base64>` format.

During secret rotation, Outpost includes one Standard Webhooks signature entry per valid secret in the `webhook-signature` header. The current secret's signature is first, followed by the previous secret's signature:

```
webhook-signature: v1,<signature-from-current-secret> v1,<signature-from-previous-secret>
```

{% tabs tabGroup="deployment" %}
{% tab label="Managed" %}
Enable Standard Webhooks mode by setting `DESTINATIONS_WEBHOOK_MODE=standard` in the Config API or in [Hookdeck Destinations settings](https://dashboard.hookdeck.com/settings/project/destinations).
Expand Down Expand Up @@ -140,7 +209,17 @@ When rotation is triggered:
1. The current secret becomes `previous_secret`
2. A new secret is generated
3. The previous secret remains valid until `previous_secret_invalid_at` (default: 24 hours)
4. Both secrets appear in the signature header during the rotation window
4. During the rotation window, the signature header contains signatures generated with both valid secrets
5. After `previous_secret_invalid_at`, the previous secret is no longer included and the signature header returns to a single signature

Signature header format depends on the webhook mode. With the default header prefix:

| Mode | Header | During rotation |
|------|--------|-----------------|
| Default | `x-outpost-signature` | `v0=<signature-from-current-secret>,<signature-from-previous-secret>` |
| Standard Webhooks | `webhook-signature` | `v1,<signature-from-current-secret> v1,<signature-from-previous-secret>` |

Receivers should treat rotation as an allow-list period: verify the request against the current secret and the previous secret, and accept the request if any signature in the header matches one of those secrets. Outpost signs with the current secret first.

## Custom Headers

Expand Down Expand Up @@ -199,7 +278,7 @@ Configure webhook operator behavior using these keys in the Config API or in [Ho
| `DESTINATIONS_WEBHOOK_MODE` | `default` | Set to `standard` for Standard Webhooks compliance |
| `DESTINATIONS_WEBHOOK_SIGNATURE_ALGORITHM` | `hmac-sha256` | Signature algorithm |
| `DESTINATIONS_WEBHOOK_SIGNATURE_ENCODING` | `hex` | Encoding: `hex` or `base64` |
| `DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE` | `{{.Body}}` | Template for signed content |
| `DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE` | `v0={{.Signatures \| join ","}}` | Template for signature header value |
| `DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE` | `{{.Body}}` | Template for signed content in default mode |
| `DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE` | `v0={{.Signatures \| join ","}}` | Template for signature header value in default mode |
{% /tab %}
{% /tabs %}
Loading