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.
Files changed (55) hide show
  1. package/AGENTS.md +54 -0
  2. package/README.md +117 -1
  3. package/dist/cli.js +2869 -0
  4. package/dist/factory.cjs +3303 -0
  5. package/dist/factory.cjs.map +1 -0
  6. package/dist/factory.d.cts +662 -0
  7. package/dist/factory.d.ts +662 -0
  8. package/dist/factory.js +3292 -0
  9. package/dist/factory.js.map +1 -0
  10. package/dist/plug-client.cjs +99 -0
  11. package/dist/plug-client.cjs.map +1 -0
  12. package/dist/plug-client.d.cts +71 -0
  13. package/dist/plug-client.d.ts +71 -0
  14. package/dist/plug-client.js +96 -0
  15. package/dist/plug-client.js.map +1 -0
  16. package/dist/templates/agent-skill.md +73 -0
  17. package/dist/templates/agents.md +41 -0
  18. package/dist/templates/cache.example.ts +11 -0
  19. package/dist/templates/cache.ts +58 -0
  20. package/dist/templates/catch.example.ts +36 -0
  21. package/dist/templates/catch.ts +119 -0
  22. package/dist/templates/config-page.tsx +20 -0
  23. package/dist/templates/debug-index-page.tsx +101 -0
  24. package/dist/templates/debug-page.tsx +48 -0
  25. package/dist/templates/graph-page.tsx +11 -0
  26. package/dist/templates/hub.tsx +450 -0
  27. package/dist/templates/inflow.example.ts +61 -0
  28. package/dist/templates/inflow.ts +98 -0
  29. package/dist/templates/khotan-config.ts +49 -0
  30. package/dist/templates/khotan-route.ts +13 -0
  31. package/dist/templates/logs-page.tsx +9 -0
  32. package/dist/templates/logs.tsx +20 -0
  33. package/dist/templates/mapping-browser.tsx +761 -0
  34. package/dist/templates/mappings-page.tsx +9 -0
  35. package/dist/templates/outflow.example.ts +52 -0
  36. package/dist/templates/outflow.ts +90 -0
  37. package/dist/templates/pass.example.ts +51 -0
  38. package/dist/templates/pass.ts +134 -0
  39. package/dist/templates/plug-debugger.tsx +1185 -0
  40. package/dist/templates/plug.example.ts +93 -0
  41. package/dist/templates/plug.ts +806 -0
  42. package/dist/templates/relay.example.ts +71 -0
  43. package/dist/templates/relay.ts +104 -0
  44. package/dist/templates/runs-table.tsx +592 -0
  45. package/dist/templates/schema.ts +505 -0
  46. package/dist/templates/skill-dashboard.md +144 -0
  47. package/dist/templates/skill-plug.md +216 -0
  48. package/dist/templates/skill-setup.md +161 -0
  49. package/dist/templates/skill-webhook.md +196 -0
  50. package/dist/templates/topology-canvas.tsx +1406 -0
  51. package/dist/templates/var-panel.tsx +276 -0
  52. package/dist/templates/webhook-events-table.tsx +241 -0
  53. package/dist/templates/wire-panel.tsx +216 -0
  54. package/dist/templates/wire.ts +155 -0
  55. 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