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,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/plug-client.ts
|
|
4
|
+
function defineContract(contract) {
|
|
5
|
+
return contract;
|
|
6
|
+
}
|
|
7
|
+
function isPlugError(err) {
|
|
8
|
+
return err instanceof Error && "status" in err && typeof err.status === "number" && "body" in err;
|
|
9
|
+
}
|
|
10
|
+
function parseBody(body) {
|
|
11
|
+
if (typeof body === "string") {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(body);
|
|
14
|
+
} catch {
|
|
15
|
+
return body;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return body;
|
|
19
|
+
}
|
|
20
|
+
function interpolatePath(path, params) {
|
|
21
|
+
return path.replace(/:([^/]+)/g, (_, key) => {
|
|
22
|
+
const value = params[key];
|
|
23
|
+
if (value === void 0) {
|
|
24
|
+
throw new Error(`Missing path parameter: "${key}"`);
|
|
25
|
+
}
|
|
26
|
+
return encodeURIComponent(value);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function createPlugClient(contract, plug, options) {
|
|
30
|
+
const globalValidateResponse = options?.validateResponse ?? true;
|
|
31
|
+
function buildClient(router) {
|
|
32
|
+
const client = {};
|
|
33
|
+
for (const [key, value] of Object.entries(router)) {
|
|
34
|
+
const route = value;
|
|
35
|
+
if ("method" in route && "path" in route) {
|
|
36
|
+
client[key] = async (input) => {
|
|
37
|
+
const def = route;
|
|
38
|
+
const params = input?.["params"] ?? {};
|
|
39
|
+
const query = input?.["query"];
|
|
40
|
+
const body = input?.["body"];
|
|
41
|
+
const headers = input?.["headers"];
|
|
42
|
+
const shouldValidateResponse = input?.["validateResponse"] ?? globalValidateResponse;
|
|
43
|
+
if (def.pathParams) {
|
|
44
|
+
def.pathParams.parse(params);
|
|
45
|
+
}
|
|
46
|
+
if (def.query && query !== void 0) {
|
|
47
|
+
def.query.parse(query);
|
|
48
|
+
}
|
|
49
|
+
if (def.body && body !== void 0) {
|
|
50
|
+
def.body.parse(body);
|
|
51
|
+
}
|
|
52
|
+
const interpolatedPath = interpolatePath(def.path, params);
|
|
53
|
+
const responses = def.responses;
|
|
54
|
+
try {
|
|
55
|
+
const responseBody = await plug.request(
|
|
56
|
+
def.method,
|
|
57
|
+
interpolatedPath,
|
|
58
|
+
{
|
|
59
|
+
...query !== void 0 ? { params: query } : {},
|
|
60
|
+
...body !== void 0 ? { body } : {},
|
|
61
|
+
...headers !== void 0 ? { headers } : {}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
const status = 200;
|
|
65
|
+
if (!responses[status]) {
|
|
66
|
+
return { status, body: responseBody };
|
|
67
|
+
}
|
|
68
|
+
if (shouldValidateResponse) {
|
|
69
|
+
const validated = responses[status].parse(responseBody);
|
|
70
|
+
return { status, body: validated };
|
|
71
|
+
}
|
|
72
|
+
return { status, body: responseBody };
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (!isPlugError(err)) throw err;
|
|
75
|
+
const status = err.status;
|
|
76
|
+
if (responses[status]) {
|
|
77
|
+
const parsed = parseBody(err.body);
|
|
78
|
+
if (shouldValidateResponse) {
|
|
79
|
+
const validated = responses[status].parse(parsed);
|
|
80
|
+
return { status, body: validated };
|
|
81
|
+
}
|
|
82
|
+
return { status, body: parsed };
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
} else {
|
|
88
|
+
client[key] = buildClient(route);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return client;
|
|
92
|
+
}
|
|
93
|
+
return buildClient(contract);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
exports.createPlugClient = createPlugClient;
|
|
97
|
+
exports.defineContract = defineContract;
|
|
98
|
+
//# sourceMappingURL=plug-client.cjs.map
|
|
99
|
+
//# sourceMappingURL=plug-client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plug-client.ts"],"names":[],"mappings":";;;AAkFO,SAAS,eAA+C,QAAA,EAAgB;AAC7E,EAAA,OAAO,QAAA;AACT;AAoCA,SAAS,YAAY,GAAA,EAAoC;AACvD,EAAA,OACE,GAAA,YAAe,SACf,QAAA,IAAY,GAAA,IACZ,OAAQ,GAAA,CAAiC,MAAA,KAAW,YACpD,MAAA,IAAU,GAAA;AAEd;AAEA,SAAS,UAAU,IAAA,EAAwB;AACzC,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,IAAA,IAAI;AACF,MAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,IACxB,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAMA,SAAS,eAAA,CAAgB,MAAc,MAAA,EAAwC;AAC7E,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,CAAC,GAAG,GAAA,KAAgB;AACnD,IAAA,MAAM,KAAA,GAAQ,OAAO,GAAG,CAAA;AACxB,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,GAAG,CAAA,CAAA,CAAG,CAAA;AAAA,IACpD;AACA,IAAA,OAAO,mBAAmB,KAAK,CAAA;AAAA,EACjC,CAAC,CAAA;AACH;AAMO,SAAS,gBAAA,CACd,QAAA,EACA,IAAA,EACA,OAAA,EACe;AACf,EAAA,MAAM,sBAAA,GAAyB,SAAS,gBAAA,IAAoB,IAAA;AAE5D,EAAA,SAAS,YAAY,MAAA,EAAiD;AACpE,IAAA,MAAM,SAAkC,EAAC;AAEzC,IAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AACjD,MAAA,MAAM,KAAA,GAAQ,KAAA;AAEd,MAAA,IAAI,QAAA,IAAY,KAAA,IAAS,MAAA,IAAU,KAAA,EAAO;AACxC,QAAA,MAAA,CAAO,GAAG,CAAA,GAAI,OAAO,KAAA,KAAoC;AACvD,UAAA,MAAM,GAAA,GAAM,KAAA;AACZ,UAAA,MAAM,MAAA,GAAU,KAAA,GAAQ,QAAQ,CAAA,IAAK,EAAC;AACtC,UAAA,MAAM,KAAA,GAAQ,QAAQ,OAAO,CAAA;AAC7B,UAAA,MAAM,IAAA,GAAO,QAAQ,MAAM,CAAA;AAC3B,UAAA,MAAM,OAAA,GAAU,QAAQ,SAAS,CAAA;AAGjC,UAAA,MAAM,sBAAA,GACH,KAAA,GAAQ,kBAAkB,CAAA,IAC3B,sBAAA;AAEF,UAAA,IAAI,IAAI,UAAA,EAAY;AAClB,YAAA,GAAA,CAAI,UAAA,CAAW,MAAM,MAAM,CAAA;AAAA,UAC7B;AACA,UAAA,IAAI,GAAA,CAAI,KAAA,IAAS,KAAA,KAAU,MAAA,EAAW;AACpC,YAAA,GAAA,CAAI,KAAA,CAAM,MAAM,KAAK,CAAA;AAAA,UACvB;AACA,UAAA,IAAI,GAAA,CAAI,IAAA,IAAQ,IAAA,KAAS,MAAA,EAAW;AAClC,YAAA,GAAA,CAAI,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,UACrB;AAEA,UAAA,MAAM,gBAAA,GAAmB,eAAA,CAAgB,GAAA,CAAI,IAAA,EAAM,MAAM,CAAA;AACzD,UAAA,MAAM,YAAY,GAAA,CAAI,SAAA;AAEtB,UAAA,IAAI;AACF,YAAA,MAAM,YAAA,GAAe,MAAM,IAAA,CAAK,OAAA;AAAA,cAC9B,GAAA,CAAI,MAAA;AAAA,cACJ,gBAAA;AAAA,cACA;AAAA,gBACE,GAAI,KAAA,KAAU,KAAA,CAAA,GAAY,EAAE,MAAA,EAAQ,KAAA,KAAU,EAAC;AAAA,gBAC/C,GAAI,IAAA,KAAS,KAAA,CAAA,GAAY,EAAE,IAAA,KAAS,EAAC;AAAA,gBACrC,GAAI,OAAA,KAAY,KAAA,CAAA,GAAY,EAAE,OAAA,KAAY;AAAC;AAC7C,aACF;AAEA,YAAA,MAAM,MAAA,GAAS,GAAA;AACf,YAAA,IAAI,CAAC,SAAA,CAAU,MAAM,CAAA,EAAG;AACtB,cAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,YAAA,EAAa;AAAA,YACtC;AAEA,YAAA,IAAI,sBAAA,EAAwB;AAC1B,cAAA,MAAM,SAAA,GAAY,SAAA,CAAU,MAAM,CAAA,CAAE,MAAM,YAAY,CAAA;AACtD,cAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAU;AAAA,YACnC;AAEA,YAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,YAAA,EAAa;AAAA,UACtC,SAAS,GAAA,EAAK;AACZ,YAAA,IAAI,CAAC,WAAA,CAAY,GAAG,CAAA,EAAG,MAAM,GAAA;AAE7B,YAAA,MAAM,SAAS,GAAA,CAAI,MAAA;AACnB,YAAA,IAAI,SAAA,CAAU,MAAM,CAAA,EAAG;AACrB,cAAA,MAAM,MAAA,GAAS,SAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AACjC,cAAA,IAAI,sBAAA,EAAwB;AAC1B,gBAAA,MAAM,SAAA,GAAY,SAAA,CAAU,MAAM,CAAA,CAAE,MAAM,MAAM,CAAA;AAChD,gBAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAU;AAAA,cACnC;AACA,cAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,YAChC;AAEA,YAAA,MAAM,GAAA;AAAA,UACR;AAAA,QACF,CAAA;AAAA,MACF,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,GAAG,CAAA,GAAI,WAAA,CAAY,KAAK,CAAA;AAAA,MACjC;AAAA,IACF;AAEA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,YAAY,QAAQ,CAAA;AAC7B","file":"plug-client.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// Schema interface — works with zod v3, v4, or any validator with .parse()\n// ---------------------------------------------------------------------------\n\nexport interface Schema<TOutput = unknown, TInput = unknown> {\n parse(data: unknown): TOutput;\n _input?: TInput;\n _output?: TOutput;\n}\n\n// ---------------------------------------------------------------------------\n// Contract types\n// ---------------------------------------------------------------------------\n\nexport interface RouteDefinition {\n method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n path: string;\n query?: Schema;\n body?: Schema;\n pathParams?: Schema;\n responses: Record<number, Schema>;\n}\n\nexport interface ContractRouter {\n [key: string]: RouteDefinition | ContractRouter;\n}\n\n// ---------------------------------------------------------------------------\n// Type-level utilities\n// ---------------------------------------------------------------------------\n\ntype SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;\ntype SchemaOutput<T> = T extends Schema<infer O> ? O : never;\n\ntype PathParams<T extends string> =\n T extends `${string}:${infer Param}/${infer Rest}`\n ? Record<Param | keyof PathParams<Rest>, string>\n : T extends `${string}:${infer Param}`\n ? Record<Param, string>\n : never;\n\ntype HasKeys<T> = keyof T extends never ? false : true;\n\ntype EndpointInput<TRoute extends RouteDefinition> = (HasKeys<\n PathParams<TRoute[\"path\"]>\n> extends true\n ? { params: PathParams<TRoute[\"path\"]> }\n : { params?: never }) &\n (TRoute extends { query: Schema }\n ? { query: SchemaInput<TRoute[\"query\"]> }\n : { query?: never }) &\n (TRoute extends { body: Schema }\n ? { body: SchemaInput<TRoute[\"body\"]> }\n : { body?: never }) & {\n headers?: Record<string, string>;\n validateResponse?: boolean;\n };\n\ntype ResponseSchemas<TRoute extends RouteDefinition> = {\n [K in keyof TRoute[\"responses\"]]: K extends number\n ? { status: K; body: SchemaOutput<TRoute[\"responses\"][K]> }\n : never;\n}[keyof TRoute[\"responses\"]];\n\ntype PlugClient<T extends ContractRouter> = {\n [K in keyof T]: T[K] extends RouteDefinition\n ? HasKeys<PathParams<T[K][\"path\"]>> extends true\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { query: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { body: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends ContractRouter\n ? PlugClient<T[K]>\n : never;\n};\n\n// ---------------------------------------------------------------------------\n// defineContract — type-narrowing identity function\n// ---------------------------------------------------------------------------\n\nexport function defineContract<const T extends ContractRouter>(contract: T): T {\n return contract;\n}\n\n// ---------------------------------------------------------------------------\n// PlugLike interface\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal interface expected from a Plug instance.\n * Matches the scaffolded Plug class's public API.\n */\nexport interface PlugLike {\n request<T>(\n method: string,\n path: string,\n options?: {\n params?: Record<string, unknown>;\n body?: unknown;\n headers?: Record<string, string>;\n },\n ): Promise<T>;\n}\n\nexport interface PlugClientOptions {\n validateResponse?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// PlugError detection — works without importing the user's PlugError class\n// ---------------------------------------------------------------------------\n\ninterface PlugErrorLike {\n name: string;\n status: number;\n body: unknown;\n}\n\nfunction isPlugError(err: unknown): err is PlugErrorLike {\n return (\n err instanceof Error &&\n \"status\" in err &&\n typeof (err as unknown as PlugErrorLike).status === \"number\" &&\n \"body\" in err\n );\n}\n\nfunction parseBody(body: unknown): unknown {\n if (typeof body === \"string\") {\n try {\n return JSON.parse(body);\n } catch {\n return body;\n }\n }\n return body;\n}\n\n// ---------------------------------------------------------------------------\n// Path interpolation\n// ---------------------------------------------------------------------------\n\nfunction interpolatePath(path: string, params: Record<string, string>): string {\n return path.replace(/:([^/]+)/g, (_, key: string) => {\n const value = params[key];\n if (value === undefined) {\n throw new Error(`Missing path parameter: \"${key}\"`);\n }\n return encodeURIComponent(value);\n });\n}\n\n// ---------------------------------------------------------------------------\n// createPlugClient\n// ---------------------------------------------------------------------------\n\nexport function createPlugClient<T extends ContractRouter>(\n contract: T,\n plug: PlugLike,\n options?: PlugClientOptions,\n): PlugClient<T> {\n const globalValidateResponse = options?.validateResponse ?? true;\n\n function buildClient(router: ContractRouter): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(router)) {\n const route = value;\n\n if (\"method\" in route && \"path\" in route) {\n client[key] = async (input?: Record<string, unknown>) => {\n const def = route as RouteDefinition;\n const params = (input?.[\"params\"] ?? {}) as Record<string, string>;\n const query = input?.[\"query\"] as Record<string, unknown> | undefined;\n const body = input?.[\"body\"];\n const headers = input?.[\"headers\"] as\n | Record<string, string>\n | undefined;\n const shouldValidateResponse =\n (input?.[\"validateResponse\"] as boolean | undefined) ??\n globalValidateResponse;\n\n if (def.pathParams) {\n def.pathParams.parse(params);\n }\n if (def.query && query !== undefined) {\n def.query.parse(query);\n }\n if (def.body && body !== undefined) {\n def.body.parse(body);\n }\n\n const interpolatedPath = interpolatePath(def.path, params);\n const responses = def.responses;\n\n try {\n const responseBody = await plug.request<unknown>(\n def.method,\n interpolatedPath,\n {\n ...(query !== undefined ? { params: query } : {}),\n ...(body !== undefined ? { body } : {}),\n ...(headers !== undefined ? { headers } : {}),\n },\n );\n\n const status = 200;\n if (!responses[status]) {\n return { status, body: responseBody };\n }\n\n if (shouldValidateResponse) {\n const validated = responses[status].parse(responseBody);\n return { status, body: validated };\n }\n\n return { status, body: responseBody };\n } catch (err) {\n if (!isPlugError(err)) throw err;\n\n const status = err.status;\n if (responses[status]) {\n const parsed = parseBody(err.body);\n if (shouldValidateResponse) {\n const validated = responses[status].parse(parsed);\n return { status, body: validated };\n }\n return { status, body: parsed };\n }\n\n throw err;\n }\n };\n } else {\n client[key] = buildClient(route);\n }\n }\n\n return client;\n }\n\n return buildClient(contract) as PlugClient<T>;\n}\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
interface Schema<TOutput = unknown, TInput = unknown> {
|
|
2
|
+
parse(data: unknown): TOutput;
|
|
3
|
+
_input?: TInput;
|
|
4
|
+
_output?: TOutput;
|
|
5
|
+
}
|
|
6
|
+
interface RouteDefinition {
|
|
7
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
8
|
+
path: string;
|
|
9
|
+
query?: Schema;
|
|
10
|
+
body?: Schema;
|
|
11
|
+
pathParams?: Schema;
|
|
12
|
+
responses: Record<number, Schema>;
|
|
13
|
+
}
|
|
14
|
+
interface ContractRouter {
|
|
15
|
+
[key: string]: RouteDefinition | ContractRouter;
|
|
16
|
+
}
|
|
17
|
+
type SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;
|
|
18
|
+
type SchemaOutput<T> = T extends Schema<infer O> ? O : never;
|
|
19
|
+
type PathParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Record<Param | keyof PathParams<Rest>, string> : T extends `${string}:${infer Param}` ? Record<Param, string> : never;
|
|
20
|
+
type HasKeys<T> = keyof T extends never ? false : true;
|
|
21
|
+
type EndpointInput<TRoute extends RouteDefinition> = (HasKeys<PathParams<TRoute["path"]>> extends true ? {
|
|
22
|
+
params: PathParams<TRoute["path"]>;
|
|
23
|
+
} : {
|
|
24
|
+
params?: never;
|
|
25
|
+
}) & (TRoute extends {
|
|
26
|
+
query: Schema;
|
|
27
|
+
} ? {
|
|
28
|
+
query: SchemaInput<TRoute["query"]>;
|
|
29
|
+
} : {
|
|
30
|
+
query?: never;
|
|
31
|
+
}) & (TRoute extends {
|
|
32
|
+
body: Schema;
|
|
33
|
+
} ? {
|
|
34
|
+
body: SchemaInput<TRoute["body"]>;
|
|
35
|
+
} : {
|
|
36
|
+
body?: never;
|
|
37
|
+
}) & {
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
validateResponse?: boolean;
|
|
40
|
+
};
|
|
41
|
+
type ResponseSchemas<TRoute extends RouteDefinition> = {
|
|
42
|
+
[K in keyof TRoute["responses"]]: K extends number ? {
|
|
43
|
+
status: K;
|
|
44
|
+
body: SchemaOutput<TRoute["responses"][K]>;
|
|
45
|
+
} : never;
|
|
46
|
+
}[keyof TRoute["responses"]];
|
|
47
|
+
type PlugClient<T extends ContractRouter> = {
|
|
48
|
+
[K in keyof T]: T[K] extends RouteDefinition ? HasKeys<PathParams<T[K]["path"]>> extends true ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
|
|
49
|
+
query: Schema;
|
|
50
|
+
} ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
|
|
51
|
+
body: Schema;
|
|
52
|
+
} ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends ContractRouter ? PlugClient<T[K]> : never;
|
|
53
|
+
};
|
|
54
|
+
declare function defineContract<const T extends ContractRouter>(contract: T): T;
|
|
55
|
+
/**
|
|
56
|
+
* Minimal interface expected from a Plug instance.
|
|
57
|
+
* Matches the scaffolded Plug class's public API.
|
|
58
|
+
*/
|
|
59
|
+
interface PlugLike {
|
|
60
|
+
request<T>(method: string, path: string, options?: {
|
|
61
|
+
params?: Record<string, unknown>;
|
|
62
|
+
body?: unknown;
|
|
63
|
+
headers?: Record<string, string>;
|
|
64
|
+
}): Promise<T>;
|
|
65
|
+
}
|
|
66
|
+
interface PlugClientOptions {
|
|
67
|
+
validateResponse?: boolean;
|
|
68
|
+
}
|
|
69
|
+
declare function createPlugClient<T extends ContractRouter>(contract: T, plug: PlugLike, options?: PlugClientOptions): PlugClient<T>;
|
|
70
|
+
|
|
71
|
+
export { type ContractRouter, type PlugClientOptions, type PlugLike, type RouteDefinition, type Schema, createPlugClient, defineContract };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
interface Schema<TOutput = unknown, TInput = unknown> {
|
|
2
|
+
parse(data: unknown): TOutput;
|
|
3
|
+
_input?: TInput;
|
|
4
|
+
_output?: TOutput;
|
|
5
|
+
}
|
|
6
|
+
interface RouteDefinition {
|
|
7
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
8
|
+
path: string;
|
|
9
|
+
query?: Schema;
|
|
10
|
+
body?: Schema;
|
|
11
|
+
pathParams?: Schema;
|
|
12
|
+
responses: Record<number, Schema>;
|
|
13
|
+
}
|
|
14
|
+
interface ContractRouter {
|
|
15
|
+
[key: string]: RouteDefinition | ContractRouter;
|
|
16
|
+
}
|
|
17
|
+
type SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;
|
|
18
|
+
type SchemaOutput<T> = T extends Schema<infer O> ? O : never;
|
|
19
|
+
type PathParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Record<Param | keyof PathParams<Rest>, string> : T extends `${string}:${infer Param}` ? Record<Param, string> : never;
|
|
20
|
+
type HasKeys<T> = keyof T extends never ? false : true;
|
|
21
|
+
type EndpointInput<TRoute extends RouteDefinition> = (HasKeys<PathParams<TRoute["path"]>> extends true ? {
|
|
22
|
+
params: PathParams<TRoute["path"]>;
|
|
23
|
+
} : {
|
|
24
|
+
params?: never;
|
|
25
|
+
}) & (TRoute extends {
|
|
26
|
+
query: Schema;
|
|
27
|
+
} ? {
|
|
28
|
+
query: SchemaInput<TRoute["query"]>;
|
|
29
|
+
} : {
|
|
30
|
+
query?: never;
|
|
31
|
+
}) & (TRoute extends {
|
|
32
|
+
body: Schema;
|
|
33
|
+
} ? {
|
|
34
|
+
body: SchemaInput<TRoute["body"]>;
|
|
35
|
+
} : {
|
|
36
|
+
body?: never;
|
|
37
|
+
}) & {
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
validateResponse?: boolean;
|
|
40
|
+
};
|
|
41
|
+
type ResponseSchemas<TRoute extends RouteDefinition> = {
|
|
42
|
+
[K in keyof TRoute["responses"]]: K extends number ? {
|
|
43
|
+
status: K;
|
|
44
|
+
body: SchemaOutput<TRoute["responses"][K]>;
|
|
45
|
+
} : never;
|
|
46
|
+
}[keyof TRoute["responses"]];
|
|
47
|
+
type PlugClient<T extends ContractRouter> = {
|
|
48
|
+
[K in keyof T]: T[K] extends RouteDefinition ? HasKeys<PathParams<T[K]["path"]>> extends true ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
|
|
49
|
+
query: Schema;
|
|
50
|
+
} ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
|
|
51
|
+
body: Schema;
|
|
52
|
+
} ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends ContractRouter ? PlugClient<T[K]> : never;
|
|
53
|
+
};
|
|
54
|
+
declare function defineContract<const T extends ContractRouter>(contract: T): T;
|
|
55
|
+
/**
|
|
56
|
+
* Minimal interface expected from a Plug instance.
|
|
57
|
+
* Matches the scaffolded Plug class's public API.
|
|
58
|
+
*/
|
|
59
|
+
interface PlugLike {
|
|
60
|
+
request<T>(method: string, path: string, options?: {
|
|
61
|
+
params?: Record<string, unknown>;
|
|
62
|
+
body?: unknown;
|
|
63
|
+
headers?: Record<string, string>;
|
|
64
|
+
}): Promise<T>;
|
|
65
|
+
}
|
|
66
|
+
interface PlugClientOptions {
|
|
67
|
+
validateResponse?: boolean;
|
|
68
|
+
}
|
|
69
|
+
declare function createPlugClient<T extends ContractRouter>(contract: T, plug: PlugLike, options?: PlugClientOptions): PlugClient<T>;
|
|
70
|
+
|
|
71
|
+
export { type ContractRouter, type PlugClientOptions, type PlugLike, type RouteDefinition, type Schema, createPlugClient, defineContract };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/plug-client.ts
|
|
2
|
+
function defineContract(contract) {
|
|
3
|
+
return contract;
|
|
4
|
+
}
|
|
5
|
+
function isPlugError(err) {
|
|
6
|
+
return err instanceof Error && "status" in err && typeof err.status === "number" && "body" in err;
|
|
7
|
+
}
|
|
8
|
+
function parseBody(body) {
|
|
9
|
+
if (typeof body === "string") {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(body);
|
|
12
|
+
} catch {
|
|
13
|
+
return body;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return body;
|
|
17
|
+
}
|
|
18
|
+
function interpolatePath(path, params) {
|
|
19
|
+
return path.replace(/:([^/]+)/g, (_, key) => {
|
|
20
|
+
const value = params[key];
|
|
21
|
+
if (value === void 0) {
|
|
22
|
+
throw new Error(`Missing path parameter: "${key}"`);
|
|
23
|
+
}
|
|
24
|
+
return encodeURIComponent(value);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function createPlugClient(contract, plug, options) {
|
|
28
|
+
const globalValidateResponse = options?.validateResponse ?? true;
|
|
29
|
+
function buildClient(router) {
|
|
30
|
+
const client = {};
|
|
31
|
+
for (const [key, value] of Object.entries(router)) {
|
|
32
|
+
const route = value;
|
|
33
|
+
if ("method" in route && "path" in route) {
|
|
34
|
+
client[key] = async (input) => {
|
|
35
|
+
const def = route;
|
|
36
|
+
const params = input?.["params"] ?? {};
|
|
37
|
+
const query = input?.["query"];
|
|
38
|
+
const body = input?.["body"];
|
|
39
|
+
const headers = input?.["headers"];
|
|
40
|
+
const shouldValidateResponse = input?.["validateResponse"] ?? globalValidateResponse;
|
|
41
|
+
if (def.pathParams) {
|
|
42
|
+
def.pathParams.parse(params);
|
|
43
|
+
}
|
|
44
|
+
if (def.query && query !== void 0) {
|
|
45
|
+
def.query.parse(query);
|
|
46
|
+
}
|
|
47
|
+
if (def.body && body !== void 0) {
|
|
48
|
+
def.body.parse(body);
|
|
49
|
+
}
|
|
50
|
+
const interpolatedPath = interpolatePath(def.path, params);
|
|
51
|
+
const responses = def.responses;
|
|
52
|
+
try {
|
|
53
|
+
const responseBody = await plug.request(
|
|
54
|
+
def.method,
|
|
55
|
+
interpolatedPath,
|
|
56
|
+
{
|
|
57
|
+
...query !== void 0 ? { params: query } : {},
|
|
58
|
+
...body !== void 0 ? { body } : {},
|
|
59
|
+
...headers !== void 0 ? { headers } : {}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
const status = 200;
|
|
63
|
+
if (!responses[status]) {
|
|
64
|
+
return { status, body: responseBody };
|
|
65
|
+
}
|
|
66
|
+
if (shouldValidateResponse) {
|
|
67
|
+
const validated = responses[status].parse(responseBody);
|
|
68
|
+
return { status, body: validated };
|
|
69
|
+
}
|
|
70
|
+
return { status, body: responseBody };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (!isPlugError(err)) throw err;
|
|
73
|
+
const status = err.status;
|
|
74
|
+
if (responses[status]) {
|
|
75
|
+
const parsed = parseBody(err.body);
|
|
76
|
+
if (shouldValidateResponse) {
|
|
77
|
+
const validated = responses[status].parse(parsed);
|
|
78
|
+
return { status, body: validated };
|
|
79
|
+
}
|
|
80
|
+
return { status, body: parsed };
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
} else {
|
|
86
|
+
client[key] = buildClient(route);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return client;
|
|
90
|
+
}
|
|
91
|
+
return buildClient(contract);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { createPlugClient, defineContract };
|
|
95
|
+
//# sourceMappingURL=plug-client.js.map
|
|
96
|
+
//# sourceMappingURL=plug-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plug-client.ts"],"names":[],"mappings":"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","file":"plug-client.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Schema interface — works with zod v3, v4, or any validator with .parse()\n// ---------------------------------------------------------------------------\n\nexport interface Schema<TOutput = unknown, TInput = unknown> {\n parse(data: unknown): TOutput;\n _input?: TInput;\n _output?: TOutput;\n}\n\n// ---------------------------------------------------------------------------\n// Contract types\n// ---------------------------------------------------------------------------\n\nexport interface RouteDefinition {\n method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n path: string;\n query?: Schema;\n body?: Schema;\n pathParams?: Schema;\n responses: Record<number, Schema>;\n}\n\nexport interface ContractRouter {\n [key: string]: RouteDefinition | ContractRouter;\n}\n\n// ---------------------------------------------------------------------------\n// Type-level utilities\n// ---------------------------------------------------------------------------\n\ntype SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;\ntype SchemaOutput<T> = T extends Schema<infer O> ? O : never;\n\ntype PathParams<T extends string> =\n T extends `${string}:${infer Param}/${infer Rest}`\n ? Record<Param | keyof PathParams<Rest>, string>\n : T extends `${string}:${infer Param}`\n ? Record<Param, string>\n : never;\n\ntype HasKeys<T> = keyof T extends never ? false : true;\n\ntype EndpointInput<TRoute extends RouteDefinition> = (HasKeys<\n PathParams<TRoute[\"path\"]>\n> extends true\n ? { params: PathParams<TRoute[\"path\"]> }\n : { params?: never }) &\n (TRoute extends { query: Schema }\n ? { query: SchemaInput<TRoute[\"query\"]> }\n : { query?: never }) &\n (TRoute extends { body: Schema }\n ? { body: SchemaInput<TRoute[\"body\"]> }\n : { body?: never }) & {\n headers?: Record<string, string>;\n validateResponse?: boolean;\n };\n\ntype ResponseSchemas<TRoute extends RouteDefinition> = {\n [K in keyof TRoute[\"responses\"]]: K extends number\n ? { status: K; body: SchemaOutput<TRoute[\"responses\"][K]> }\n : never;\n}[keyof TRoute[\"responses\"]];\n\ntype PlugClient<T extends ContractRouter> = {\n [K in keyof T]: T[K] extends RouteDefinition\n ? HasKeys<PathParams<T[K][\"path\"]>> extends true\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { query: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { body: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends ContractRouter\n ? PlugClient<T[K]>\n : never;\n};\n\n// ---------------------------------------------------------------------------\n// defineContract — type-narrowing identity function\n// ---------------------------------------------------------------------------\n\nexport function defineContract<const T extends ContractRouter>(contract: T): T {\n return contract;\n}\n\n// ---------------------------------------------------------------------------\n// PlugLike interface\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal interface expected from a Plug instance.\n * Matches the scaffolded Plug class's public API.\n */\nexport interface PlugLike {\n request<T>(\n method: string,\n path: string,\n options?: {\n params?: Record<string, unknown>;\n body?: unknown;\n headers?: Record<string, string>;\n },\n ): Promise<T>;\n}\n\nexport interface PlugClientOptions {\n validateResponse?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// PlugError detection — works without importing the user's PlugError class\n// ---------------------------------------------------------------------------\n\ninterface PlugErrorLike {\n name: string;\n status: number;\n body: unknown;\n}\n\nfunction isPlugError(err: unknown): err is PlugErrorLike {\n return (\n err instanceof Error &&\n \"status\" in err &&\n typeof (err as unknown as PlugErrorLike).status === \"number\" &&\n \"body\" in err\n );\n}\n\nfunction parseBody(body: unknown): unknown {\n if (typeof body === \"string\") {\n try {\n return JSON.parse(body);\n } catch {\n return body;\n }\n }\n return body;\n}\n\n// ---------------------------------------------------------------------------\n// Path interpolation\n// ---------------------------------------------------------------------------\n\nfunction interpolatePath(path: string, params: Record<string, string>): string {\n return path.replace(/:([^/]+)/g, (_, key: string) => {\n const value = params[key];\n if (value === undefined) {\n throw new Error(`Missing path parameter: \"${key}\"`);\n }\n return encodeURIComponent(value);\n });\n}\n\n// ---------------------------------------------------------------------------\n// createPlugClient\n// ---------------------------------------------------------------------------\n\nexport function createPlugClient<T extends ContractRouter>(\n contract: T,\n plug: PlugLike,\n options?: PlugClientOptions,\n): PlugClient<T> {\n const globalValidateResponse = options?.validateResponse ?? true;\n\n function buildClient(router: ContractRouter): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(router)) {\n const route = value;\n\n if (\"method\" in route && \"path\" in route) {\n client[key] = async (input?: Record<string, unknown>) => {\n const def = route as RouteDefinition;\n const params = (input?.[\"params\"] ?? {}) as Record<string, string>;\n const query = input?.[\"query\"] as Record<string, unknown> | undefined;\n const body = input?.[\"body\"];\n const headers = input?.[\"headers\"] as\n | Record<string, string>\n | undefined;\n const shouldValidateResponse =\n (input?.[\"validateResponse\"] as boolean | undefined) ??\n globalValidateResponse;\n\n if (def.pathParams) {\n def.pathParams.parse(params);\n }\n if (def.query && query !== undefined) {\n def.query.parse(query);\n }\n if (def.body && body !== undefined) {\n def.body.parse(body);\n }\n\n const interpolatedPath = interpolatePath(def.path, params);\n const responses = def.responses;\n\n try {\n const responseBody = await plug.request<unknown>(\n def.method,\n interpolatedPath,\n {\n ...(query !== undefined ? { params: query } : {}),\n ...(body !== undefined ? { body } : {}),\n ...(headers !== undefined ? { headers } : {}),\n },\n );\n\n const status = 200;\n if (!responses[status]) {\n return { status, body: responseBody };\n }\n\n if (shouldValidateResponse) {\n const validated = responses[status].parse(responseBody);\n return { status, body: validated };\n }\n\n return { status, body: responseBody };\n } catch (err) {\n if (!isPlugError(err)) throw err;\n\n const status = err.status;\n if (responses[status]) {\n const parsed = parseBody(err.body);\n if (shouldValidateResponse) {\n const validated = responses[status].parse(parsed);\n return { status, body: validated };\n }\n return { status, body: parsed };\n }\n\n throw err;\n }\n };\n } else {\n client[key] = buildClient(route);\n }\n }\n\n return client;\n }\n\n return buildClient(contract) as PlugClient<T>;\n}\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: khotan-probe
|
|
3
|
+
description: >
|
|
4
|
+
Inspect and debug khotan plugs via the CLI. Prefer `khotan plug`
|
|
5
|
+
(legacy alias: `khotan probe`). Use when verifying API
|
|
6
|
+
response shapes against typed endpoint definitions, debugging type
|
|
7
|
+
mismatches between declared schemas and actual responses, or exploring
|
|
8
|
+
available plugs and their endpoints.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Inspect and debug khotan plugs via the CLI. Prefer `khotan plug` (legacy alias: `khotan probe`). Use when verifying API response shapes against typed endpoint definitions, debugging type mismatches between declared schemas and actual responses, or exploring available plugs and their endpoints.
|
|
12
|
+
|
|
13
|
+
**Requires**: A running dev server with `KHOTAN_DEBUG=1` set.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### List all plugs
|
|
18
|
+
```bash
|
|
19
|
+
npx khotan plug --list
|
|
20
|
+
```
|
|
21
|
+
Returns: `{ ok, plugs: [{ name, baseUrl, authType, varsConfigured }] }`
|
|
22
|
+
|
|
23
|
+
### Show plug info and endpoints
|
|
24
|
+
```bash
|
|
25
|
+
npx khotan plug <plugName> --info
|
|
26
|
+
```
|
|
27
|
+
Returns: `{ ok, plug: { name, baseUrl, authType, vars, endpoints } }`
|
|
28
|
+
|
|
29
|
+
### Fire a request through a plug
|
|
30
|
+
```bash
|
|
31
|
+
npx khotan plug <plugName> GET /products
|
|
32
|
+
npx khotan plug <plugName> POST /subscriptions --body '{"url":"https://example.com"}'
|
|
33
|
+
npx khotan plug <plugName> GET /products --params '{"limit":"10"}'
|
|
34
|
+
npx khotan plug <plugName> GET /products --headers '{"X-Custom":"value"}'
|
|
35
|
+
```
|
|
36
|
+
Returns: `{ ok, request, response: { status, timing, size, body }, matchedEndpoint }`
|
|
37
|
+
|
|
38
|
+
### Fire via named endpoint
|
|
39
|
+
```bash
|
|
40
|
+
npx khotan plug <plugName> --endpoint listProducts
|
|
41
|
+
```
|
|
42
|
+
Resolves method and path from the endpoint definition automatically.
|
|
43
|
+
|
|
44
|
+
### Compare response against schema
|
|
45
|
+
```bash
|
|
46
|
+
npx khotan plug <plugName> --endpoint listProducts --compare
|
|
47
|
+
```
|
|
48
|
+
Returns: `{ ..., comparison: { match, expected, actual, mismatches } }`
|
|
49
|
+
|
|
50
|
+
Each mismatch has `{ path, issue, note }` where issue is `missing`, `extra`, or `type_mismatch` and path uses JSONPath notation (e.g. `$.items[].sku`).
|
|
51
|
+
|
|
52
|
+
## Options
|
|
53
|
+
|
|
54
|
+
| Flag | Description |
|
|
55
|
+
|------|-------------|
|
|
56
|
+
| `--port <n>` | Dev server port (default: from .env.local → .env → 3000) |
|
|
57
|
+
| `--base-path <p>` | API base path (default: `/api/khotan`) |
|
|
58
|
+
| `--list` | List registered plugs |
|
|
59
|
+
| `--info` | Show plug metadata |
|
|
60
|
+
| `--endpoint <name>` | Fire using named endpoint |
|
|
61
|
+
| `--compare` | Diff response against schema |
|
|
62
|
+
| `--body <json>` | Request body |
|
|
63
|
+
| `--params <json>` | Query params |
|
|
64
|
+
| `--headers <json>` | Extra headers |
|
|
65
|
+
|
|
66
|
+
## Workflow
|
|
67
|
+
|
|
68
|
+
1. **Discover**: `--list` to find plugs, `--info` to see endpoints
|
|
69
|
+
2. **Probe**: Fire a request via endpoint name or raw method/path
|
|
70
|
+
3. **Compare**: Add `--compare` to check response shape against Zod schema
|
|
71
|
+
4. **Fix**: If mismatches found, update the endpoint's response schema or adjust API expectations
|
|
72
|
+
|
|
73
|
+
All output is JSON on stdout — parse with standard JSON tools.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# khotan-data
|
|
2
|
+
|
|
3
|
+
Production-grade data flow, ETL, and webhook components for Next.js + Drizzle + Postgres.
|
|
4
|
+
|
|
5
|
+
## Skills
|
|
6
|
+
|
|
7
|
+
| Skill | When to use |
|
|
8
|
+
|-------|-------------|
|
|
9
|
+
| [khotan-setup](skills/khotan-setup/SKILL.md) | Initializing khotan in a new project, adding the database schema, configuring the factory |
|
|
10
|
+
| [khotan-plug](skills/khotan-plug/SKILL.md) | Connecting to a new API, defining endpoint contracts, configuring authentication |
|
|
11
|
+
| [khotan-dashboard](skills/khotan-dashboard/SKILL.md) | Adding a management interface, configuring plug variables in the browser |
|
|
12
|
+
| [khotan-webhook](skills/khotan-webhook/SKILL.md) | Receiving webhooks, registering callback URLs, processing incoming events |
|
|
13
|
+
| [khotan-probe](skills/khotan-probe/SKILL.md) | Debugging plugs via CLI with `khotan plug` (legacy alias: `probe`) |
|
|
14
|
+
|
|
15
|
+
## Quick Reference
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx khotan init # Initialize project
|
|
19
|
+
npx khotan add plug --yes # Add HTTP client component
|
|
20
|
+
npx khotan add hub --yes # Add dashboard UI
|
|
21
|
+
npx khotan generate # Scaffold Drizzle schema
|
|
22
|
+
npx khotan migrate # Apply database migrations
|
|
23
|
+
npx khotan plug --list # Debug: list registered plugs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Key Files
|
|
27
|
+
|
|
28
|
+
| File | Purpose |
|
|
29
|
+
|------|---------|
|
|
30
|
+
| `khotan.config.ts` | CLI config — sets outputDir |
|
|
31
|
+
| `{outputDir}/khotan.ts` | Factory config — register plugs, resources, adapter |
|
|
32
|
+
| `src/app/api/khotan/[...all]/route.ts` | Catch-all API route |
|
|
33
|
+
|
|
34
|
+
## Environment Variables
|
|
35
|
+
|
|
36
|
+
| Variable | Purpose |
|
|
37
|
+
|----------|---------|
|
|
38
|
+
| `DATABASE_URL` | Postgres connection (Drizzle) |
|
|
39
|
+
| `KHOTAN_SECRET` | AES-256-GCM key for encrypting plug variables |
|
|
40
|
+
| `KHOTAN_DEBUG` | Enables debug routes and the `plug` CLI (`probe` alias) |
|
|
41
|
+
| `KHOTAN_WEBHOOK_URL` | Public URL for webhook callbacks |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Example: Catch
|
|
3
|
+
// Generated by khotan CLI · https://github.com/khotan-data
|
|
4
|
+
//
|
|
5
|
+
// Copy this file, rename it for your webhook source/event, and register the
|
|
6
|
+
// exported catch handler in {outputDir}/khotan.ts.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
import { catchEvent, type CatchContext } from "./catch";
|
|
10
|
+
|
|
11
|
+
async function stripeInvoiceCatchWorkflow(ctx: CatchContext) {
|
|
12
|
+
"use workflow";
|
|
13
|
+
|
|
14
|
+
async function persistEvent() {
|
|
15
|
+
"use step";
|
|
16
|
+
console.log("Handling webhook event", {
|
|
17
|
+
eventType: ctx.eventType,
|
|
18
|
+
khotanRunId: ctx.khotanRunId,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Khotan already records webhook deliveries. Add app-specific side effects
|
|
22
|
+
// here, such as updating a local table or enqueueing downstream work.
|
|
23
|
+
console.log("Webhook payload", {
|
|
24
|
+
event: ctx.event,
|
|
25
|
+
headers: ctx.headers,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await persistEvent();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const stripeInvoiceCatch = catchEvent({
|
|
33
|
+
name: "stripe-invoices",
|
|
34
|
+
events: ["invoice.paid"],
|
|
35
|
+
workflow: stripeInvoiceCatchWorkflow,
|
|
36
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Catch — durable webhook event processing via Vercel Workflow
|
|
3
|
+
// Generated by khotan CLI · https://github.com/khotan-data
|
|
4
|
+
//
|
|
5
|
+
// This file defines the catchEvent() builder and types. Create per-service
|
|
6
|
+
// catch files (e.g. pollinate-catch.ts) using this builder to handle verified
|
|
7
|
+
// webhook events with durable, retryable workflow steps.
|
|
8
|
+
//
|
|
9
|
+
// Catch workflows receive a CatchContext with the parsed event payload,
|
|
10
|
+
// event type, and headers. Steps have full Node.js access — import your
|
|
11
|
+
// own db, services, etc.
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Context — serializable data passed to the workflow
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export interface CatchContext {
|
|
19
|
+
/** Parsed webhook payload */
|
|
20
|
+
event: Record<string, unknown>;
|
|
21
|
+
/** Event type extracted from payload (e.g. "order.created") */
|
|
22
|
+
eventType: string;
|
|
23
|
+
/** Incoming request headers */
|
|
24
|
+
headers: Record<string, string>;
|
|
25
|
+
/** Khotan run ID created for this webhook handler execution */
|
|
26
|
+
khotanRunId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Workflow type — the function signature your workflow must conform to
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export type CatchWorkflow = (ctx: CatchContext) => Promise<void>;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Config — passed to the catchEvent() builder
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export interface CatchConfig {
|
|
40
|
+
/** Unique name for this catch handler (used for DB tracking and Hub display) */
|
|
41
|
+
name: string;
|
|
42
|
+
/** Event types this catch should receive */
|
|
43
|
+
events?: string[];
|
|
44
|
+
/** Workflow function that processes the event */
|
|
45
|
+
workflow: CatchWorkflow;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Registration — returned by catchEvent(), consumed by factory config
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
export interface CatchRegistration {
|
|
53
|
+
type: "catch";
|
|
54
|
+
name: string;
|
|
55
|
+
events?: string[];
|
|
56
|
+
workflow: CatchWorkflow;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Builder
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export function catchEvent(config: CatchConfig): CatchRegistration {
|
|
64
|
+
return {
|
|
65
|
+
type: "catch",
|
|
66
|
+
name: config.name,
|
|
67
|
+
events: config.events,
|
|
68
|
+
workflow: config.workflow,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Usage Example (create a file like webhooks/pollinate-catch.ts)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
//
|
|
76
|
+
// import { catchEvent, type CatchContext } from "./catch";
|
|
77
|
+
// Khotan already records webhook deliveries in khotan_webhook_events and links
|
|
78
|
+
// them to khotan_runs. Use your catch workflow for app-specific side effects.
|
|
79
|
+
//
|
|
80
|
+
// async function pollinateCatchWorkflow(ctx: CatchContext) {
|
|
81
|
+
// "use workflow";
|
|
82
|
+
//
|
|
83
|
+
// async function notifyOps() {
|
|
84
|
+
// "use step";
|
|
85
|
+
// console.log("Handled webhook", {
|
|
86
|
+
// eventType: ctx.eventType,
|
|
87
|
+
// khotanRunId: ctx.khotanRunId,
|
|
88
|
+
// });
|
|
89
|
+
// }
|
|
90
|
+
//
|
|
91
|
+
// await notifyOps();
|
|
92
|
+
// }
|
|
93
|
+
//
|
|
94
|
+
// export const pollinateCatch = catchEvent({
|
|
95
|
+
// name: "pollinate-orders",
|
|
96
|
+
// events: ["order.created"],
|
|
97
|
+
// workflow: pollinateCatchWorkflow,
|
|
98
|
+
// });
|
|
99
|
+
//
|
|
100
|
+
// Then register in your khotan config:
|
|
101
|
+
//
|
|
102
|
+
// plugs: [{
|
|
103
|
+
// name: "pollinate",
|
|
104
|
+
// plug: pollinatePlug,
|
|
105
|
+
// wires: [pollinateWire],
|
|
106
|
+
// catches: [pollinateCatch],
|
|
107
|
+
// }]
|