khotan-data 0.2.0 → 0.3.1

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
@@ -1,8 +1,8 @@
1
1
  # khotan-data
2
2
 
3
- Data primitives for TypeScript ETL pipelines, transforms, and Drizzle Postgres integration.
3
+ Data sync, ETL, and webhook primitives for Next.js + Drizzle + Postgres. shadcn for data plumbing.
4
4
 
5
- Built for **Next.js + Drizzle + Postgres** projects. Think better-auth for data management.
5
+ Built for **Next.js + Drizzle + Postgres** projects. Think shadcn × better-auth, but for data.
6
6
 
7
7
  ## Install
8
8
 
@@ -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
@@ -46,7 +46,7 @@ function checkNpmPackages(cwd, packages) {
46
46
  ...pkgJson.dependencies,
47
47
  ...pkgJson.devDependencies
48
48
  };
49
- return packages.filter((pkg) => !(pkg in allDeps));
49
+ return packages.filter((pkg2) => !(pkg2 in allDeps));
50
50
  } catch {
51
51
  return packages;
52
52
  }
@@ -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
  }
@@ -699,11 +708,6 @@ function scaffoldCoreFiles(cwd, outputDir) {
699
708
  "templates",
700
709
  "khotan-config.ts"
701
710
  );
702
- const routeTemplatePath = path2.resolve(
703
- __dirname2,
704
- "templates",
705
- "khotan-route.ts"
706
- );
707
711
  const khotanTsPath = path2.join(path2.resolve(cwd, outputDir), "khotan.ts");
