khotan-data 0.0.1 → 0.1.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/AGENTS.md +54 -0
- package/README.md +62 -0
- package/dist/cli.js +2585 -0
- package/dist/factory.cjs +2319 -0
- package/dist/factory.cjs.map +1 -0
- package/dist/factory.d.cts +475 -0
- package/dist/factory.d.ts +475 -0
- package/dist/factory.js +2311 -0
- package/dist/factory.js.map +1 -0
- package/dist/plug-client.cjs +99 -0
- package/dist/plug-client.cjs.map +1 -0
- package/dist/plug-client.d.cts +71 -0
- package/dist/plug-client.d.ts +71 -0
- package/dist/plug-client.js +96 -0
- package/dist/plug-client.js.map +1 -0
- package/dist/templates/agent-skill.md +73 -0
- package/dist/templates/agents.md +41 -0
- package/dist/templates/catch.example.ts +36 -0
- package/dist/templates/catch.ts +107 -0
- package/dist/templates/config-page.tsx +20 -0
- package/dist/templates/debug-index-page.tsx +101 -0
- package/dist/templates/debug-page.tsx +48 -0
- package/dist/templates/graph-page.tsx +11 -0
- package/dist/templates/hub.tsx +450 -0
- package/dist/templates/inflow.example.ts +61 -0
- package/dist/templates/inflow.ts +99 -0
- package/dist/templates/khotan-config.ts +40 -0
- package/dist/templates/khotan-route.ts +13 -0
- package/dist/templates/logs-page.tsx +9 -0
- package/dist/templates/logs.tsx +20 -0
- package/dist/templates/outflow.example.ts +52 -0
- package/dist/templates/outflow.ts +90 -0
- package/dist/templates/pass.example.ts +51 -0
- package/dist/templates/pass.ts +124 -0
- package/dist/templates/plug-debugger.tsx +1185 -0
- package/dist/templates/plug.example.ts +93 -0
- package/dist/templates/plug.ts +806 -0
- package/dist/templates/relay.example.ts +61 -0
- package/dist/templates/relay.ts +95 -0
- package/dist/templates/runs-table.tsx +592 -0
- package/dist/templates/schema.ts +424 -0
- package/dist/templates/skill-dashboard.md +144 -0
- package/dist/templates/skill-plug.md +193 -0
- package/dist/templates/skill-setup.md +119 -0
- package/dist/templates/skill-webhook.md +196 -0
- package/dist/templates/topology-canvas.tsx +1406 -0
- package/dist/templates/var-panel.tsx +276 -0
- package/dist/templates/webhook-events-table.tsx +241 -0
- package/dist/templates/wire-panel.tsx +216 -0
- package/dist/templates/wire.ts +155 -0
- package/package.json +46 -5
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: khotan-plug
|
|
3
|
+
description: >
|
|
4
|
+
Create and configure khotan Plugs — HTTP clients for external APIs with
|
|
5
|
+
auth, retry, pagination, and typed endpoints. Use when connecting to a
|
|
6
|
+
new API, defining endpoint contracts, configuring authentication, or
|
|
7
|
+
creating a typed API client.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Create and configure khotan Plugs — HTTP clients for external APIs with auth, retry, pagination, and typed endpoints. Use when connecting to a new API, defining endpoint contracts, configuring authentication, or creating a typed API client.
|
|
11
|
+
|
|
12
|
+
## Scaffold
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx khotan add plug --yes
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Creates `{outputDir}/plugs/plug.ts` (the Plug runtime) and `plug.example.ts` (typed contract example).
|
|
19
|
+
|
|
20
|
+
## Creating a Plug
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { plug, bearer, apiKey, basic, custom } from "./plug";
|
|
24
|
+
|
|
25
|
+
export const stripePlug = plug({
|
|
26
|
+
name: "stripe",
|
|
27
|
+
baseUrl: "https://api.stripe.com/v1",
|
|
28
|
+
auth: bearer(process.env.STRIPE_KEY!),
|
|
29
|
+
retry: { attempts: 3, backoff: 1000 },
|
|
30
|
+
timeout: 30000,
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Auth Strategies
|
|
35
|
+
|
|
36
|
+
| Function | Usage |
|
|
37
|
+
|----------|-------|
|
|
38
|
+
| `bearer(token)` | `Authorization: Bearer <token>` — static string or async function |
|
|
39
|
+
| `basic(user, pass)` | `Authorization: Basic <base64>` |
|
|
40
|
+
| `apiKey(name, value)` | Custom header (default) or query param with `{ in: "query" }` |
|
|
41
|
+
| `custom(fn)` | Full control: `(headers) => { headers.set(...) }` |
|
|
42
|
+
| `tokenExchange(config)` | OAuth-style: exchanges variables for bearer token, caches, auto-refreshes on 401 |
|
|
43
|
+
|
|
44
|
+
### Token Exchange Example
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const auth = tokenExchange({
|
|
48
|
+
tokenUrl: "/oauth/token",
|
|
49
|
+
buildBody: (vars) => ({ grant_type: "client_credentials", client_id: vars.clientId, client_secret: vars.clientSecret }),
|
|
50
|
+
parseToken: (res) => ({ token: res.access_token, expiresIn: res.expires_in }),
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Vars (Runtime Variables)
|
|
55
|
+
|
|
56
|
+
For variables managed via the Hub UI instead of env vars:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
export const myPlug = plug({
|
|
60
|
+
baseUrl: "https://api.example.com",
|
|
61
|
+
auth: bearer(() => ""), // overridden by vars
|
|
62
|
+
vars: [
|
|
63
|
+
{ key: "apiKey", label: "API Key", type: "text", secret: true },
|
|
64
|
+
{ key: "orgId", label: "Org ID", type: "text", defaultValue: "org_demo" },
|
|
65
|
+
{ key: "_token", label: "", type: "text", hidden: true },
|
|
66
|
+
] as const,
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Vars are encrypted in the database via `KHOTAN_SECRET` and injected per-request. Hidden vars (prefixed `_`) are internal storage (cached tokens, etc). `defaultValue` seeds the database the first time the plug is initialized, and later Hub/CLI edits override that stored value.
|
|
71
|
+
|
|
72
|
+
## Typed Endpoints
|
|
73
|
+
|
|
74
|
+
Define Zod schemas inline on the plug:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { z } from "zod";
|
|
78
|
+
|
|
79
|
+
export const myPlug = plug({
|
|
80
|
+
baseUrl: "https://api.example.com",
|
|
81
|
+
auth: bearer(process.env.API_KEY!),
|
|
82
|
+
endpoints: {
|
|
83
|
+
listProducts: {
|
|
84
|
+
method: "GET",
|
|
85
|
+
path: "/products",
|
|
86
|
+
query: z.object({ page: z.number().optional(), limit: z.number().optional() }),
|
|
87
|
+
responses: { 200: z.object({ data: z.array(z.object({ id: z.string(), name: z.string() })), total: z.number() }) },
|
|
88
|
+
},
|
|
89
|
+
createProduct: {
|
|
90
|
+
method: "POST",
|
|
91
|
+
path: "/products",
|
|
92
|
+
body: z.object({ name: z.string(), price: z.number() }),
|
|
93
|
+
responses: { 201: z.object({ id: z.string() }) },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Endpoints power the plug debugger UI, `khotan plug --compare`, and typed clients.
|
|
100
|
+
|
|
101
|
+
## Typed Client (Contract Pattern)
|
|
102
|
+
|
|
103
|
+
For separate contract definition + type-safe calls:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { defineContract, createPlugClient } from "khotan-data/plug";
|
|
107
|
+
|
|
108
|
+
const contract = defineContract({
|
|
109
|
+
listProducts: {
|
|
110
|
+
method: "GET",
|
|
111
|
+
path: "/products",
|
|
112
|
+
query: z.object({ page: z.number().optional() }),
|
|
113
|
+
responses: { 200: z.object({ data: z.array(ProductSchema), total: z.number() }) },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const client = createPlugClient(contract, myPlug);
|
|
118
|
+
const result = await client.listProducts({ query: { page: 1 } });
|
|
119
|
+
// result.status — 200
|
|
120
|
+
// result.body.data — typed as Product[]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Hooks
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const myPlug = plug({
|
|
127
|
+
// ...
|
|
128
|
+
hooks: {
|
|
129
|
+
beforeRequest: async (ctx) => {
|
|
130
|
+
// ctx.vars, ctx.setVars, ctx.headers, ctx.url
|
|
131
|
+
},
|
|
132
|
+
afterResponse: async (response, ctx) => {
|
|
133
|
+
// inspect/transform response
|
|
134
|
+
},
|
|
135
|
+
onUnauthorized: async (ctx) => {
|
|
136
|
+
// refresh token, update vars
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Registering in Factory
|
|
143
|
+
|
|
144
|
+
In `{outputDir}/khotan.ts`:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
plugs: [
|
|
148
|
+
{
|
|
149
|
+
name: "stripe",
|
|
150
|
+
plug: stripePlug,
|
|
151
|
+
flows: [
|
|
152
|
+
{ name: "charges-inflow", type: "inflow", schedule: "0 * * * *", resource: "orders" },
|
|
153
|
+
],
|
|
154
|
+
// Optional: wires, catches, passes
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Making Requests
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Direct
|
|
163
|
+
const products = await myPlug.get("/products", { params: { limit: "10" } });
|
|
164
|
+
const created = await myPlug.post("/products", { body: { name: "Widget" } });
|
|
165
|
+
|
|
166
|
+
// With vars (factory injects these automatically in wire/debug contexts)
|
|
167
|
+
const data = await myPlug.get("/items", { vars: { apiKey: "..." } });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Debugging
|
|
171
|
+
|
|
172
|
+
Use `khotan plug` to test plugs against the running dev server. `khotan probe` remains as a legacy alias:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npx khotan plug myPlug --info # See endpoints
|
|
176
|
+
npx khotan plug myPlug GET /products # Fire request
|
|
177
|
+
npx khotan plug myPlug --endpoint listProducts --compare # Check schema
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Set `KHOTAN_DEBUG=1` for verbose `[khotan:auth]` and `[khotan:request]` console logs.
|
|
181
|
+
|
|
182
|
+
## Managing Vars
|
|
183
|
+
|
|
184
|
+
Use the CLI to inspect and update stored plug variables:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npx khotan plug vars --list
|
|
188
|
+
npx khotan plug vars myPlug
|
|
189
|
+
npx khotan plug vars myPlug set --json '{"apiKey":"secret","orgId":"org_live"}'
|
|
190
|
+
npx khotan plug vars myPlug clear
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Variable reads mask `secret` fields automatically. Hub and CLI both talk to the same `/api/khotan/variables/*` routes.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: khotan-setup
|
|
3
|
+
description: >
|
|
4
|
+
Set up khotan-data in a Next.js + Drizzle + Postgres project. Use when
|
|
5
|
+
initializing khotan in a new project, adding the database schema,
|
|
6
|
+
configuring the factory, or troubleshooting missing setup steps.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Set up khotan-data in a Next.js + Drizzle + Postgres project. Use when initializing khotan in a new project, adding the database schema, configuring the factory, or troubleshooting missing setup steps.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install khotan-data
|
|
15
|
+
npx khotan init
|
|
16
|
+
npx khotan add schema --yes
|
|
17
|
+
npx khotan migrate
|
|
18
|
+
npx khotan add plug --yes
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What Init Creates
|
|
22
|
+
|
|
23
|
+
`npx khotan init` scaffolds three files (never overwrites existing):
|
|
24
|
+
|
|
25
|
+
| File | Purpose |
|
|
26
|
+
|------|---------|
|
|
27
|
+
| `khotan.config.ts` | CLI config — sets `outputDir` (default: `src/khotan` or `khotan`) |
|
|
28
|
+
| `{outputDir}/khotan.ts` | Factory config — register plugs, resources, adapter |
|
|
29
|
+
| `src/app/api/khotan/[...all]/route.ts` | Catch-all API route |
|
|
30
|
+
|
|
31
|
+
Use `npx khotan init --full` for greenfield projects — also installs drizzle-orm, postgres, drizzle-kit, and shadcn.
|
|
32
|
+
|
|
33
|
+
## Factory Config Pattern
|
|
34
|
+
|
|
35
|
+
Edit `{outputDir}/khotan.ts` after init:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { khotan, drizzleAdapter } from "khotan-data/factory";
|
|
39
|
+
import { db } from "@/db";
|
|
40
|
+
import { stripeChargesInflow } from "./flows/stripe-charges";
|
|
41
|
+
|
|
42
|
+
const khotanData = khotan({
|
|
43
|
+
adapter: drizzleAdapter(db),
|
|
44
|
+
resources: [
|
|
45
|
+
{ name: "products", connectField: "sku" },
|
|
46
|
+
{ name: "orders", connectField: "order_number" },
|
|
47
|
+
],
|
|
48
|
+
plugs: [
|
|
49
|
+
{
|
|
50
|
+
name: "stripe",
|
|
51
|
+
plug: stripePlug,
|
|
52
|
+
flows: [
|
|
53
|
+
stripeChargesInflow,
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export default khotanData;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The factory auto-upserts plugs, flows, and resources to the database on first API request.
|
|
63
|
+
|
|
64
|
+
## Route Handler
|
|
65
|
+
|
|
66
|
+
The catch-all route delegates all HTTP methods to the factory:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { toNextJsHandler } from "khotan-data/factory";
|
|
70
|
+
import khotanData from "@/lib/khotan/khotan";
|
|
71
|
+
|
|
72
|
+
export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(khotanData.handler);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Database Setup
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx khotan add schema --yes # Scaffolds Drizzle table definitions
|
|
79
|
+
npx khotan migrate # Generates + applies migrations (needs DATABASE_URL)
|
|
80
|
+
npx khotan migrate --push # Or push directly without migration files
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Tables created: `khotan_plugs`, `khotan_resources`, `khotan_flows`, `khotan_wires`, `khotan_runs`, `khotan_mappings`.
|
|
84
|
+
|
|
85
|
+
The schema command auto-detects your Drizzle schema directory, updates `drizzle.config.ts` glob pattern, and adds the barrel re-export.
|
|
86
|
+
|
|
87
|
+
## Environment Variables
|
|
88
|
+
|
|
89
|
+
| Variable | Required | Purpose |
|
|
90
|
+
|----------|----------|---------|
|
|
91
|
+
| `DATABASE_URL` | Yes | Postgres connection (used by Drizzle) |
|
|
92
|
+
| `KHOTAN_SECRET` | For variables | AES-256-GCM key for encrypting plug vars |
|
|
93
|
+
| `KHOTAN_DEBUG` | For debugging | Enables `/debug/*` routes and the `plug` CLI (`probe` alias) |
|
|
94
|
+
| `KHOTAN_WEBHOOK_URL` | For webhooks | Public URL for wire callbacks |
|
|
95
|
+
|
|
96
|
+
## Next.js Config
|
|
97
|
+
|
|
98
|
+
Add to `next.config.ts` if using local/tarball install:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const nextConfig = {
|
|
102
|
+
serverExternalPackages: ["khotan-data"],
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Verify Setup
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
curl http://localhost:3000/api/khotan/plugs # Should list registered plugs
|
|
110
|
+
curl http://localhost:3000/api/khotan/flows # Should list flows
|
|
111
|
+
curl http://localhost:3000/api/khotan/resources # Should list resources
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Troubleshooting
|
|
115
|
+
|
|
116
|
+
- **Empty plug list**: Factory upserts on first request — hit any endpoint first, then check `/plugs`
|
|
117
|
+
- **"Cannot find module khotan-data"**: Add to `serverExternalPackages` in next.config.ts
|
|
118
|
+
- **Migration fails**: Ensure `DATABASE_URL` is set and Postgres is reachable
|
|
119
|
+
- **Init won't overwrite**: By design — delete the file manually if you need to re-scaffold
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: khotan-webhook
|
|
3
|
+
description: >
|
|
4
|
+
Set up webhook subscriptions and event processing with khotan Wires,
|
|
5
|
+
Catch, and Pass. Use when receiving webhooks from external services,
|
|
6
|
+
registering callback URLs, processing incoming events durably, or
|
|
7
|
+
forwarding events between services.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Set up webhook subscriptions and event processing with khotan Wires, Catch, and Pass. Use when receiving webhooks from external services, registering callback URLs, processing incoming events durably, or forwarding events between services.
|
|
11
|
+
|
|
12
|
+
## Wire (Webhook Subscriptions)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx khotan add wire --yes
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Scaffolds `{outputDir}/wires/wire.ts` (the builder) and `src/components/khotan/wire.tsx` (UI panel).
|
|
19
|
+
|
|
20
|
+
### Creating a Wire
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { wire } from "./wire";
|
|
24
|
+
|
|
25
|
+
export const stripeWire = wire({
|
|
26
|
+
events: ["invoice.paid", "charge.succeeded"],
|
|
27
|
+
|
|
28
|
+
async onSubscribe(ctx) {
|
|
29
|
+
const res = await ctx.plug.post<{ id: string; secret: string }>(
|
|
30
|
+
"/webhook_endpoints",
|
|
31
|
+
{
|
|
32
|
+
body: {
|
|
33
|
+
url: ctx.callbackUrl,
|
|
34
|
+
enabled_events: ctx.events,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
await ctx.setWireVars({ webhookSecret: res.secret });
|
|
39
|
+
return { remoteId: res.id };
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async onUnsubscribe(ctx) {
|
|
43
|
+
await ctx.plug.delete(`/webhook_endpoints/${ctx.remoteId}`);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async onVerify(ctx) {
|
|
47
|
+
const signature = ctx.headers["stripe-signature"];
|
|
48
|
+
// Verify HMAC using ctx.wireVars.webhookSecret and ctx.body (raw text)
|
|
49
|
+
return isValidSignature(signature, ctx.body, ctx.wireVars.webhookSecret);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Hook Contexts
|
|
55
|
+
|
|
56
|
+
**onSubscribe** receives:
|
|
57
|
+
- `ctx.plug` — Plug with vars/auth auto-injected (BoundPlug)
|
|
58
|
+
- `ctx.callbackUrl` — The URL to register with the external service
|
|
59
|
+
- `ctx.events` — Event types to subscribe to
|
|
60
|
+
- `ctx.wireVars` / `ctx.setWireVars()` — Persist wire-specific data (secrets, tokens)
|
|
61
|
+
- Must return `{ remoteId: string }`
|
|
62
|
+
|
|
63
|
+
**onUnsubscribe** receives:
|
|
64
|
+
- `ctx.plug` — BoundPlug
|
|
65
|
+
- `ctx.remoteId` — The ID returned from onSubscribe
|
|
66
|
+
- `ctx.wireVars` / `ctx.setWireVars()`
|
|
67
|
+
|
|
68
|
+
**onVerify** receives:
|
|
69
|
+
- `ctx.headers` — Incoming request headers
|
|
70
|
+
- `ctx.body` — Raw request body (for signature verification)
|
|
71
|
+
- `ctx.wireVars` — Wire-specific vars
|
|
72
|
+
- Must return `boolean`
|
|
73
|
+
|
|
74
|
+
### Registering Wires
|
|
75
|
+
|
|
76
|
+
In `{outputDir}/khotan.ts`:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { stripeWire } from "./wires/stripe-wire";
|
|
80
|
+
|
|
81
|
+
plugs: [
|
|
82
|
+
{
|
|
83
|
+
name: "stripe",
|
|
84
|
+
plug: stripePlug,
|
|
85
|
+
wires: [stripeWire],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Programmatic Wire API
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const w = khotanData.wire("stripe");
|
|
94
|
+
await w.create("https://your-domain.com/api/khotan/webhook/stripe");
|
|
95
|
+
await w.get(); // Get wire status
|
|
96
|
+
await w.delete(wireId); // Disconnect
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Webhook Callback URL
|
|
100
|
+
|
|
101
|
+
Wires register callbacks at: `{webhookUrl}/api/khotan/webhook/{plugName}`
|
|
102
|
+
|
|
103
|
+
For local dev, use ngrok or similar tunnel and set `KHOTAN_WEBHOOK_URL`.
|
|
104
|
+
|
|
105
|
+
## Catch (Durable Event Processing)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx khotan add catch --yes
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Process webhook events durably via Vercel Workflow:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { catchEvent } from "./webhooks/catch";
|
|
115
|
+
|
|
116
|
+
const processInvoice = catchEvent(async (ctx) => {
|
|
117
|
+
"use workflow";
|
|
118
|
+
|
|
119
|
+
async function persist() {
|
|
120
|
+
"use step";
|
|
121
|
+
// Write to database — retried on failure
|
|
122
|
+
await db.insert(invoices).values(ctx.event);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await persist();
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Register on the source plug:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
{ name: "stripe", plug: stripePlug, wires: [stripeWire], catches: [processInvoice] }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Pass (Event Forwarding)
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx khotan add pass --yes
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Forward webhook events to another service:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { pass } from "./webhooks/pass";
|
|
145
|
+
|
|
146
|
+
const forwardToSlack = pass({
|
|
147
|
+
to: "slack", // Destination plug name (must be registered)
|
|
148
|
+
workflow: async (ctx) => {
|
|
149
|
+
"use workflow";
|
|
150
|
+
// ctx.event — the incoming webhook payload
|
|
151
|
+
// ctx.destVars — destination plug variables
|
|
152
|
+
async function forward() {
|
|
153
|
+
"use step";
|
|
154
|
+
await ctx.destPlug.post("/messages", {
|
|
155
|
+
body: { text: `New event: ${ctx.event.type}` },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
await forward();
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Register on the source plug:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
{ name: "stripe", plug: stripePlug, wires: [stripeWire], passes: [forwardToSlack] }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Webhook Flow
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
External Service → POST /api/khotan/webhook/:plugName
|
|
173
|
+
→ onVerify (signature check)
|
|
174
|
+
→ Parse event type
|
|
175
|
+
→ Start catch workflows (durable processing)
|
|
176
|
+
→ Start pass workflows (event forwarding)
|
|
177
|
+
→ Return { received: true }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Dependencies
|
|
181
|
+
|
|
182
|
+
- **Wire**: Requires `plug` and `schema` components
|
|
183
|
+
- **Catch**: Requires `wire`; needs `workflow` package for Vercel Workflow
|
|
184
|
+
- **Pass**: Requires `wire` and `plug`; needs `workflow` package
|
|
185
|
+
|
|
186
|
+
## Hub Integration
|
|
187
|
+
|
|
188
|
+
The WirePanel in the Hub UI lets users connect/disconnect webhooks from the browser. It calls `POST /api/khotan/wires/:plugName` with the callback URL.
|
|
189
|
+
|
|
190
|
+
## Debugging Webhooks
|
|
191
|
+
|
|
192
|
+
1. Check wire status: `GET /api/khotan/wires/:plugName`
|
|
193
|
+
2. Verify `onVerify` logic: check wire vars contain the signing secret
|
|
194
|
+
3. Check factory logs: `KHOTAN_DEBUG=1` enables `[khotan:wire]` log lines
|
|
195
|
+
4. 401 on webhook receive = `onVerify` returning false
|
|
196
|
+
5. 500 on webhook = missing `workflow` package or catch/pass misconfigured
|