khotan-data 0.1.1 → 0.3.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 +60 -19
- package/dist/cli.js +183 -46
- package/dist/factory.cjs +86 -9
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +46 -1
- package/dist/factory.d.ts +46 -1
- package/dist/factory.js +86 -10
- package/dist/factory.js.map +1 -1
- package/dist/templates/api-state.tsx +249 -0
- package/dist/templates/catch.example.ts +25 -17
- package/dist/templates/catch.ts +20 -15
- package/dist/templates/debug-index-page.tsx +56 -36
- package/dist/templates/hub.tsx +105 -36
- package/dist/templates/inflow.example.ts +46 -38
- package/dist/templates/inflow.ts +37 -31
- package/dist/templates/khotan-config.ts +28 -0
- package/dist/templates/mapping-browser.tsx +56 -44
- package/dist/templates/outflow.example.ts +39 -31
- package/dist/templates/outflow.ts +28 -23
- package/dist/templates/pass.example.ts +38 -30
- package/dist/templates/pass.ts +29 -24
- package/dist/templates/plug-debugger.tsx +15 -7
- package/dist/templates/relay.example.ts +52 -44
- package/dist/templates/relay.ts +38 -33
- package/dist/templates/runs-table.tsx +133 -130
- package/dist/templates/skill-dashboard.md +2 -1
- package/dist/templates/skill-setup.md +113 -2
- package/dist/templates/skill-webhook.md +45 -23
- 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.cjs
CHANGED
|
@@ -261,6 +261,47 @@ function hexToBytes(hex) {
|
|
|
261
261
|
function bytesToHex(bytes) {
|
|
262
262
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
263
263
|
}
|
|
264
|
+
var CLI_TOKEN_SCHEME = "KhotanCLI";
|
|
265
|
+
var CLI_TOKEN_WINDOW_MS = 6e4;
|
|
266
|
+
async function deriveCliToken(secret, timestamp2) {
|
|
267
|
+
const key = await crypto.subtle.importKey(
|
|
268
|
+
"raw",
|
|
269
|
+
new TextEncoder().encode(secret),
|
|
270
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
271
|
+
false,
|
|
272
|
+
["sign"]
|
|
273
|
+
);
|
|
274
|
+
const sig = await crypto.subtle.sign(
|
|
275
|
+
"HMAC",
|
|
276
|
+
key,
|
|
277
|
+
new TextEncoder().encode(`khotan-cli:${timestamp2}`)
|
|
278
|
+
);
|
|
279
|
+
return bytesToHex(new Uint8Array(sig));
|
|
280
|
+
}
|
|
281
|
+
function timingSafeEqualHex(a, b) {
|
|
282
|
+
if (a.length !== b.length) return false;
|
|
283
|
+
let diff = 0;
|
|
284
|
+
for (let i = 0; i < a.length; i++) {
|
|
285
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
286
|
+
}
|
|
287
|
+
return diff === 0;
|
|
288
|
+
}
|
|
289
|
+
async function isCliRequestAuthorized(request, secret) {
|
|
290
|
+
if (process.env["NODE_ENV"] === "production") return false;
|
|
291
|
+
if (!secret) return false;
|
|
292
|
+
const header = request.headers.get("authorization");
|
|
293
|
+
if (!header?.startsWith(`${CLI_TOKEN_SCHEME} `)) return false;
|
|
294
|
+
const token = header.slice(CLI_TOKEN_SCHEME.length + 1).trim();
|
|
295
|
+
const dotIdx = token.indexOf(".");
|
|
296
|
+
if (dotIdx === -1) return false;
|
|
297
|
+
const timestamp2 = token.slice(0, dotIdx);
|
|
298
|
+
const provided = token.slice(dotIdx + 1);
|
|
299
|
+
const ts = Number.parseInt(timestamp2, 10);
|
|
300
|
+
if (!Number.isFinite(ts)) return false;
|
|
301
|
+
if (Math.abs(Date.now() - ts) > CLI_TOKEN_WINDOW_MS) return false;
|
|
302
|
+
const expected = await deriveCliToken(secret, timestamp2);
|
|
303
|
+
return timingSafeEqualHex(provided, expected);
|
|
304
|
+
}
|
|
264
305
|
function bindPlugWithVars(plug, vars, setVars) {
|
|
265
306
|
const opts = (extra) => ({
|
|
266
307
|
...extra,
|
|
@@ -1106,9 +1147,14 @@ function coerceDate(value) {
|
|
|
1106
1147
|
}
|
|
1107
1148
|
function isCronRequestAuthorized(request) {
|
|
1108
1149
|
const secret = process.env["CRON_SECRET"]?.trim();
|
|
1109
|
-
if (!secret)
|
|
1150
|
+
if (!secret) {
|
|
1151
|
+
return process.env["NODE_ENV"] !== "production";
|
|
1152
|
+
}
|
|
1110
1153
|
return request.headers.get("authorization") === `Bearer ${secret}`;
|
|
1111
1154
|
}
|
|
1155
|
+
function isDebugEnabled() {
|
|
1156
|
+
return Boolean(process?.env?.["KHOTAN_DEBUG"]) && process?.env?.["NODE_ENV"] !== "production";
|
|
1157
|
+
}
|
|
1112
1158
|
function isWorkflowCancelledError(error) {
|
|
1113
1159
|
if (!error || typeof error !== "object") return false;
|
|
1114
1160
|
const record = error;
|
|
@@ -1402,8 +1448,18 @@ function canonicalizeConnectValue(resource, connectValue) {
|
|
|
1402
1448
|
);
|
|
1403
1449
|
}
|
|
1404
1450
|
function khotan(config) {
|
|
1405
|
-
const { adapter, plugs, resources = [], caches = [] } = config;
|
|
1451
|
+
const { adapter, plugs, resources = [], caches = [], authorize } = config;
|
|
1406
1452
|
const instanceId = crypto.randomUUID();
|
|
1453
|
+
if (!authorize) {
|
|
1454
|
+
console.warn(
|
|
1455
|
+
"[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."
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
if (!(config.secret ?? process.env["KHOTAN_SECRET"])) {
|
|
1459
|
+
console.warn(
|
|
1460
|
+
"[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."
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1407
1463
|
const plugNames = /* @__PURE__ */ new Set();
|
|
1408
1464
|
for (const plug of plugs) {
|
|
1409
1465
|
if (plugNames.has(plug.name)) {
|
|
@@ -2230,7 +2286,31 @@ function khotan(config) {
|
|
|
2230
2286
|
const webhookEventsIdx = segments.indexOf("webhook-events");
|
|
2231
2287
|
const variablesIdx = segments.indexOf("variables");
|
|
2232
2288
|
const cronIdx = segments.indexOf("cron");
|
|
2289
|
+
const webhookIdx = segments.indexOf("webhook");
|
|
2233
2290
|
const debugIdx = segments.indexOf("debug");
|
|
2291
|
+
const isInboundWebhook = webhookIdx !== -1 && webhookIdx === segments.length - 2;
|
|
2292
|
+
const isCronRoute = cronIdx !== -1 && cronIdx === segments.length - 1;
|
|
2293
|
+
const isDebugRoute = debugIdx !== -1;
|
|
2294
|
+
if (authorize && !isInboundWebhook && !isCronRoute && !isDebugRoute) {
|
|
2295
|
+
let allowed = await isCliRequestAuthorized(request, secret);
|
|
2296
|
+
if (!allowed) {
|
|
2297
|
+
try {
|
|
2298
|
+
allowed = await authorize(request);
|
|
2299
|
+
} catch {
|
|
2300
|
+
allowed = false;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
if (!allowed) {
|
|
2304
|
+
return Response.json(
|
|
2305
|
+
{
|
|
2306
|
+
error: "Unauthorized",
|
|
2307
|
+
code: "authorize_rejected",
|
|
2308
|
+
hint: "Management routes (/api/khotan/*) require your `authorize` hook to pass. KHOTAN_SECRET is an encryption key, not an HTTP credential \u2014 sending it as a Bearer token will not authenticate the request. To trigger a flow: call khotanData.flow(name).start() from server code (no HTTP/auth needed), or send a credential your authorize hook accepts (e.g. a session cookie or your own token). The khotan CLI authenticates automatically via a dev-only token derived from KHOTAN_SECRET."
|
|
2309
|
+
},
|
|
2310
|
+
{ status: 401 }
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2234
2314
|
const limit = Math.min(
|
|
2235
2315
|
Math.max(
|
|
2236
2316
|
Number.parseInt(url.searchParams.get("limit") ?? "20", 10) || 20,
|
|
@@ -2272,15 +2352,13 @@ function khotan(config) {
|
|
|
2272
2352
|
return Response.json(result);
|
|
2273
2353
|
}
|
|
2274
2354
|
if (debugIdx !== -1 && debugIdx === segments.length - 1) {
|
|
2275
|
-
|
|
2276
|
-
if (!debugActive) {
|
|
2355
|
+
if (!isDebugEnabled()) {
|
|
2277
2356
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
2278
2357
|
}
|
|
2279
2358
|
return Response.json({ enabled: true });
|
|
2280
2359
|
}
|
|
2281
2360
|
if (debugIdx !== -1 && debugIdx === segments.length - 2) {
|
|
2282
|
-
|
|
2283
|
-
if (!debugActive) {
|
|
2361
|
+
if (!isDebugEnabled()) {
|
|
2284
2362
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
2285
2363
|
}
|
|
2286
2364
|
const plugName = segments[debugIdx + 1];
|
|
@@ -2582,7 +2660,6 @@ function khotan(config) {
|
|
|
2582
2660
|
const result = await dispatchScheduledFlows({ runType });
|
|
2583
2661
|
return Response.json(result);
|
|
2584
2662
|
}
|
|
2585
|
-
const webhookIdx = segments.indexOf("webhook");
|
|
2586
2663
|
if (webhookIdx !== -1 && webhookIdx === segments.length - 2) {
|
|
2587
2664
|
const plugName = segments[webhookIdx + 1];
|
|
2588
2665
|
const plugReg = plugs.find((p) => p.name === plugName);
|
|
@@ -2799,8 +2876,7 @@ function khotan(config) {
|
|
|
2799
2876
|
return Response.json({ received: true }, { status: 202 });
|
|
2800
2877
|
}
|
|
2801
2878
|
if (debugIdx !== -1 && debugIdx === segments.length - 2) {
|
|
2802
|
-
|
|
2803
|
-
if (!debugActive) {
|
|
2879
|
+
if (!isDebugEnabled()) {
|
|
2804
2880
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
2805
2881
|
}
|
|
2806
2882
|
const plugName = segments[debugIdx + 1];
|
|
@@ -3293,6 +3369,7 @@ exports.__setWorkflowGetRunForTests = __setWorkflowGetRunForTests;
|
|
|
3293
3369
|
exports.__setWorkflowGetWritableForTests = __setWorkflowGetWritableForTests;
|
|
3294
3370
|
exports.__setWorkflowStartForTests = __setWorkflowStartForTests;
|
|
3295
3371
|
exports.bindWorkflowPlug = bindWorkflowPlug;
|
|
3372
|
+
exports.deriveCliToken = deriveCliToken;
|
|
3296
3373
|
exports.drizzleAdapter = drizzleAdapter;
|
|
3297
3374
|
exports.khotan = khotan;
|
|
3298
3375
|
exports.khotanCache = khotanCache;
|