708
712
  if (!fs4.existsSync(khotanTsPath)) {
709
713
  fs4.mkdirSync(path2.dirname(khotanTsPath), { recursive: true });
@@ -721,13 +725,67 @@ function scaffoldCoreFiles(cwd, outputDir) {
721
725
  const routePath = path2.join(routeDir, "route.ts");
722
726
  if (!fs4.existsSync(routePath)) {
723
727
  fs4.mkdirSync(routeDir, { recursive: true });
724
- fs4.copyFileSync(routeTemplatePath, routePath);
728
+ const khotanImportPath = outputDir.startsWith("src/") ? `@/${outputDir.slice(4)}/khotan` : `@/${outputDir}/khotan`;
729
+ const routeContent = `// ============================================================================
730
+ // Khotan API Route \u2014 catch-all handler for /api/khotan/*
731
+ // Generated by khotan CLI \xB7 https://github.com/khotan-io/khotan-data
732
+ //
733
+ // This file wires your khotan config into a Next.js App Router route.
734
+ // ============================================================================
735
+
736
+ import { toNextJsHandler } from "khotan-data/factory";
737
+ import khotanData from "${khotanImportPath}";
738
+
739
+ export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(
740
+ khotanData.handler,
741
+ );
742
+ `;
743
+ fs4.writeFileSync(routePath, routeContent, "utf-8");
725
744
  created.push(path2.relative(cwd, routePath));
726
745
  } else {
727
746
  console.log(`\u2713 ${path2.relative(cwd, routePath)} already exists, skipping`);
728
747
  }
729
748
  return created;
730
749
  }
750
+ var MIDDLEWARE_CANDIDATES = [
751
+ "middleware.ts",
752
+ "middleware.js",
753
+ "src/middleware.ts",
754
+ "src/middleware.js",
755
+ "proxy.ts",
756
+ "proxy.js",
757
+ "src/proxy.ts",
758
+ "src/proxy.js"
759
+ ];
760
+ function warnAboutWorkflowProxy(cwd) {
761
+ const found = MIDDLEWARE_CANDIDATES.map((rel) => ({
762
+ rel,
763
+ abs: path2.join(cwd, rel)
764
+ })).find((c) => fs4.existsSync(c.abs));
765
+ if (!found) return false;
766
+ let contents = "";
767
+ try {
768
+ contents = fs4.readFileSync(found.abs, "utf-8");
769
+ } catch {
770
+ return false;
771
+ }
772
+ if (/\.well-known|workflow/i.test(contents)) {
773
+ return false;
774
+ }
775
+ console.log(
776
+ `
777
+ \u26A0 Detected ${found.rel}. Vercel Workflow (used by inflows, outflows,
778
+ relays, catch, and pass) communicates over /.well-known/workflow/*.
779
+ If your middleware/proxy matcher captures these paths, durable runs
780
+ will silently fail. Exclude them from your matcher, e.g.:
781
+
782
+ export const config = {
783
+ matcher: ["/((?!_next|.well-known/workflow).*)"],
784
+ };
785
+ `
786
+ );
787
+ return true;
788
+ }
731
789
  async function runFullSetup(cwd) {
732
790
  const results = [];
733
791
  const pm = detectPackageManager(cwd);
@@ -839,6 +897,14 @@ Installing shadcn components: ${missingShadcn.join(", ")}...`
839
897
  results.push({ name: "Scaffold core files", status: "skipped" });
840
898
  }
841
899
  results.push(ensureKhotanDataInstalled(cwd));
900
+ warnAboutWorkflowProxy(cwd);
901
+ const skillCount = scaffoldAgentSkills(cwd);
902
+ if (skillCount > 0) {
903
+ console.log(`\u2713 Installed ${String(skillCount)} agent skills`);
904
+ results.push({ name: "Install agent skills", status: "success" });
905
+ } else {
906
+ results.push({ name: "Install agent skills", status: "skipped" });
907
+ }
842
908
  return results;
843
909
  }
844
910
  function ensureKhotanDataInstalled(cwd) {
@@ -867,6 +933,7 @@ async function runInit(cwd) {
867
933
  }
868
934
  scaffoldCoreFiles(cwd, outputDir);
869
935
  ensureKhotanDataInstalled(cwd);
936
+ warnAboutWorkflowProxy(cwd);
870
937
  return fs4.existsSync(configPath);
871
938
  }
872
939
  var SKILL_COMPONENTS = [
@@ -939,6 +1006,7 @@ ${String(failed.length)} step(s) failed. You may need to run them manually.`
939
1006
  }
940
1007
  const coreFiles = scaffoldCoreFiles(cwd, outputDir);
941
1008
  ensureKhotanDataInstalled(cwd);
1009
+ warnAboutWorkflowProxy(cwd);
942
1010
  let installSkills2 = opts.yes ?? false;
943
1011
  if (!installSkills2 && process.stdin.isTTY) {
944
1012
  const response = await prompts2({
@@ -1805,13 +1873,13 @@ function diffSchemas(expected, actual, basePath = "$") {
1805
1873
  }
1806
1874
  return diffObjectSchema(expected, actual, basePath);
1807
1875
  }
1808
- function diffTypedNode(expected, actual, path16) {
1876
+ function diffTypedNode(expected, actual, path17) {
1809
1877
  const expectedType = expected["_type"];
1810
1878
  if (expectedType === "array") {
1811
1879
  if (actual.type !== "array") {
1812
1880
  return [
1813
1881
  {
1814
- path: path16,
1882
+ path: path17,
1815
1883
  issue: "type_mismatch",
1816
1884
  note: `expected array, got ${actual.type}`
1817
1885
  }
@@ -1819,13 +1887,13 @@ function diffTypedNode(expected, actual, path16) {
1819
1887
  }
1820
1888
  const itemSchema = expected["items"];
1821
1889
  if (!itemSchema || !actual.items) return [];
1822
- return diffSchemas(itemSchema, actual.items, `${path16}[]`);
1890
+ return diffSchemas(itemSchema, actual.items, `${path17}[]`);
1823
1891
  }
1824
1892
  const normalizedExpected = normalizeType(expectedType);
1825
1893
  if (normalizedExpected !== actual.type && actual.type !== "null") {
1826
1894
  return [
1827
1895
  {
1828
- path: path16,
1896
+ path: path17,
1829
1897
  issue: "type_mismatch",
1830
1898
  note: `expected ${expectedType}, got ${actual.type}`
1831
1899
  }
@@ -1833,11 +1901,11 @@ function diffTypedNode(expected, actual, path16) {
1833
1901
  }
1834
1902
  return [];
1835
1903
  }
1836
- function diffObjectSchema(expected, actual, path16) {
1904
+ function diffObjectSchema(expected, actual, path17) {
1837
1905
  if (actual.type !== "object") {
1838
1906
  return [
1839
1907
  {
1840
- path: path16,
1908
+ path: path17,
1841
1909
  issue: "type_mismatch",
1842
1910
  note: `expected object, got ${actual.type}`
1843
1911
  }
@@ -1846,7 +1914,7 @@ function diffObjectSchema(expected, actual, path16) {
1846
1914
  const mismatches = [];
1847
1915
  const actualProps = actual.properties;
1848
1916
  for (const [key, typeDesc] of Object.entries(expected)) {
1849
- const childPath = path16 === "$" ? `$.${key}` : `${path16}.${key}`;
1917
+ const childPath = path17 === "$" ? `$.${key}` : `${path17}.${key}`;
1850
1918
  const typeStr = typeof typeDesc === "string" ? typeDesc : null;
1851
1919
  const isOptional = typeStr?.endsWith("?") ?? false;
1852
1920
  if (!(key in actualProps)) {
@@ -1884,7 +1952,7 @@ function diffObjectSchema(expected, actual, path16) {
1884
1952
  }
1885
1953
  for (const key of Object.keys(actualProps)) {
1886
1954
  if (!(key in expected)) {
1887
- const childPath = path16 === "$" ? `$.${key}` : `${path16}.${key}`;
1955
+ const childPath = path17 === "$" ? `$.${key}` : `${path17}.${key}`;
1888
1956
  mismatches.push({ path: childPath, issue: "extra" });
1889
1957
  }
1890
1958
  }
@@ -2942,8 +3010,12 @@ withApiOptions2(
2942
3010
  });
2943
3011
 
2944
3012
  // src/cli/index.ts
3013
+ var __cliDirname = path2.dirname(fileURLToPath(import.meta.url));
3014
+ var pkg = JSON.parse(
3015
+ fs4.readFileSync(path2.resolve(__cliDirname, "..", "package.json"), "utf-8")
3016
+ );
2945
3017
  var program = new Command();
2946
- program.name("khotan").description("Scaffold data components into your project").version("0.0.1");
3018
+ program.name("khotan").description("Scaffold data components into your project").version(pkg.version);
2947
3019
  program.addCommand(initCommand);
2948
3020
  program.addCommand(addCommand);
2949
3021
  program.addCommand(generateCommand);
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(