@hogsend/cli 0.7.0 → 0.9.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 +441 -4
- package/dist/bin.js.map +1 -1
- package/package.json +2 -2
- package/skills/hogsend-authoring-destinations/SKILL.md +217 -0
- package/skills/hogsend-authoring-emails/references/tracking-and-unsubscribe.md +7 -2
- package/skills/hogsend-authoring-journeys/SKILL.md +11 -4
- package/skills/hogsend-authoring-journeys/references/journey-context.md +22 -8
- package/skills/hogsend-cli/SKILL.md +1 -0
- package/skills/hogsend-client-sdk/SKILL.md +58 -1
- package/skills/hogsend-client-sdk/references/api-surface.md +108 -0
- package/skills/hogsend-extending/SKILL.md +42 -11
- package/skills/hogsend-extending/references/swap-a-provider.md +15 -2
- package/skills/hogsend-webhooks-and-workflows/SKILL.md +32 -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 +566 -0
- package/src/lib/http.ts +6 -0
|
@@ -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,20 @@ 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* event stream (`contact.*`, `email.*`,
|
|
81
|
+
`journey.completed`, `bucket.*`). Two halves:
|
|
82
|
+
- **Subscriber endpoints** — manage the `webhook_endpoints` rows with
|
|
83
|
+
`hogsend webhooks …` (hogsend-cli skill) or `hs.webhooks.*` (hogsend-client-sdk
|
|
84
|
+
skill); verify signed deliveries with `verifyHogsendWebhook`. `create`/`update`
|
|
85
|
+
now take a `kind` + `config`: the default `kind="webhook"` is the byte-identical
|
|
86
|
+
signed `whsec_` POST (returns a one-time `secret`); a keyed destination
|
|
87
|
+
(`kind="posthog"|"segment"|"slack"|…`) carries its credentials in `config`
|
|
88
|
+
(e.g. `{ apiKey }`) and returns NO secret. Same durable retry/backoff/DLQ spine
|
|
89
|
+
either way — `kind` just selects the delivery-time transform.
|
|
90
|
+
- **Code-defined destinations** — author a delivery-time transform for a new
|
|
91
|
+
fan-out TARGET (a CRM, a warehouse, a custom shape) with `defineDestination()`
|
|
92
|
+
in `src/destinations/`. This is the symmetric twin of `defineWebhookSource`
|
|
93
|
+
on the OUTBOUND side. → the **hogsend-authoring-destinations** skill. NOTE:
|
|
94
|
+
outbound is no longer "just code in a journey" — for event fan-out, reach for
|
|
95
|
+
a destination, not a per-step integration call.
|
|
@@ -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,
|