@hogsend/cli 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +440 -4
- package/dist/bin.js.map +1 -1
- package/package.json +2 -2
- package/skills/hogsend-cli/SKILL.md +1 -0
- package/skills/hogsend-client-sdk/SKILL.md +40 -1
- package/skills/hogsend-client-sdk/references/api-surface.md +99 -0
- package/skills/hogsend-webhooks-and-workflows/SKILL.md +20 -5
- package/skills/hogsend-webhooks-and-workflows/references/webhook-source.md +57 -12
- package/src/commands/index.ts +2 -0
- package/src/commands/webhooks.ts +563 -0
- package/src/lib/http.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hogsend/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"tsup": "^8.5.1",
|
|
33
33
|
"tsx": "^4.22.4",
|
|
34
34
|
"vitest": "^4.1.7",
|
|
35
|
-
"@hogsend/studio": "^0.
|
|
35
|
+
"@hogsend/studio": "^0.8.0",
|
|
36
36
|
"@repo/typescript-config": "0.0.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
@@ -74,6 +74,7 @@ Most commands READ (admin API). A handful WRITE through the data plane — marke
|
|
|
74
74
|
| `hogsend events <userId>` | Raw event stream for one user (READ — `<userId>` stays the read path). |
|
|
75
75
|
| `hogsend events send <name>` | **(write)** Push an event → `POST /v1/events`. `--email`/`--user-id` (≥1 required), `--prop`/`--props` (event props), `--contact-prop`/`--contact-props` (contact props), `--list`/`--unlist`, `--idempotency-key`, `--timestamp`. |
|
|
76
76
|
| `hogsend emails send <template>` | **(write)** Send a transactional email → `POST /v1/emails`. `--to`/`--user-id` (≥1 required), `--prop`/`--props`, `--subject`, `--from`, `--reply-to`, `--category`, `--idempotency-key`, `--skip-preference-check` (needs full-admin). |
|
|
77
|
+
| `hogsend webhooks list/get/create/update/delete/rotate-secret/test` | Manage **outbound** signed webhook endpoints (the event stream Hogsend emits to your URLs) → `/v1/admin/webhooks`. Needs the **admin key**, not the data key. `create --url <url>` + repeatable `--event <type>` or `--all-events`; the signing secret prints ONCE on `create` + `rotate-secret`. |
|
|
77
78
|
| `hogsend skills list/add` | Manage these bundled agent skills. |
|
|
78
79
|
| `hogsend upgrade` | Bump `@hogsend/*` deps to latest + refresh vendored skills. |
|
|
79
80
|
| `hogsend setup` | Interactive LOCAL onboarding (docker, secret, migrate). |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hogsend-client-sdk
|
|
3
|
-
description: Use when calling Hogsend from your own product/app code (a signup handler, a billing webhook, a cron) via the @hogsend/client SDK + public data-plane API — new Hogsend({ baseUrl, apiKey }), then contacts.upsert/find/delete, events.send (alias .track), emails.send, lists.list/subscribe/unsubscribe. Teaches the contactProperties-vs-eventProperties split on POST /v1/events, the ingest-scoped HOGSEND_API_KEY, the 202 + listsError warning, and HogsendAPIError/RateLimitError. NOT for use inside a journey (there, use sendEmail()/ctx.trigger()). The scaffold ships a preconfigured `hs` at src/lib/hogsend.ts.
|
|
3
|
+
description: Use when calling Hogsend from your own product/app code (a signup handler, a billing webhook, a cron) via the @hogsend/client SDK + public data-plane API — new Hogsend({ baseUrl, apiKey }), then contacts.upsert/find/delete, events.send (alias .track), emails.send, lists.list/subscribe/unsubscribe, webhooks.create/list/get/update/delete/rotateSecret/sendTest (ADMIN plane — needs a full-admin key), and verifyHogsendWebhook for the subscriber side. Teaches the contactProperties-vs-eventProperties split on POST /v1/events, the ingest-scoped HOGSEND_API_KEY, the 202 + listsError warning, and HogsendAPIError/RateLimitError. NOT for use inside a journey (there, use sendEmail()/ctx.trigger()). The scaffold ships a preconfigured `hs` at src/lib/hogsend.ts.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: withSeismic
|
|
@@ -103,6 +103,45 @@ await hs.emails.send({ // POST /v1/emails
|
|
|
103
103
|
await hs.lists.list(); // GET /v1/lists -> ListSummary[]
|
|
104
104
|
await hs.lists.subscribe({ list: "newsletter", email: "ada@example.com" });
|
|
105
105
|
await hs.lists.unsubscribe({ list: "newsletter", userId: "u_1" });
|
|
106
|
+
|
|
107
|
+
// Webhooks (ADMIN plane — needs a full-admin apiKey, NOT the ingest key) ---
|
|
108
|
+
await hs.webhooks.create({ // POST /v1/admin/webhooks
|
|
109
|
+
url: "https://your.app/hooks",
|
|
110
|
+
eventTypes: ["contact.created", "email.sent"],
|
|
111
|
+
}); // -> endpoint incl. full `secret` (shown ONCE)
|
|
112
|
+
await hs.webhooks.list(); // -> WebhookEndpoint[]
|
|
113
|
+
await hs.webhooks.rotateSecret("we_123"); // -> { id, secret, secretPrefix } (secret ONCE)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## `hs.webhooks` — outbound endpoints (DIFFERENT plane + key)
|
|
117
|
+
|
|
118
|
+
`hs.webhooks.*` manages the **outbound** signed event stream Hogsend emits to
|
|
119
|
+
your URLs (`contact.*`, `email.*`, `journey.completed`, `bucket.*`). Unlike every
|
|
120
|
+
other resource above, it targets the **ADMIN plane** (`/v1/admin/webhooks`) and
|
|
121
|
+
**requires a full-admin `apiKey`** — a leaked ingest key must never register an
|
|
122
|
+
exfiltration endpoint. Construct a separate `Hogsend` instance with an admin key
|
|
123
|
+
if your data-plane `hs` only holds an ingest key.
|
|
124
|
+
|
|
125
|
+
`create`/`list`/`get`/`update`/`delete`/`rotateSecret`/`sendTest`. The full
|
|
126
|
+
signing secret (`whsec_…`) is returned **only once** — on `create` and
|
|
127
|
+
`rotateSecret`; `list`/`get` only expose `secretPrefix`. Store it on create.
|
|
128
|
+
|
|
129
|
+
**Subscriber side:** in the handler that RECEIVES Hogsend's signed POSTs, call
|
|
130
|
+
`verifyHogsendWebhook({ payload, headers, secret })` (also exported from
|
|
131
|
+
`@hogsend/client`). Pass the **raw request body bytes** (never a re-stringified
|
|
132
|
+
object); it returns the parsed `{ id, type, timestamp, data }` envelope and
|
|
133
|
+
THROWS on a bad/missing signature or a timestamp outside the 5-minute tolerance.
|
|
134
|
+
Deliveries are at-least-once — dedupe on the `Webhook-Id` header.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { verifyHogsendWebhook } from "@hogsend/client";
|
|
138
|
+
|
|
139
|
+
const event = verifyHogsendWebhook({
|
|
140
|
+
payload: rawBody, // the EXACT bytes Hogsend signed
|
|
141
|
+
headers: req.headers,
|
|
142
|
+
secret: process.env.HOGSEND_WEBHOOK_SECRET!, // whsec_… from create / rotate
|
|
143
|
+
});
|
|
144
|
+
// switch on event.type …
|
|
106
145
|
```
|
|
107
146
|
|
|
108
147
|
## `eventProperties` vs `contactProperties` (the split that trips people up)
|
|
@@ -157,6 +157,105 @@ For how `subscribe`/`unsubscribe` interact with a list's `defaultOptIn` polarity
|
|
|
157
157
|
(opt-in needs an exact `true`, opt-out is blocked only on an exact `false`), see
|
|
158
158
|
the hogsend-authoring-lists skill.
|
|
159
159
|
|
|
160
|
+
## `hs.webhooks` (ADMIN plane — full-admin key required)
|
|
161
|
+
|
|
162
|
+
Manage **outbound** webhook endpoints (the Svix-style signed event stream Hogsend
|
|
163
|
+
emits to subscriber URLs). Every method targets `/v1/admin/webhooks` and requires
|
|
164
|
+
a **full-admin** `apiKey` — NOT the ingest data key the resources above use.
|
|
165
|
+
Signing-secret management is the same trust class as API-key management.
|
|
166
|
+
|
|
167
|
+
### `webhooks.create(input) → CreatedWebhookEndpoint`
|
|
168
|
+
|
|
169
|
+
`POST /v1/admin/webhooks`. Register an endpoint subscribed to one or more of the
|
|
170
|
+
12 outbound event types. Returns the endpoint INCLUDING the full signing
|
|
171
|
+
`secret` (`whsec_…`) — shown ONLY here and on `rotateSecret`. Store it now.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
type CreateWebhookInput = {
|
|
175
|
+
url: string;
|
|
176
|
+
eventTypes: OutboundEventType[]; // contact.* | email.* | journey.completed | bucket.*
|
|
177
|
+
description?: string;
|
|
178
|
+
disabled?: boolean;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
interface WebhookEndpoint {
|
|
182
|
+
id: string; // we_…
|
|
183
|
+
url: string;
|
|
184
|
+
description: string | null;
|
|
185
|
+
eventTypes: OutboundEventType[];
|
|
186
|
+
secretPrefix: string; // first 12 chars, e.g. whsec_AbCd
|
|
187
|
+
status: "enabled" | "disabled";
|
|
188
|
+
organizationId: string | null;
|
|
189
|
+
lastDeliveryAt: string | null; // ISO
|
|
190
|
+
createdAt: string; // ISO
|
|
191
|
+
updatedAt: string; // ISO
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
type CreatedWebhookEndpoint = WebhookEndpoint & { secret: string }; // full secret ONCE
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `webhooks.list(opts?) → WebhookEndpoint[]`
|
|
198
|
+
|
|
199
|
+
`GET /v1/admin/webhooks`. Newest first; `opts` = `{ limit?, offset?,
|
|
200
|
+
includeDisabled? }`. Returns the endpoints array (unwrapped from the
|
|
201
|
+
`{ endpoints, total, limit, offset }` envelope). No `secret` — `secretPrefix` only.
|
|
202
|
+
|
|
203
|
+
### `webhooks.get(id) → WebhookEndpoint`
|
|
204
|
+
|
|
205
|
+
`GET /v1/admin/webhooks/{id}`. One endpoint (404 → `HogsendAPIError`). No secret.
|
|
206
|
+
|
|
207
|
+
### `webhooks.update(id, input) → WebhookEndpoint`
|
|
208
|
+
|
|
209
|
+
`PATCH /v1/admin/webhooks/{id}`. Only the provided fields change;
|
|
210
|
+
`description: null` clears it. Does NOT return or rotate the secret.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
type UpdateWebhookInput = {
|
|
214
|
+
url?: string;
|
|
215
|
+
eventTypes?: OutboundEventType[];
|
|
216
|
+
description?: string | null;
|
|
217
|
+
disabled?: boolean;
|
|
218
|
+
};
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### `webhooks.delete(id) → { deleted }`
|
|
222
|
+
|
|
223
|
+
`DELETE /v1/admin/webhooks/{id}`. Hard-delete; cascade drops its deliveries.
|
|
224
|
+
|
|
225
|
+
### `webhooks.rotateSecret(id) → { id, secret, secretPrefix }`
|
|
226
|
+
|
|
227
|
+
`POST /v1/admin/webhooks/{id}/rotate-secret`. Hard cutover — the OLD secret is
|
|
228
|
+
invalidated immediately; in-flight retries re-sign with the new one. The new
|
|
229
|
+
`secret` is returned ONCE. Update every subscriber.
|
|
230
|
+
|
|
231
|
+
### `webhooks.sendTest(id) → { enqueued, eventType: "webhook.test" }`
|
|
232
|
+
|
|
233
|
+
`POST /v1/admin/webhooks/{id}/test`. Enqueues an out-of-band `webhook.test`
|
|
234
|
+
delivery, sent regardless of the endpoint's subscribed `eventTypes`.
|
|
235
|
+
|
|
236
|
+
## `verifyHogsendWebhook(opts)` — the subscriber side
|
|
237
|
+
|
|
238
|
+
A standalone helper exported from `@hogsend/client` (not on the `Hogsend`
|
|
239
|
+
instance) for the handler that RECEIVES Hogsend's signed POSTs.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { verifyHogsendWebhook } from "@hogsend/client";
|
|
243
|
+
|
|
244
|
+
const event = verifyHogsendWebhook({
|
|
245
|
+
payload: rawBody, // the EXACT raw request body bytes — never a re-stringified object
|
|
246
|
+
headers: req.headers,
|
|
247
|
+
secret: "whsec_…", // the endpoint secret from create / rotateSecret
|
|
248
|
+
});
|
|
249
|
+
// event === { id, type, timestamp, data }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- Returns the parsed envelope (`{ id, type, timestamp, data }`) on success.
|
|
253
|
+
- **THROWS** on a bad signature, a missing signature header, or a timestamp
|
|
254
|
+
outside the 5-minute tolerance — wrap in `try/catch` and reply `401` on throw.
|
|
255
|
+
- Accepts both the `Webhook-*` and `svix-*` header aliases (case-insensitive).
|
|
256
|
+
Uses svix when available, with a pure `node:crypto` HMAC-SHA256 fallback.
|
|
257
|
+
- Deliveries are **at-least-once** — dedupe on the `Webhook-Id` (`event.id`).
|
|
258
|
+
|
|
160
259
|
## Construction options (recap)
|
|
161
260
|
|
|
162
261
|
```ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hogsend-webhooks-and-workflows
|
|
3
|
-
description: Use when adding an inbound webhook source in src/webhook-sources/ (defineWebhookSource — auth
|
|
3
|
+
description: Use when adding an inbound webhook source in src/webhook-sources/ (defineWebhookSource — auth as a match|signature discriminated union, optional Zod schema, transform(payload, ctx) -> IngestEvent | null, served at POST /v1/webhooks/:id), reaching for a built-in integration preset (Clerk/Supabase/Stripe/Segment), or a custom Hatchet task in src/workflows/ passed as extraWorkflows (NOT workflows) to createWorker, including the idempotent batched expand→migrate→contract backfill pattern. Outbound signed webhooks are managed separately (hogsend webhooks CLI / hs.webhooks).
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: withSeismic
|
|
@@ -25,10 +25,20 @@ Relative imports use the ESM `.js` extension.
|
|
|
25
25
|
|
|
26
26
|
- **`defineWebhookSource({ meta, auth, schema?, transform })`** (from
|
|
27
27
|
`@hogsend/engine`) — declares one source served at `POST /v1/webhooks/:id`.
|
|
28
|
-
`auth`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
`auth` is a **discriminated union on `type`**: `"match"` (shared-secret
|
|
29
|
+
equality against a header/`Authorization: Bearer`; OPEN when the secret is
|
|
30
|
+
unset) or `"signature"` (`scheme: "svix" | "stripe" | "hmac-hex"`, with an
|
|
31
|
+
`envKey`, optional `header`/`fallbackMatchHeader`; FAILS CLOSED with 401 when
|
|
32
|
+
the secret is unset). `schema` is an optional Zod validator; `transform(payload,
|
|
33
|
+
ctx)` returns an `IngestEvent | null` (`null` = accept-and-skip). Register
|
|
34
|
+
sources in `src/webhook-sources/index.ts` and pass them to
|
|
35
|
+
`createApp(client, { webhookSources })` in `src/index.ts`.
|
|
36
|
+
- **Built-in integration presets** — the engine ships four ready-made inbound
|
|
37
|
+
sources (Clerk, Supabase, Stripe, Segment) served at
|
|
38
|
+
`POST /v1/webhooks/{clerk,supabase,stripe,segment}` with no code to write. Each
|
|
39
|
+
mounts only when its secret env var is set AND `ENABLED_WEBHOOK_PRESETS`
|
|
40
|
+
allows it (`"*"`/absent = auto, a csv of ids = exactly those, `"none"` = off).
|
|
41
|
+
Defining your own source with the SAME id overrides the preset (you win).
|
|
32
42
|
- **`IngestEvent`** — the shape `transform` must return:
|
|
33
43
|
`{ event, userId, userEmail, properties, idempotencyKey? }`. The route feeds it
|
|
34
44
|
straight into `ingestEvent()`, so a webhook can enroll users into journeys.
|
|
@@ -66,3 +76,8 @@ Relative imports use the ESM `.js` extension.
|
|
|
66
76
|
helpers.
|
|
67
77
|
- To verify a webhook or task against a running instance (events landing,
|
|
68
78
|
contacts upserted, journeys firing), see the **hogsend-cli** skill.
|
|
79
|
+
- **Inbound vs outbound:** this skill is about *inbound* sources (HTTP → engine).
|
|
80
|
+
The engine also emits an *outbound* signed event stream (`contact.*`,
|
|
81
|
+
`email.*`, `journey.completed`, `bucket.*`) to subscriber URLs — manage those
|
|
82
|
+
endpoints with `hogsend webhooks …` (hogsend-cli skill) or `hs.webhooks.*`
|
|
83
|
+
(hogsend-client-sdk skill), and verify deliveries with `verifyHogsendWebhook`.
|
|
@@ -18,19 +18,40 @@ You write the source files; the engine owns the route. Edit only
|
|
|
18
18
|
| `meta.id` | `string` | The `:sourceId` segment in the URL. Keep it URL-safe. |
|
|
19
19
|
| `meta.name` | `string` | Human label. |
|
|
20
20
|
| `meta.description?` | `string` | Optional. |
|
|
21
|
-
| `auth
|
|
22
|
-
| `auth.envKey` | `string` | Env var holding the expected secret value. |
|
|
23
|
-
| `auth.type` | `"match"` | Only mode today: header value must equal the env value. |
|
|
21
|
+
| `auth` | discriminated union on `type` | `"match"` or `"signature"` — see below. |
|
|
24
22
|
| `schema?` | `z.ZodSchema<T>` | Optional Zod validator; on success `payload` is typed `T`. |
|
|
25
23
|
| `transform(payload, ctx)` | `=> Promise<IngestEvent \| null>` | Map payload → event. Return `null` to accept-and-skip. |
|
|
26
24
|
|
|
27
|
-
### Auth
|
|
25
|
+
### Auth: a discriminated union on `type`
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
`auth` is a discriminated union — pick the variant that matches your provider:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// "match" — plain shared-secret equality (the PostHog scaffold source uses this)
|
|
31
|
+
auth: { type: "match"; header: string; envKey: string }
|
|
32
|
+
|
|
33
|
+
// "signature" — provider HMAC verification over the EXACT raw body bytes
|
|
34
|
+
auth: {
|
|
35
|
+
type: "signature";
|
|
36
|
+
scheme: "svix" | "stripe" | "hmac-hex";
|
|
37
|
+
envKey: string;
|
|
38
|
+
header?: string; // the signature header to read
|
|
39
|
+
fallbackMatchHeader?: string; // e.g. Supabase's plain x-supabase-webhook-secret
|
|
40
|
+
verify?(args): boolean | Promise<boolean>; // optional override of the scheme
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Auth behaviour (important — the two variants differ on the unset-secret case):**
|
|
45
|
+
|
|
46
|
+
- **`"match"`** enforces auth **only when the env secret is set**. If
|
|
47
|
+
`process.env[envKey]` is empty/undefined the source is treated as **open** (no
|
|
48
|
+
auth). When the secret is present, the request must send it in `header` or as
|
|
49
|
+
`Authorization: Bearer <secret>`, else the route returns `401`. Always set the
|
|
50
|
+
secret in any non-local environment.
|
|
51
|
+
- **`"signature"`** **FAILS CLOSED** — when its secret is unset the route returns
|
|
52
|
+
`401` and never reaches `transform`. The route reads the EXACT raw body once
|
|
53
|
+
and verifies the HMAC (Svix / Stripe `t=…,v1=…` with 5-min tolerance / generic
|
|
54
|
+
hex HMAC) over those bytes. The raw bytes are also exposed as `ctx.rawBody`.
|
|
34
55
|
|
|
35
56
|
### Validation
|
|
36
57
|
|
|
@@ -52,9 +73,11 @@ interface IngestEvent {
|
|
|
52
73
|
}
|
|
53
74
|
```
|
|
54
75
|
|
|
55
|
-
`ctx` is `{ db, logger }` — a Drizzle `Database
|
|
56
|
-
|
|
57
|
-
|
|
76
|
+
`ctx` is `{ db, logger, rawBody?, headers? }` — a Drizzle `Database`, the engine
|
|
77
|
+
logger, and (populated by the route) the EXACT raw request body bytes and the
|
|
78
|
+
lowercased request headers, for lookups/diagnostics or provider-specific raw
|
|
79
|
+
access inside the transform. It does **not** carry `hatchet` or the registry;
|
|
80
|
+
those are applied by the route when it calls `ingestEvent`.
|
|
58
81
|
|
|
59
82
|
Notes that match the engine's behaviour:
|
|
60
83
|
|
|
@@ -160,6 +183,28 @@ const app = createApp(client, { webhookSources });
|
|
|
160
183
|
|
|
161
184
|
That's it. Your source is now live at `POST /v1/webhooks/stripe`.
|
|
162
185
|
|
|
186
|
+
## Built-in integration presets (no code)
|
|
187
|
+
|
|
188
|
+
Before writing a source, check whether the engine already ships one. Four
|
|
189
|
+
presets are built into `@hogsend/engine` and served with no consumer code:
|
|
190
|
+
|
|
191
|
+
| Preset | Route | Scheme | Secret env var |
|
|
192
|
+
|--------|-------|--------|----------------|
|
|
193
|
+
| `clerk` | `POST /v1/webhooks/clerk` | `svix` | `CLERK_WEBHOOK_SECRET` |
|
|
194
|
+
| `supabase` | `POST /v1/webhooks/supabase` | `svix` (+ plain `x-supabase-webhook-secret` fallback) | `SUPABASE_WEBHOOK_SECRET` |
|
|
195
|
+
| `stripe` | `POST /v1/webhooks/stripe` | `stripe` | `STRIPE_WEBHOOK_SECRET` |
|
|
196
|
+
| `segment` | `POST /v1/webhooks/segment` | `hmac-hex` | `SEGMENT_WEBHOOK_SECRET` |
|
|
197
|
+
|
|
198
|
+
A preset mounts only when **both** its secret env var is set **and**
|
|
199
|
+
`ENABLED_WEBHOOK_PRESETS` allows it: `"*"`/absent = auto (every preset whose
|
|
200
|
+
secret is set), a comma-separated list of ids = exactly those (still requires the
|
|
201
|
+
secret), `"none"` = all off. A preset with no secret is never mounted (signature
|
|
202
|
+
sources fail closed). Defining your own `defineWebhookSource` with the SAME id
|
|
203
|
+
**overrides** the preset — the consumer always wins. Each preset's exact
|
|
204
|
+
provider-event → Hogsend-event mapping (and the `contactProperties` vs
|
|
205
|
+
`eventProperties` split) lives in its source file under
|
|
206
|
+
`packages/engine/src/webhook-sources/presets/`.
|
|
207
|
+
|
|
163
208
|
## Authoring a new source — checklist
|
|
164
209
|
|
|
165
210
|
1. Create `src/webhook-sources/<id>.ts` exporting a `defineWebhookSource({...})`.
|
package/src/commands/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { statsCommand } from "./stats.js";
|
|
|
12
12
|
import { studioCommand } from "./studio.js";
|
|
13
13
|
import type { Command } from "./types.js";
|
|
14
14
|
import { upgradeCommand } from "./upgrade.js";
|
|
15
|
+
import { webhooksCommand } from "./webhooks.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* The command registry. The router (src/bin.ts) matches the leading argv token
|
|
@@ -29,6 +30,7 @@ export const commands: Command[] = [
|
|
|
29
30
|
eventsCommand,
|
|
30
31
|
emailsCommand,
|
|
31
32
|
campaignsCommand,
|
|
33
|
+
webhooksCommand,
|
|
32
34
|
studioCommand,
|
|
33
35
|
setupCommand,
|
|
34
36
|
skillsCommand,
|