khotan-data 0.2.0 → 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 CHANGED
@@ -99,7 +99,13 @@ authorize: async (request) => {
99
99
  ```
100
100
 
101
101
  - `KHOTAN_SECRET` encrypts plug credentials **at rest** (AES-256-GCM). It is not
102
- an auth credential — it never gates requests. Set it to a high-entropy value.
102
+ an auth credential — it never gates requests, and **must not** be sent as a
103
+ `Bearer` token. Management routes are gated only by `authorize` (plus a
104
+ dev-only CLI HMAC token derived from the secret). A rejected request returns
105
+ `401` with `code: "authorize_rejected"` and a `hint`. To trigger a flow over
106
+ HTTP (`POST /api/khotan/flows/{flowId}/runs`), send a credential your
107
+ `authorize` hook accepts — or just call `khotanData.flow(name).start()` from
108
+ server code, which needs no auth. Set the secret to a high-entropy value.
103
109
  - Inbound webhooks (verified via per-plug `onVerify`), the cron dispatcher
104
110
  (`CRON_SECRET`), and debug routes (`KHOTAN_DEBUG`, non-production only) are
105
111
  exempt from `authorize` automatically.
@@ -128,32 +134,38 @@ export const shopifyProductsSnapshotCache = cache({
128
134
 
129
135
  Inside workflows, use `khotanCache(ctx, "name")` for snapshots, cursors, and dedupe markers:
130
136
 
137
+ Declare `"use step"` functions at module top level and pass them serializable
138
+ values only (`ctx` is plain data). Nesting steps inside the `"use workflow"`
139
+ function fails at runtime — the Workflow compiler cannot hoist closures that
140
+ capture workflow scope.
141
+
131
142
  ```typescript
132
143
  import { khotanCache } from "khotan-data/factory";
133
144
 
134
- async function shopifyProductsWorkflow(ctx: InflowContext) {
135
- "use workflow";
136
-
137
- async function syncProducts() {
138
- "use step";
139
- const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
140
- const previous =
141
- (await snapshotCache.get<Array<Record<string, unknown>>>("latest")) ?? [];
145
+ // Step: top-level, retried independently, full Node.js access.
146
+ async function syncProducts(ctx: InflowContext) {
147
+ "use step";
148
+ const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
149
+ const previous =
150
+ (await snapshotCache.get<Array<Record<string, unknown>>>("latest")) ?? [];
142
151
 
143
- const response = await shopifyPlug.get<{ data?: Array<Record<string, unknown>> }>("/products");
144
- const records = Array.isArray(response.data) ? response.data : [];
152
+ const response = await shopifyPlug.get<{ data?: Array<Record<string, unknown>> }>("/products");
153
+ const records = Array.isArray(response.data) ? response.data : [];
145
154
 
146
- await snapshotCache.set("latest", records);
155
+ await snapshotCache.set("latest", records);
147
156
 
148
- return {
149
- extracted: records.length,
150
- transformed: records.length,
151
- created: records.length,
152
- metadata: { previousCount: previous.length },
153
- };
154
- }
157
+ return {
158
+ extracted: records.length,
159
+ transformed: records.length,
160
+ created: records.length,
161
+ metadata: { previousCount: previous.length },
162
+ };
163
+ }
155
164
 
156
- return syncProducts();
165
+ // Workflow: orchestration only.
166
+ async function shopifyProductsWorkflow(ctx: InflowContext) {
167
+ "use workflow";
168
+ return syncProducts(ctx);
157
169
  }
158
170
  ```
159
171
 
package/dist/cli.js CHANGED
@@ -678,6 +678,15 @@ function resolveAgentsMdPaths(content, targets) {
678
678
  // src/cli/commands/init.ts
679
679
  var __dirname2 = path2.dirname(fileURLToPath(import.meta.url));
680
680
  function resolveOutputDir(projectRoot) {
681
+ const configPath = path2.join(projectRoot, "khotan.config.ts");
682
+ if (fs4.existsSync(configPath)) {
683
+ try {
684
+ const content = fs4.readFileSync(configPath, "utf-8");
685
+ const match = /outputDir:\s*["']([^"']+)["']/.exec(content);
686
+ if (match?.[1]) return match[1];
687
+ } catch {
688
+ }
689
+ }
681
690
  if (fs4.existsSync(path2.join(projectRoot, "src", "app"))) {
682
691
  return "src/khotan";
683
692
  }
@@ -728,6 +737,45 @@ function scaffoldCoreFiles(cwd, outputDir) {
728
737
  }
729
738
  return created;
730
739
  }
740
+ var MIDDLEWARE_CANDIDATES = [
741
+ "middleware.ts",
742
+ "middleware.js",
743
+ "src/middleware.ts",
744
+ "src/middleware.js",
745
+ "proxy.ts",
746
+ "proxy.js",
747
+ "src/proxy.ts",
748
+ "src/proxy.js"
749
+ ];
750
+ function warnAboutWorkflowProxy(cwd) {
751
+ const found = MIDDLEWARE_CANDIDATES.map((rel) => ({
752
+ rel,
753
+ abs: path2.join(cwd, rel)
754
+ })).find((c) => fs4.existsSync(c.abs));
755
+ if (!found) return false;
756
+ let contents = "";
757
+ try {
758
+ contents = fs4.readFileSync(found.abs, "utf-8");
759
+ } catch {
760
+ return false;
761
+ }
762
+ if (/\.well-known|workflow/i.test(contents)) {
763
+ return false;
764
+ }
765
+ console.log(
766
+ `
767
+ \u26A0 Detected ${found.rel}. Vercel Workflow (used by inflows, outflows,
768
+ relays, catch, and pass) communicates over /.well-known/workflow/*.
769
+ If your middleware/proxy matcher captures these paths, durable runs
770
+ will silently fail. Exclude them from your matcher, e.g.:
771
+
772
+ export const config = {
773
+ matcher: ["/((?!_next|.well-known/workflow).*)"],
774
+ };
775
+ `
776
+ );
777
+ return true;
778
+ }
731
779
  async function runFullSetup(cwd) {
732
780
  const results = [];
733
781
  const pm = detectPackageManager(cwd);
@@ -839,6 +887,7 @@ Installing shadcn components: ${missingShadcn.join(", ")}...`
839
887
  results.push({ name: "Scaffold core files", status: "skipped" });
840
888
  }
841
889
  results.push(ensureKhotanDataInstalled(cwd));
890
+ warnAboutWorkflowProxy(cwd);
842
891
  return results;
843
892
  }
844
893
  function ensureKhotanDataInstalled(cwd) {
@@ -867,6 +916,7 @@ async function runInit(cwd) {
867
916
  }
868
917
  scaffoldCoreFiles(cwd, outputDir);
869
918
  ensureKhotanDataInstalled(cwd);
919
+ warnAboutWorkflowProxy(cwd);
870
920
  return fs4.existsSync(configPath);
871
921
  }
872
922
  var SKILL_COMPONENTS = [
@@ -939,6 +989,7 @@ ${String(failed.length)} step(s) failed. You may need to run them manually.`
939
989
  }
940
990
  const coreFiles = scaffoldCoreFiles(cwd, outputDir);
941
991
  ensureKhotanDataInstalled(cwd);
992
+ warnAboutWorkflowProxy(cwd);
942
993
  let installSkills2 = opts.yes ?? false;
943
994
  if (!installSkills2 && process.stdin.isTTY) {
944
995
  const response = await prompts2({
package/dist/factory.cjs CHANGED
@@ -2301,7 +2301,14 @@ function khotan(config) {
2301
2301
  }
2302
2302
  }
2303
2303
  if (!allowed) {
2304
- return Response.json({ error: "Unauthorized" }, { status: 401 });
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
+ );
2305
2312
  }
2306
2313
  }
2307
2314
  const limit = Math.min(