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/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) return true;
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
- const debugActive = process?.env?.["KHOTAN_DEBUG"];
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
- const debugActive = process?.env?.["KHOTAN_DEBUG"];
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
- const debugActive = process?.env?.["KHOTAN_DEBUG"];
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;