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 +34 -22
- package/dist/cli.js +88 -16
- package/dist/factory.cjs +8 -1
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +9 -1
- package/dist/factory.d.ts +9 -1
- package/dist/factory.js +8 -1
- package/dist/factory.js.map +1 -1
- package/dist/templates/catch.example.ts +25 -17
- package/dist/templates/catch.ts +20 -15
- package/dist/templates/hub.tsx +96 -13
- package/dist/templates/inflow.example.ts +46 -38
- package/dist/templates/inflow.ts +37 -31
- package/dist/templates/khotan-config.ts +16 -6
- 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/relay.example.ts +52 -44
- package/dist/templates/relay.ts +38 -33
- package/dist/templates/skill-dashboard.md +2 -1
- package/dist/templates/skill-setup.md +77 -1
- package/dist/templates/skill-webhook.md +45 -23
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# khotan-data
|
|
2
2
|
|
|
3
|
-
Data
|
|
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
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
152
|
+
const response = await shopifyPlug.get<{ data?: Array<Record<string, unknown>> }>("/products");
|
|
153
|
+
const records = Array.isArray(response.data) ? response.data : [];
|
|
145
154
|
|
|
146
|
-
|
|
155
|
+
await snapshotCache.set("latest", records);
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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, `${
|
|
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:
|
|
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,
|
|
1904
|
+
function diffObjectSchema(expected, actual, path17) {
|
|
1837
1905
|
if (actual.type !== "object") {
|
|
1838
1906
|
return [
|
|
1839
1907
|
{
|
|
1840
|
-
path:
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|