khotan-data 0.0.1 → 0.1.1
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 +117 -1
- package/dist/cli.js +2869 -0
- package/dist/factory.cjs +3303 -0
- package/dist/factory.cjs.map +1 -0
- package/dist/factory.d.cts +662 -0
- package/dist/factory.d.ts +662 -0
- package/dist/factory.js +3292 -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/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.example.ts +36 -0
- package/dist/templates/catch.ts +119 -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 +98 -0
- package/dist/templates/khotan-config.ts +49 -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/mapping-browser.tsx +761 -0
- package/dist/templates/mappings-page.tsx +9 -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 +134 -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 +71 -0
- package/dist/templates/relay.ts +104 -0
- package/dist/templates/runs-table.tsx +592 -0
- package/dist/templates/schema.ts +505 -0
- package/dist/templates/skill-dashboard.md +144 -0
- package/dist/templates/skill-plug.md +216 -0
- package/dist/templates/skill-setup.md +161 -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,216 @@
|
|
|
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
|
+
## Preferred Pattern
|
|
102
|
+
|
|
103
|
+
Keep each integration in a single app-owned plug file when possible:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { z } from "zod";
|
|
107
|
+
import { plug, basic } from "./plug";
|
|
108
|
+
|
|
109
|
+
const ProductSchema = z.object({
|
|
110
|
+
id: z.string(),
|
|
111
|
+
sku: z.string(),
|
|
112
|
+
name: z.string(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export type Product = z.infer<typeof ProductSchema>;
|
|
116
|
+
|
|
117
|
+
export const myPlug = plug({
|
|
118
|
+
name: "my-service",
|
|
119
|
+
baseUrl: "https://api.example.com",
|
|
120
|
+
auth: basic(process.env.API_USER!, process.env.API_KEY!),
|
|
121
|
+
endpoints: {
|
|
122
|
+
listProducts: {
|
|
123
|
+
method: "GET",
|
|
124
|
+
path: "/products",
|
|
125
|
+
query: z.object({ page: z.number().optional(), limit: z.number().optional() }),
|
|
126
|
+
responses: { 200: z.array(ProductSchema) },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This keeps the runtime plug, debugger metadata, `khotan plug --compare`, and any exported types in one place.
|
|
133
|
+
|
|
134
|
+
## Hooks
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const myPlug = plug({
|
|
138
|
+
// ...
|
|
139
|
+
hooks: {
|
|
140
|
+
beforeRequest: async (ctx) => {
|
|
141
|
+
// ctx.vars, ctx.setVars, ctx.headers, ctx.url
|
|
142
|
+
},
|
|
143
|
+
afterResponse: async (response, ctx) => {
|
|
144
|
+
// inspect/transform response
|
|
145
|
+
},
|
|
146
|
+
onUnauthorized: async (ctx) => {
|
|
147
|
+
// refresh token, update vars
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Registering in Factory
|
|
154
|
+
|
|
155
|
+
In `{outputDir}/khotan.ts`:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
plugs: [
|
|
159
|
+
{
|
|
160
|
+
name: "stripe",
|
|
161
|
+
plug: stripePlug,
|
|
162
|
+
flows: [
|
|
163
|
+
{ name: "charges-inflow", type: "inflow", schedule: "0 * * * *", resource: "orders" },
|
|
164
|
+
],
|
|
165
|
+
// Optional: wires, catches, passes
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Making Requests
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// Direct
|
|
174
|
+
const products = await myPlug.get("/products", { params: { limit: "10" } });
|
|
175
|
+
const created = await myPlug.post("/products", { body: { name: "Widget" } });
|
|
176
|
+
|
|
177
|
+
// With vars (factory injects these automatically in wire/debug contexts)
|
|
178
|
+
const data = await myPlug.get("/items", { vars: { apiKey: "..." } });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Debugging
|
|
182
|
+
|
|
183
|
+
Use `khotan plug` to test plugs against the running dev server. `khotan probe` remains as a legacy alias:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npx khotan plug myPlug --info # See endpoints
|
|
187
|
+
npx khotan plug myPlug GET /products # Fire request
|
|
188
|
+
npx khotan plug myPlug --endpoint listProducts --compare # Check schema
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Set `KHOTAN_DEBUG=1` for verbose `[khotan:auth]` and `[khotan:request]` console logs.
|
|
192
|
+
|
|
193
|
+
### Recommended Plug Workflow
|
|
194
|
+
|
|
195
|
+
1. Create the plug file and auth/hook setup.
|
|
196
|
+
2. Add a small set of typed endpoints directly on the plug (`listProducts`, `getProduct`, etc).
|
|
197
|
+
3. Run the app with `KHOTAN_DEBUG=1`.
|
|
198
|
+
4. Use `npx khotan plug myPlug --info` to confirm the endpoints are visible to the debugger.
|
|
199
|
+
5. Use `npx khotan plug myPlug --endpoint listProducts --compare` against the live API.
|
|
200
|
+
6. Tighten schemas until the compare output matches the real payload shape you care about.
|
|
201
|
+
7. Only then build inflows, relays, outflows, or webhook handlers on top of those endpoints.
|
|
202
|
+
|
|
203
|
+
The package does not paginate or delta-sync for you automatically inside user flows. Your app code decides which typed endpoints to call, what page size to use, when to stop, and how to implement full, test, partial, backfill, reconcile, or delta runs.
|
|
204
|
+
|
|
205
|
+
## Managing Vars
|
|
206
|
+
|
|
207
|
+
Use the CLI to inspect and update stored plug variables:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npx khotan plug vars --list
|
|
211
|
+
npx khotan plug vars myPlug
|
|
212
|
+
npx khotan plug vars myPlug set --json '{"apiKey":"secret","orgId":"org_live"}'
|
|
213
|
+
npx khotan plug vars myPlug clear
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Variable reads mask `secret` fields automatically. Hub and CLI both talk to the same `/api/khotan/variables/*` routes.
|
|
@@ -0,0 +1,161 @@
|
|
|
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", mapping: { connectField: "sku" } },
|
|
46
|
+
{ name: "orders", mapping: { 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
|
+
| `CRON_SECRET` | For production cron | Protects the built-in `/api/khotan/cron` dispatcher route |
|
|
96
|
+
|
|
97
|
+
## Next.js Config
|
|
98
|
+
|
|
99
|
+
Add to `next.config.ts` if using local/tarball install:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const nextConfig = {
|
|
103
|
+
serverExternalPackages: ["khotan-data"],
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Verify Setup
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
curl http://localhost:3000/api/khotan/plugs # Should list registered plugs
|
|
111
|
+
curl http://localhost:3000/api/khotan/flows # Should list flows
|
|
112
|
+
curl http://localhost:3000/api/khotan/resources # Should list resources
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Scheduled Flows On Vercel
|
|
116
|
+
|
|
117
|
+
Khotan flow `schedule` values are runtime source-of-truth metadata. On Vercel, prefer a single dispatcher CRON instead of defining one platform CRON per flow.
|
|
118
|
+
|
|
119
|
+
Add one entry to `vercel.json`:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"crons": [
|
|
124
|
+
{ "path": "/api/khotan/cron", "schedule": "* * * * *" }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then define schedules only on your flows in `{outputDir}/khotan.ts`:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
{
|
|
133
|
+
name: "products-inflow",
|
|
134
|
+
type: "inflow",
|
|
135
|
+
schedule: "0 * * * *",
|
|
136
|
+
resource: "products",
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The dispatcher route evaluates which flows are due on each tick and starts them through the normal run-tracking path. If `CRON_SECRET` is set, Vercel should call the route with `Authorization: Bearer <CRON_SECRET>`.
|
|
141
|
+
|
|
142
|
+
## Typical Build Order
|
|
143
|
+
|
|
144
|
+
After init and schema setup, the usual path to a working sync is:
|
|
145
|
+
|
|
146
|
+
1. Add or author a plug file for the external service.
|
|
147
|
+
2. Define a few typed endpoints directly on the plug with Zod response schemas.
|
|
148
|
+
3. Start the app with `KHOTAN_DEBUG=1`.
|
|
149
|
+
4. Verify the plug is visible with `npx khotan plug --list` and `npx khotan plug myPlug --info`.
|
|
150
|
+
5. Hit live endpoints with `npx khotan plug myPlug --endpoint listProducts --compare` until the schemas match the real API shape you intend to use.
|
|
151
|
+
6. Register the plug in `{outputDir}/khotan.ts` with resources and flows.
|
|
152
|
+
7. Only after endpoint verification, build inflows, relays, outflows, or webhook handlers on top of those live-checked endpoints.
|
|
153
|
+
|
|
154
|
+
This keeps sync logic grounded in real API payloads before you write pagination, mapping, or transformation code.
|
|
155
|
+
|
|
156
|
+
## Troubleshooting
|
|
157
|
+
|
|
158
|
+
- **Empty plug list**: Factory upserts on first request — hit any endpoint first, then check `/plugs`
|
|
159
|
+
- **"Cannot find module khotan-data"**: Add to `serverExternalPackages` in next.config.ts
|
|
160
|
+
- **Migration fails**: Ensure `DATABASE_URL` is set and Postgres is reachable
|
|
161
|
+
- **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
|