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 +32 -20
- package/dist/cli.js +51 -0
- 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 +11 -0
- 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 +1 -1
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
|
|
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
|
@@ -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(
|
|
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(
|