khotan-data 0.1.1 → 0.2.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/README.md +29 -0
- package/dist/cli.js +132 -46
- package/dist/factory.cjs +79 -9
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +38 -1
- package/dist/factory.d.ts +38 -1
- package/dist/factory.js +79 -10
- package/dist/factory.js.map +1 -1
- package/dist/templates/api-state.tsx +249 -0
- package/dist/templates/debug-index-page.tsx +56 -36
- package/dist/templates/hub.tsx +9 -23
- package/dist/templates/khotan-config.ts +17 -0
- package/dist/templates/mapping-browser.tsx +56 -44
- package/dist/templates/plug-debugger.tsx +15 -7
- package/dist/templates/runs-table.tsx +133 -130
- package/dist/templates/skill-setup.md +37 -2
- package/dist/templates/topology-canvas.tsx +19 -30
- package/dist/templates/var-panel.tsx +33 -10
- package/dist/templates/webhook-events-table.tsx +105 -102
- package/dist/templates/wire-panel.tsx +30 -8
- package/package.json +1 -1
package/dist/factory.d.cts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { PgDatabase } from 'drizzle-orm/pg-core';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Derives the CLI auth token: HMAC-SHA256 over the timestamp, keyed by the
|
|
5
|
+
* KHOTAN_SECRET. One-way, so the raw secret (the encryption key) never travels
|
|
6
|
+
* over the wire — even a token captured from a dev log can't be reversed into
|
|
7
|
+
* the secret. Exported so the CLI can compute the same value.
|
|
8
|
+
*/
|
|
9
|
+
declare function deriveCliToken(secret: string, timestamp: string): Promise<string>;
|
|
3
10
|
type ResourceConnectField = string | [string, ...string[]];
|
|
4
11
|
interface ResourcePlugParticipation {
|
|
5
12
|
uniqueIdentifier: string;
|
|
@@ -551,12 +558,42 @@ interface KhotanAdapter {
|
|
|
551
558
|
lastRunStatus: KhotanTerminalRunStatus;
|
|
552
559
|
}): Promise<void>;
|
|
553
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* Authorize an incoming request to the khotan management API.
|
|
563
|
+
*
|
|
564
|
+
* Return `true` to allow the request, `false` to reject it with `401`.
|
|
565
|
+
* The function receives the raw `Request`, so it composes directly with
|
|
566
|
+
* session libraries such as better-auth:
|
|
567
|
+
*
|
|
568
|
+
* ```ts
|
|
569
|
+
* authorize: async (request) => {
|
|
570
|
+
* const session = await auth.api.getSession({ headers: request.headers });
|
|
571
|
+
* return session?.user?.role === "admin";
|
|
572
|
+
* }
|
|
573
|
+
* ```
|
|
574
|
+
*
|
|
575
|
+
* Throwing is treated the same as returning `false`.
|
|
576
|
+
*
|
|
577
|
+
* The following routes are intentionally exempt and are NOT passed to
|
|
578
|
+
* `authorize` (they have their own protection):
|
|
579
|
+
* - Inbound webhooks (`POST .../webhook/:plug`) — verified per-plug via `onVerify`.
|
|
580
|
+
* - The cron dispatcher (`.../cron`) — protected by `CRON_SECRET`.
|
|
581
|
+
* - Debug routes (`.../debug...`) — gated by `KHOTAN_DEBUG` and disabled in production.
|
|
582
|
+
*/
|
|
583
|
+
type KhotanAuthorize = (request: Request) => boolean | Promise<boolean>;
|
|
554
584
|
interface KhotanConfig {
|
|
555
585
|
adapter: KhotanAdapter;
|
|
556
586
|
plugs: PlugRegistration[];
|
|
557
587
|
resources?: ResourceRegistration[];
|
|
558
588
|
caches?: CacheRegistration[];
|
|
559
589
|
secret?: string;
|
|
590
|
+
/**
|
|
591
|
+
* Gate every management route (plugs, variables, flows, runs, wires,
|
|
592
|
+
* mappings, caches, resources, webhook handlers/events) behind a custom
|
|
593
|
+
* authorization check. Strongly recommended for any deployed app — without
|
|
594
|
+
* it the management API is publicly accessible. See {@link KhotanAuthorize}.
|
|
595
|
+
*/
|
|
596
|
+
authorize?: KhotanAuthorize;
|
|
560
597
|
}
|
|
561
598
|
type KhotanHandler = (request: Request) => Promise<Response>;
|
|
562
599
|
interface WireInstance {
|
|
@@ -659,4 +696,4 @@ interface NextJsRouteHandlers {
|
|
|
659
696
|
}
|
|
660
697
|
declare function toNextJsHandler(factoryHandler: KhotanHandler): NextJsRouteHandlers;
|
|
661
698
|
|
|
662
|
-
export { type BindablePlug, type BoundPlug, type CacheEntryRecord, type CacheInstance, type CacheRegistration, type CacheScope, type CatchRegistration, type CatchWorkflowContext, type FlowInstance, type FlowRegistration, type FlowRunContext, type FlowRunResult, type FlowSelectorOptions, type FlowStartOptions, type FlowType, type FlowWorkflowContext, type KhotanAdapter, type KhotanConfig, type KhotanHandler, type KhotanInstance, type KhotanRunStatus, type KhotanRunUpdate, type KhotanTerminalRunStatus, type PassRegistration, type PassWorkflowContext, type PlugRegistration, type ResourceConnectField, type ResourceMappingRegistration, type ResourcePlugParticipation, type ResourceRegistration, type VarField, type WebhookRegistration, type WireInstance, type WireRegistration, type WireSubscribeContext, type WireUnsubscribeContext, type WireVerifyContext, __setWorkflowGetRunForTests, __setWorkflowGetWritableForTests, __setWorkflowStartForTests, bindWorkflowPlug, drizzleAdapter, khotan, khotanCache, khotanMappings, sendUpdate, toNextJsHandler };
|
|
699
|
+
export { type BindablePlug, type BoundPlug, type CacheEntryRecord, type CacheInstance, type CacheRegistration, type CacheScope, type CatchRegistration, type CatchWorkflowContext, type FlowInstance, type FlowRegistration, type FlowRunContext, type FlowRunResult, type FlowSelectorOptions, type FlowStartOptions, type FlowType, type FlowWorkflowContext, type KhotanAdapter, type KhotanAuthorize, type KhotanConfig, type KhotanHandler, type KhotanInstance, type KhotanRunStatus, type KhotanRunUpdate, type KhotanTerminalRunStatus, type PassRegistration, type PassWorkflowContext, type PlugRegistration, type ResourceConnectField, type ResourceMappingRegistration, type ResourcePlugParticipation, type ResourceRegistration, type VarField, type WebhookRegistration, type WireInstance, type WireRegistration, type WireSubscribeContext, type WireUnsubscribeContext, type WireVerifyContext, __setWorkflowGetRunForTests, __setWorkflowGetWritableForTests, __setWorkflowStartForTests, bindWorkflowPlug, deriveCliToken, drizzleAdapter, khotan, khotanCache, khotanMappings, sendUpdate, toNextJsHandler };
|
package/dist/factory.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { PgDatabase } from 'drizzle-orm/pg-core';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Derives the CLI auth token: HMAC-SHA256 over the timestamp, keyed by the
|
|
5
|
+
* KHOTAN_SECRET. One-way, so the raw secret (the encryption key) never travels
|
|
6
|
+
* over the wire — even a token captured from a dev log can't be reversed into
|
|
7
|
+
* the secret. Exported so the CLI can compute the same value.
|
|
8
|
+
*/
|
|
9
|
+
declare function deriveCliToken(secret: string, timestamp: string): Promise<string>;
|
|
3
10
|
type ResourceConnectField = string | [string, ...string[]];
|
|
4
11
|
interface ResourcePlugParticipation {
|
|
5
12
|
uniqueIdentifier: string;
|
|
@@ -551,12 +558,42 @@ interface KhotanAdapter {
|
|
|
551
558
|
lastRunStatus: KhotanTerminalRunStatus;
|
|
552
559
|
}): Promise<void>;
|
|
553
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* Authorize an incoming request to the khotan management API.
|
|
563
|
+
*
|
|
564
|
+
* Return `true` to allow the request, `false` to reject it with `401`.
|
|
565
|
+
* The function receives the raw `Request`, so it composes directly with
|
|
566
|
+
* session libraries such as better-auth:
|
|
567
|
+
*
|
|
568
|
+
* ```ts
|
|
569
|
+
* authorize: async (request) => {
|
|
570
|
+
* const session = await auth.api.getSession({ headers: request.headers });
|
|
571
|
+
* return session?.user?.role === "admin";
|
|
572
|
+
* }
|
|
573
|
+
* ```
|
|
574
|
+
*
|
|
575
|
+
* Throwing is treated the same as returning `false`.
|
|
576
|
+
*
|
|
577
|
+
* The following routes are intentionally exempt and are NOT passed to
|
|
578
|
+
* `authorize` (they have their own protection):
|
|
579
|
+
* - Inbound webhooks (`POST .../webhook/:plug`) — verified per-plug via `onVerify`.
|
|
580
|
+
* - The cron dispatcher (`.../cron`) — protected by `CRON_SECRET`.
|
|
581
|
+
* - Debug routes (`.../debug...`) — gated by `KHOTAN_DEBUG` and disabled in production.
|
|
582
|
+
*/
|
|
583
|
+
type KhotanAuthorize = (request: Request) => boolean | Promise<boolean>;
|
|
554
584
|
interface KhotanConfig {
|
|
555
585
|
adapter: KhotanAdapter;
|
|
556
586
|
plugs: PlugRegistration[];
|
|
557
587
|
resources?: ResourceRegistration[];
|
|
558
588
|
caches?: CacheRegistration[];
|
|
559
589
|
secret?: string;
|
|
590
|
+
/**
|
|
591
|
+
* Gate every management route (plugs, variables, flows, runs, wires,
|
|
592
|
+
* mappings, caches, resources, webhook handlers/events) behind a custom
|
|
593
|
+
* authorization check. Strongly recommended for any deployed app — without
|
|
594
|
+
* it the management API is publicly accessible. See {@link KhotanAuthorize}.
|
|
595
|
+
*/
|
|
596
|
+
authorize?: KhotanAuthorize;
|
|
560
597
|
}
|
|
561
598
|
type KhotanHandler = (request: Request) => Promise<Response>;
|
|
562
599
|
interface WireInstance {
|
|
@@ -659,4 +696,4 @@ interface NextJsRouteHandlers {
|
|
|
659
696
|
}
|
|
660
697
|
declare function toNextJsHandler(factoryHandler: KhotanHandler): NextJsRouteHandlers;
|
|
661
698
|
|
|
662
|
-
export { type BindablePlug, type BoundPlug, type CacheEntryRecord, type CacheInstance, type CacheRegistration, type CacheScope, type CatchRegistration, type CatchWorkflowContext, type FlowInstance, type FlowRegistration, type FlowRunContext, type FlowRunResult, type FlowSelectorOptions, type FlowStartOptions, type FlowType, type FlowWorkflowContext, type KhotanAdapter, type KhotanConfig, type KhotanHandler, type KhotanInstance, type KhotanRunStatus, type KhotanRunUpdate, type KhotanTerminalRunStatus, type PassRegistration, type PassWorkflowContext, type PlugRegistration, type ResourceConnectField, type ResourceMappingRegistration, type ResourcePlugParticipation, type ResourceRegistration, type VarField, type WebhookRegistration, type WireInstance, type WireRegistration, type WireSubscribeContext, type WireUnsubscribeContext, type WireVerifyContext, __setWorkflowGetRunForTests, __setWorkflowGetWritableForTests, __setWorkflowStartForTests, bindWorkflowPlug, drizzleAdapter, khotan, khotanCache, khotanMappings, sendUpdate, toNextJsHandler };
|
|
699
|
+
export { type BindablePlug, type BoundPlug, type CacheEntryRecord, type CacheInstance, type CacheRegistration, type CacheScope, type CatchRegistration, type CatchWorkflowContext, type FlowInstance, type FlowRegistration, type FlowRunContext, type FlowRunResult, type FlowSelectorOptions, type FlowStartOptions, type FlowType, type FlowWorkflowContext, type KhotanAdapter, type KhotanAuthorize, type KhotanConfig, type KhotanHandler, type KhotanInstance, type KhotanRunStatus, type KhotanRunUpdate, type KhotanTerminalRunStatus, type PassRegistration, type PassWorkflowContext, type PlugRegistration, type ResourceConnectField, type ResourceMappingRegistration, type ResourcePlugParticipation, type ResourceRegistration, type VarField, type WebhookRegistration, type WireInstance, type WireRegistration, type WireSubscribeContext, type WireUnsubscribeContext, type WireVerifyContext, __setWorkflowGetRunForTests, __setWorkflowGetWritableForTests, __setWorkflowStartForTests, bindWorkflowPlug, deriveCliToken, drizzleAdapter, khotan, khotanCache, khotanMappings, sendUpdate, toNextJsHandler };
|
package/dist/factory.js
CHANGED
|
@@ -259,6 +259,47 @@ function hexToBytes(hex) {
|
|
|
259
259
|
function bytesToHex(bytes) {
|
|
260
260
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
261
261
|
}
|
|
262
|
+
var CLI_TOKEN_SCHEME = "KhotanCLI";
|
|
263
|
+
var CLI_TOKEN_WINDOW_MS = 6e4;
|
|
264
|
+
async function deriveCliToken(secret, timestamp2) {
|
|
265
|
+
const key = await crypto.subtle.importKey(
|
|
266
|
+
"raw",
|
|
267
|
+
new TextEncoder().encode(secret),
|
|
268
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
269
|
+
false,
|
|
270
|
+
["sign"]
|
|
271
|
+
);
|
|
272
|
+
const sig = await crypto.subtle.sign(
|
|
273
|
+
"HMAC",
|
|
274
|
+
key,
|
|
275
|
+
new TextEncoder().encode(`khotan-cli:${timestamp2}`)
|
|
276
|
+
);
|
|
277
|
+
return bytesToHex(new Uint8Array(sig));
|
|
278
|
+
}
|
|
279
|
+
function timingSafeEqualHex(a, b) {
|
|
280
|
+
if (a.length !== b.length) return false;
|
|
281
|
+
let diff = 0;
|
|
282
|
+
for (let i = 0; i < a.length; i++) {
|
|
283
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
284
|
+
}
|
|
285
|
+
return diff === 0;
|
|
286
|
+
}
|
|
287
|
+
async function isCliRequestAuthorized(request, secret) {
|
|
288
|
+
if (process.env["NODE_ENV"] === "production") return false;
|
|
289
|
+
if (!secret) return false;
|
|
290
|
+
const header = request.headers.get("authorization");
|
|
291
|
+
if (!header?.startsWith(`${CLI_TOKEN_SCHEME} `)) return false;
|
|
292
|
+
const token = header.slice(CLI_TOKEN_SCHEME.length + 1).trim();
|
|
293
|
+
const dotIdx = token.indexOf(".");
|
|
294
|
+
if (dotIdx === -1) return false;
|
|
295
|
+
const timestamp2 = token.slice(0, dotIdx);
|
|
296
|
+
const provided = token.slice(dotIdx + 1);
|
|
297
|
+
const ts = Number.parseInt(timestamp2, 10);
|
|
298
|
+
if (!Number.isFinite(ts)) return false;
|
|
299
|
+
if (Math.abs(Date.now() - ts) > CLI_TOKEN_WINDOW_MS) return false;
|
|
300
|
+
const expected = await deriveCliToken(secret, timestamp2);
|
|
301
|
+
return timingSafeEqualHex(provided, expected);
|
|
302
|
+
}
|
|
262
303
|
function bindPlugWithVars(plug, vars, setVars) {
|
|
263
304
|
const opts = (extra) => ({
|
|
264
305
|
...extra,
|
|
@@ -1104,9 +1145,14 @@ function coerceDate(value) {
|
|
|
1104
1145
|
}
|
|
1105
1146
|
function isCronRequestAuthorized(request) {
|
|
1106
1147
|
const secret = process.env["CRON_SECRET"]?.trim();
|
|
1107
|
-
if (!secret)
|
|
1148
|
+
if (!secret) {
|
|
1149
|
+
return process.env["NODE_ENV"] !== "production";
|
|
1150
|
+
}
|
|
1108
1151
|
return request.headers.get("authorization") === `Bearer ${secret}`;
|
|
1109
1152
|
}
|
|
1153
|
+
function isDebugEnabled() {
|
|
1154
|
+
return Boolean(process?.env?.["KHOTAN_DEBUG"]) && process?.env?.["NODE_ENV"] !== "production";
|
|
1155
|
+
}
|
|
1110
1156
|
function isWorkflowCancelledError(error) {
|
|
1111
1157
|
if (!error || typeof error !== "object") return false;
|
|
1112
1158
|
const record = error;
|
|
@@ -1400,8 +1446,18 @@ function canonicalizeConnectValue(resource, connectValue) {
|
|
|
1400
1446
|
);
|
|
1401
1447
|
}
|
|
1402
1448
|
function khotan(config) {
|
|
1403
|
-
const { adapter, plugs, resources = [], caches = [] } = config;
|
|
1449
|
+
const { adapter, plugs, resources = [], caches = [], authorize } = config;
|
|
1404
1450
|
const instanceId = crypto.randomUUID();
|
|
1451
|
+
if (!authorize) {
|
|
1452
|
+
console.warn(
|
|
1453
|
+
"[khotan] No `authorize` hook configured: the management API (/api/khotan/*) is publicly accessible. Pass `authorize` to gate it behind your auth layer (e.g. better-auth). This is required for any deployed environment."
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
if (!(config.secret ?? process.env["KHOTAN_SECRET"])) {
|
|
1457
|
+
console.warn(
|
|
1458
|
+
"[khotan] No `secret`/`KHOTAN_SECRET` configured: plug credentials and wire metadata will not be encrypted at rest. Set KHOTAN_SECRET to a high-entropy value."
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1405
1461
|
const plugNames = /* @__PURE__ */ new Set();
|
|
1406
1462
|
for (const plug of plugs) {
|
|
1407
1463
|
if (plugNames.has(plug.name)) {
|
|
@@ -2228,7 +2284,24 @@ function khotan(config) {
|
|
|
2228
2284
|
const webhookEventsIdx = segments.indexOf("webhook-events");
|
|
2229
2285
|
const variablesIdx = segments.indexOf("variables");
|
|
2230
2286
|
const cronIdx = segments.indexOf("cron");
|
|
2287
|
+
const webhookIdx = segments.indexOf("webhook");
|
|
2231
2288
|
const debugIdx = segments.indexOf("debug");
|
|
2289
|
+
const isInboundWebhook = webhookIdx !== -1 && webhookIdx === segments.length - 2;
|
|
2290
|
+
const isCronRoute = cronIdx !== -1 && cronIdx === segments.length - 1;
|
|
2291
|
+
const isDebugRoute = debugIdx !== -1;
|
|
2292
|
+
if (authorize && !isInboundWebhook && !isCronRoute && !isDebugRoute) {
|
|
2293
|
+
let allowed = await isCliRequestAuthorized(request, secret);
|
|
2294
|
+
if (!allowed) {
|
|
2295
|
+
try {
|
|
2296
|
+
allowed = await authorize(request);
|
|
2297
|
+
} catch {
|
|
2298
|
+
allowed = false;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
if (!allowed) {
|
|
2302
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2232
2305
|
const limit = Math.min(
|
|
2233
2306
|
Math.max(
|
|
2234
2307
|
Number.parseInt(url.searchParams.get("limit") ?? "20", 10) || 20,
|
|
@@ -2270,15 +2343,13 @@ function khotan(config) {
|
|
|
2270
2343
|
return Response.json(result);
|
|
2271
2344
|
}
|
|
2272
2345
|
if (debugIdx !== -1 && debugIdx === segments.length - 1) {
|
|
2273
|
-
|
|
2274
|
-
if (!debugActive) {
|
|
2346
|
+
if (!isDebugEnabled()) {
|
|
2275
2347
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
2276
2348
|
}
|
|
2277
2349
|
return Response.json({ enabled: true });
|
|
2278
2350
|
}
|
|
2279
2351
|
if (debugIdx !== -1 && debugIdx === segments.length - 2) {
|
|
2280
|
-
|
|
2281
|
-
if (!debugActive) {
|
|
2352
|
+
if (!isDebugEnabled()) {
|
|
2282
2353
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
2283
2354
|
}
|
|
2284
2355
|
const plugName = segments[debugIdx + 1];
|
|
@@ -2580,7 +2651,6 @@ function khotan(config) {
|
|
|
2580
2651
|
const result = await dispatchScheduledFlows({ runType });
|
|
2581
2652
|
return Response.json(result);
|
|
2582
2653
|
}
|
|
2583
|
-
const webhookIdx = segments.indexOf("webhook");
|
|
2584
2654
|
if (webhookIdx !== -1 && webhookIdx === segments.length - 2) {
|
|
2585
2655
|
const plugName = segments[webhookIdx + 1];
|
|
2586
2656
|
const plugReg = plugs.find((p) => p.name === plugName);
|
|
@@ -2797,8 +2867,7 @@ function khotan(config) {
|
|
|
2797
2867
|
return Response.json({ received: true }, { status: 202 });
|
|
2798
2868
|
}
|
|
2799
2869
|
if (debugIdx !== -1 && debugIdx === segments.length - 2) {
|
|
2800
|
-
|
|
2801
|
-
if (!debugActive) {
|
|
2870
|
+
if (!isDebugEnabled()) {
|
|
2802
2871
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
2803
2872
|
}
|
|
2804
2873
|
const plugName = segments[debugIdx + 1];
|
|
@@ -3287,6 +3356,6 @@ function toNextJsHandler(factoryHandler) {
|
|
|
3287
3356
|
};
|
|
3288
3357
|
}
|
|
3289
3358
|
|
|
3290
|
-
export { __setWorkflowGetRunForTests, __setWorkflowGetWritableForTests, __setWorkflowStartForTests, bindWorkflowPlug, drizzleAdapter, khotan, khotanCache, khotanMappings, sendUpdate, toNextJsHandler };
|
|
3359
|
+
export { __setWorkflowGetRunForTests, __setWorkflowGetWritableForTests, __setWorkflowStartForTests, bindWorkflowPlug, deriveCliToken, drizzleAdapter, khotan, khotanCache, khotanMappings, sendUpdate, toNextJsHandler };
|
|
3291
3360
|
//# sourceMappingURL=factory.js.map
|
|
3292
3361
|
//# sourceMappingURL=factory.js.map
|