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/README.md +60 -19
- package/dist/cli.js +183 -46
- package/dist/factory.cjs +86 -9
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +46 -1
- package/dist/factory.d.ts +46 -1
- package/dist/factory.js +86 -10
- package/dist/factory.js.map +1 -1
- package/dist/templates/api-state.tsx +249 -0
- package/dist/templates/catch.example.ts +25 -17
- package/dist/templates/catch.ts +20 -15
- package/dist/templates/debug-index-page.tsx +56 -36
- package/dist/templates/hub.tsx +105 -36
- package/dist/templates/inflow.example.ts +46 -38
- package/dist/templates/inflow.ts +37 -31
- package/dist/templates/khotan-config.ts +28 -0
- package/dist/templates/mapping-browser.tsx +56 -44
- 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/plug-debugger.tsx +15 -7
- package/dist/templates/relay.example.ts +52 -44
- package/dist/templates/relay.ts +38 -33
- package/dist/templates/runs-table.tsx +133 -130
- package/dist/templates/skill-dashboard.md +2 -1
- package/dist/templates/skill-setup.md +113 -2
- package/dist/templates/skill-webhook.md +45 -23
- package/dist/templates/topology-canvas.tsx +19 -30
- package/dist/templates/var-panel.tsx +33 -10
- package/dist/templates/webhook-events-table.tsx +105 -102
- package/dist/templates/wire-panel.tsx +30 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,11 @@ import { shopifyProductsSnapshotCache } from "@/lib/khotan/caches/shopify-produc
|
|
|
53
53
|
|
|
54
54
|
const khotanData = khotan({
|
|
55
55
|
adapter: drizzleAdapter(db),
|
|
56
|
+
// Gate the management API behind your auth layer (see "Security" below).
|
|
57
|
+
authorize: async (request) => {
|
|
58
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
59
|
+
return Boolean(session?.user);
|
|
60
|
+
},
|
|
56
61
|
resources: [
|
|
57
62
|
{ name: "products", mapping: { connectField: "sku" } },
|
|
58
63
|
],
|
|
@@ -79,6 +84,36 @@ await khotanData.flow("products-inflow", { plugName: "shopify" }).start({
|
|
|
79
84
|
});
|
|
80
85
|
```
|
|
81
86
|
|
|
87
|
+
## Security
|
|
88
|
+
|
|
89
|
+
The management API (`/api/khotan/*`) exposes plug credentials and operational
|
|
90
|
+
controls. It is **public unless you gate it**. Pass an `authorize` hook — it
|
|
91
|
+
receives the raw `Request` and returns `true`/`false`, so it composes directly
|
|
92
|
+
with session libraries like better-auth:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
authorize: async (request) => {
|
|
96
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
97
|
+
return session?.user?.role === "admin";
|
|
98
|
+
},
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `KHOTAN_SECRET` encrypts plug credentials **at rest** (AES-256-GCM). It is not
|
|
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.
|
|
109
|
+
- Inbound webhooks (verified via per-plug `onVerify`), the cron dispatcher
|
|
110
|
+
(`CRON_SECRET`), and debug routes (`KHOTAN_DEBUG`, non-production only) are
|
|
111
|
+
exempt from `authorize` automatically.
|
|
112
|
+
- `KHOTAN_DEBUG` is force-disabled when `NODE_ENV=production`. The cron route
|
|
113
|
+
fails closed in production when `CRON_SECRET` is unset.
|
|
114
|
+
- Protect the Hub dashboard page (e.g. `/config`) with your app's middleware —
|
|
115
|
+
`authorize` only guards the API.
|
|
116
|
+
|
|
82
117
|
## Caches
|
|
83
118
|
|
|
84
119
|
Use first-class caches when a flow, relay, catch, or pass needs durable state between runs.
|
|
@@ -99,32 +134,38 @@ export const shopifyProductsSnapshotCache = cache({
|
|
|
99
134
|
|
|
100
135
|
Inside workflows, use `khotanCache(ctx, "name")` for snapshots, cursors, and dedupe markers:
|
|
101
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
|
+
|
|
102
142
|
```typescript
|
|
103
143
|
import { khotanCache } from "khotan-data/factory";
|
|
104
144
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const previous =
|
|
112
|
-
(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")) ?? [];
|
|
113
151
|
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
const response = await shopifyPlug.get<{ data?: Array<Record<string, unknown>> }>("/products");
|
|
153
|
+
const records = Array.isArray(response.data) ? response.data : [];
|
|
116
154
|
|
|
117
|
-
|
|
155
|
+
await snapshotCache.set("latest", records);
|
|
118
156
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
157
|
+
return {
|
|
158
|
+
extracted: records.length,
|
|
159
|
+
transformed: records.length,
|
|
160
|
+
created: records.length,
|
|
161
|
+
metadata: { previousCount: previous.length },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
126
164
|
|
|
127
|
-
|
|
165
|
+
// Workflow: orchestration only.
|
|
166
|
+
async function shopifyProductsWorkflow(ctx: InflowContext) {
|
|
167
|
+
"use workflow";
|
|
168
|
+
return syncProducts(ctx);
|
|
128
169
|
}
|
|
129
170
|
```
|
|
130
171
|
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import path2 from 'path';
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
7
|
import prompts2 from 'prompts';
|
|
8
|
+
import crypto from 'crypto';
|
|
8
9
|
|
|
9
10
|
// src/cli/config-template.ts
|
|
10
11
|
function configTemplate(outputDir) {
|
|
@@ -152,6 +153,11 @@ var COMPONENTS = {
|
|
|
152
153
|
outputFile: "wires/wire.ts",
|
|
153
154
|
outputBase: "outputDir"
|
|
154
155
|
},
|
|
156
|
+
{
|
|
157
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
158
|
+
outputFile: "api-state.tsx",
|
|
159
|
+
outputBase: "components"
|
|
160
|
+
},
|
|
155
161
|
{
|
|
156
162
|
templatePath: path2.resolve(__dirname$1, "templates", "wire-panel.tsx"),
|
|
157
163
|
outputFile: "wire.tsx",
|
|
@@ -202,6 +208,11 @@ var COMPONENTS = {
|
|
|
202
208
|
]
|
|
203
209
|
},
|
|
204
210
|
files: [
|
|
211
|
+
{
|
|
212
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
213
|
+
outputFile: "api-state.tsx",
|
|
214
|
+
outputBase: "components"
|
|
215
|
+
},
|
|
205
216
|
{
|
|
206
217
|
templatePath: path2.resolve(__dirname$1, "templates", "hub.tsx"),
|
|
207
218
|
outputFile: "hub.tsx",
|
|
@@ -227,6 +238,11 @@ var COMPONENTS = {
|
|
|
227
238
|
shadcnComponents: ["card", "table", "badge", "button"]
|
|
228
239
|
},
|
|
229
240
|
files: [
|
|
241
|
+
{
|
|
242
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
243
|
+
outputFile: "api-state.tsx",
|
|
244
|
+
outputBase: "components"
|
|
245
|
+
},
|
|
230
246
|
{
|
|
231
247
|
templatePath: path2.resolve(__dirname$1, "templates", "logs.tsx"),
|
|
232
248
|
outputFile: "logs.tsx",
|
|
@@ -256,6 +272,11 @@ var COMPONENTS = {
|
|
|
256
272
|
shadcnComponents: ["card", "table", "button", "input", "label"]
|
|
257
273
|
},
|
|
258
274
|
files: [
|
|
275
|
+
{
|
|
276
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
277
|
+
outputFile: "api-state.tsx",
|
|
278
|
+
outputBase: "components"
|
|
279
|
+
},
|
|
259
280
|
{
|
|
260
281
|
templatePath: path2.resolve(
|
|
261
282
|
__dirname$1,
|
|
@@ -276,6 +297,11 @@ var COMPONENTS = {
|
|
|
276
297
|
shadcnComponents: ["card", "badge", "button", "input", "label"]
|
|
277
298
|
},
|
|
278
299
|
files: [
|
|
300
|
+
{
|
|
301
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
302
|
+
outputFile: "api-state.tsx",
|
|
303
|
+
outputBase: "components"
|
|
304
|
+
},
|
|
279
305
|
{
|
|
280
306
|
templatePath: path2.resolve(__dirname$1, "templates", "plug-debugger.tsx"),
|
|
281
307
|
outputFile: "plug-debugger.tsx",
|
|
@@ -523,6 +549,11 @@ var BLOCKS = {
|
|
|
523
549
|
shadcnComponents: ["card", "badge"]
|
|
524
550
|
},
|
|
525
551
|
files: [
|
|
552
|
+
{
|
|
553
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
554
|
+
outputFile: "api-state.tsx",
|
|
555
|
+
outputBase: "components"
|
|
556
|
+
},
|
|
526
557
|
{
|
|
527
558
|
templatePath: path2.resolve(
|
|
528
559
|
__dirname$1,
|
|
@@ -647,6 +678,15 @@ function resolveAgentsMdPaths(content, targets) {
|
|
|
647
678
|
// src/cli/commands/init.ts
|
|
648
679
|
var __dirname2 = path2.dirname(fileURLToPath(import.meta.url));
|
|
649
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
|
+
}
|
|
650
690
|
if (fs4.existsSync(path2.join(projectRoot, "src", "app"))) {
|
|
651
691
|
return "src/khotan";
|
|
652
692
|
}
|
|
@@ -697,6 +737,45 @@ function scaffoldCoreFiles(cwd, outputDir) {
|
|
|
697
737
|
}
|
|
698
738
|
return created;
|
|
699
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
|
+
}
|
|
700
779
|
async function runFullSetup(cwd) {
|
|
701
780
|
const results = [];
|
|
702
781
|
const pm = detectPackageManager(cwd);
|
|
@@ -808,6 +887,7 @@ Installing shadcn components: ${missingShadcn.join(", ")}...`
|
|
|
808
887
|
results.push({ name: "Scaffold core files", status: "skipped" });
|
|
809
888
|
}
|
|
810
889
|
results.push(ensureKhotanDataInstalled(cwd));
|
|
890
|
+
warnAboutWorkflowProxy(cwd);
|
|
811
891
|
return results;
|
|
812
892
|
}
|
|
813
893
|
function ensureKhotanDataInstalled(cwd) {
|
|
@@ -836,6 +916,7 @@ async function runInit(cwd) {
|
|
|
836
916
|
}
|
|
837
917
|
scaffoldCoreFiles(cwd, outputDir);
|
|
838
918
|
ensureKhotanDataInstalled(cwd);
|
|
919
|
+
warnAboutWorkflowProxy(cwd);
|
|
839
920
|
return fs4.existsSync(configPath);
|
|
840
921
|
}
|
|
841
922
|
var SKILL_COMPONENTS = [
|
|
@@ -908,6 +989,7 @@ ${String(failed.length)} step(s) failed. You may need to run them manually.`
|
|
|
908
989
|
}
|
|
909
990
|
const coreFiles = scaffoldCoreFiles(cwd, outputDir);
|
|
910
991
|
ensureKhotanDataInstalled(cwd);
|
|
992
|
+
warnAboutWorkflowProxy(cwd);
|
|
911
993
|
let installSkills2 = opts.yes ?? false;
|
|
912
994
|
if (!installSkills2 && process.stdin.isTTY) {
|
|
913
995
|
const response = await prompts2({
|
|
@@ -1774,13 +1856,13 @@ function diffSchemas(expected, actual, basePath = "$") {
|
|
|
1774
1856
|
}
|
|
1775
1857
|
return diffObjectSchema(expected, actual, basePath);
|
|
1776
1858
|
}
|
|
1777
|
-
function diffTypedNode(expected, actual,
|
|
1859
|
+
function diffTypedNode(expected, actual, path16) {
|
|
1778
1860
|
const expectedType = expected["_type"];
|
|
1779
1861
|
if (expectedType === "array") {
|
|
1780
1862
|
if (actual.type !== "array") {
|
|
1781
1863
|
return [
|
|
1782
1864
|
{
|
|
1783
|
-
path:
|
|
1865
|
+
path: path16,
|
|
1784
1866
|
issue: "type_mismatch",
|
|
1785
1867
|
note: `expected array, got ${actual.type}`
|
|
1786
1868
|
}
|
|
@@ -1788,13 +1870,13 @@ function diffTypedNode(expected, actual, path15) {
|
|
|
1788
1870
|
}
|
|
1789
1871
|
const itemSchema = expected["items"];
|
|
1790
1872
|
if (!itemSchema || !actual.items) return [];
|
|
1791
|
-
return diffSchemas(itemSchema, actual.items, `${
|
|
1873
|
+
return diffSchemas(itemSchema, actual.items, `${path16}[]`);
|
|
1792
1874
|
}
|
|
1793
1875
|
const normalizedExpected = normalizeType(expectedType);
|
|
1794
1876
|
if (normalizedExpected !== actual.type && actual.type !== "null") {
|
|
1795
1877
|
return [
|
|
1796
1878
|
{
|
|
1797
|
-
path:
|
|
1879
|
+
path: path16,
|
|
1798
1880
|
issue: "type_mismatch",
|
|
1799
1881
|
note: `expected ${expectedType}, got ${actual.type}`
|
|
1800
1882
|
}
|
|
@@ -1802,11 +1884,11 @@ function diffTypedNode(expected, actual, path15) {
|
|
|
1802
1884
|
}
|
|
1803
1885
|
return [];
|
|
1804
1886
|
}
|
|
1805
|
-
function diffObjectSchema(expected, actual,
|
|
1887
|
+
function diffObjectSchema(expected, actual, path16) {
|
|
1806
1888
|
if (actual.type !== "object") {
|
|
1807
1889
|
return [
|
|
1808
1890
|
{
|
|
1809
|
-
path:
|
|
1891
|
+
path: path16,
|
|
1810
1892
|
issue: "type_mismatch",
|
|
1811
1893
|
note: `expected object, got ${actual.type}`
|
|
1812
1894
|
}
|
|
@@ -1815,7 +1897,7 @@ function diffObjectSchema(expected, actual, path15) {
|
|
|
1815
1897
|
const mismatches = [];
|
|
1816
1898
|
const actualProps = actual.properties;
|
|
1817
1899
|
for (const [key, typeDesc] of Object.entries(expected)) {
|
|
1818
|
-
const childPath =
|
|
1900
|
+
const childPath = path16 === "$" ? `$.${key}` : `${path16}.${key}`;
|
|
1819
1901
|
const typeStr = typeof typeDesc === "string" ? typeDesc : null;
|
|
1820
1902
|
const isOptional = typeStr?.endsWith("?") ?? false;
|
|
1821
1903
|
if (!(key in actualProps)) {
|
|
@@ -1853,7 +1935,7 @@ function diffObjectSchema(expected, actual, path15) {
|
|
|
1853
1935
|
}
|
|
1854
1936
|
for (const key of Object.keys(actualProps)) {
|
|
1855
1937
|
if (!(key in expected)) {
|
|
1856
|
-
const childPath =
|
|
1938
|
+
const childPath = path16 === "$" ? `$.${key}` : `${path16}.${key}`;
|
|
1857
1939
|
mismatches.push({ path: childPath, issue: "extra" });
|
|
1858
1940
|
}
|
|
1859
1941
|
}
|
|
@@ -1874,6 +1956,48 @@ function normalizeType(typeStr) {
|
|
|
1874
1956
|
return lower;
|
|
1875
1957
|
}
|
|
1876
1958
|
}
|
|
1959
|
+
var CLI_TOKEN_SCHEME = "KhotanCLI";
|
|
1960
|
+
function readSecretFromEnvFile(filePath) {
|
|
1961
|
+
try {
|
|
1962
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1963
|
+
for (const line of content.split("\n")) {
|
|
1964
|
+
const trimmed = line.trim();
|
|
1965
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1966
|
+
const match = /^KHOTAN_SECRET\s*=\s*["']?(.*?)["']?\s*$/.exec(trimmed);
|
|
1967
|
+
if (match) return match[1] ?? null;
|
|
1968
|
+
}
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
function resolveKhotanSecret(cwd = process.cwd()) {
|
|
1974
|
+
const fromEnv = process.env["KHOTAN_SECRET"]?.trim();
|
|
1975
|
+
if (fromEnv) return fromEnv;
|
|
1976
|
+
return readSecretFromEnvFile(path2.join(cwd, ".env.local")) ?? readSecretFromEnvFile(path2.join(cwd, ".env")) ?? null;
|
|
1977
|
+
}
|
|
1978
|
+
function cliAuthHeader() {
|
|
1979
|
+
const secret = resolveKhotanSecret();
|
|
1980
|
+
if (!secret) return null;
|
|
1981
|
+
const timestamp = String(Date.now());
|
|
1982
|
+
const sig = crypto.createHmac("sha256", secret).update(`khotan-cli:${timestamp}`).digest("hex");
|
|
1983
|
+
return `${CLI_TOKEN_SCHEME} ${timestamp}.${sig}`;
|
|
1984
|
+
}
|
|
1985
|
+
function cliFetch(url, init) {
|
|
1986
|
+
const auth = cliAuthHeader();
|
|
1987
|
+
if (!auth) {
|
|
1988
|
+
return init === void 0 ? fetch(url) : fetch(url, init);
|
|
1989
|
+
}
|
|
1990
|
+
const headers = {
|
|
1991
|
+
...init?.headers ?? {},
|
|
1992
|
+
Authorization: auth
|
|
1993
|
+
};
|
|
1994
|
+
return fetch(url, { ...init, headers });
|
|
1995
|
+
}
|
|
1996
|
+
function unauthorizedHint() {
|
|
1997
|
+
return resolveKhotanSecret() ? "Unauthorized. The dev server rejected the CLI token \u2014 confirm KHOTAN_SECRET matches the value the server is running with." : "Unauthorized. Set KHOTAN_SECRET in your environment (or .env.local) so the CLI can authenticate against your authorize() hook.";
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// src/cli/commands/plug-vars.ts
|
|
1877
2001
|
function output(obj) {
|
|
1878
2002
|
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
1879
2003
|
}
|
|
@@ -1911,13 +2035,14 @@ function resolvePort(portFlag) {
|
|
|
1911
2035
|
async function checkConnectivity(baseUrl) {
|
|
1912
2036
|
let res;
|
|
1913
2037
|
try {
|
|
1914
|
-
res = await
|
|
2038
|
+
res = await cliFetch(`${baseUrl}/plugs`);
|
|
1915
2039
|
} catch {
|
|
1916
2040
|
fail(
|
|
1917
2041
|
"connect_failed",
|
|
1918
2042
|
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
1919
2043
|
);
|
|
1920
2044
|
}
|
|
2045
|
+
if (res.status === 401) fail("unauthorized", unauthorizedHint());
|
|
1921
2046
|
if (!res.ok) {
|
|
1922
2047
|
fail(
|
|
1923
2048
|
"api_unavailable",
|
|
@@ -1931,11 +2056,11 @@ var varsCommand = new Command("vars").description("View and manage plug variable
|
|
|
1931
2056
|
const baseUrl = `http://localhost:${port}${opts.basePath}`;
|
|
1932
2057
|
await checkConnectivity(baseUrl);
|
|
1933
2058
|
if (opts.list) {
|
|
1934
|
-
const plugsRes = await
|
|
2059
|
+
const plugsRes = await cliFetch(`${baseUrl}/plugs`);
|
|
1935
2060
|
const plugs = await plugsRes.json();
|
|
1936
2061
|
const variables = await Promise.all(
|
|
1937
2062
|
plugs.map(async (plug) => {
|
|
1938
|
-
const res = await
|
|
2063
|
+
const res = await cliFetch(`${baseUrl}/variables/${plug.name}`);
|
|
1939
2064
|
if (res.status === 404) {
|
|
1940
2065
|
return {
|
|
1941
2066
|
plugName: plug.name,
|
|
@@ -1964,7 +2089,7 @@ var varsCommand = new Command("vars").description("View and manage plug variable
|
|
|
1964
2089
|
}
|
|
1965
2090
|
const resolvedAction = action ?? "show";
|
|
1966
2091
|
if (resolvedAction === "show") {
|
|
1967
|
-
const res = await
|
|
2092
|
+
const res = await cliFetch(`${baseUrl}/variables/${plugName}`);
|
|
1968
2093
|
if (res.status === 404) {
|
|
1969
2094
|
fail("plug_not_found", `Plug "${plugName}" not found.`);
|
|
1970
2095
|
}
|
|
@@ -1991,7 +2116,7 @@ var varsCommand = new Command("vars").description("View and manage plug variable
|
|
|
1991
2116
|
} catch {
|
|
1992
2117
|
fail("invalid_json", "Could not parse --json as JSON.");
|
|
1993
2118
|
}
|
|
1994
|
-
const res = await
|
|
2119
|
+
const res = await cliFetch(`${baseUrl}/variables/${plugName}`, {
|
|
1995
2120
|
method: "POST",
|
|
1996
2121
|
headers: { "Content-Type": "application/json" },
|
|
1997
2122
|
body: JSON.stringify(payload)
|
|
@@ -2007,7 +2132,7 @@ var varsCommand = new Command("vars").description("View and manage plug variable
|
|
|
2007
2132
|
return;
|
|
2008
2133
|
}
|
|
2009
2134
|
if (resolvedAction === "clear") {
|
|
2010
|
-
const res = await
|
|
2135
|
+
const res = await cliFetch(`${baseUrl}/variables/${plugName}`, {
|
|
2011
2136
|
method: "DELETE"
|
|
2012
2137
|
});
|
|
2013
2138
|
if (!res.ok && res.status !== 204) {
|
|
@@ -2075,7 +2200,7 @@ function tryParseJson(value) {
|
|
|
2075
2200
|
async function checkConnectivity2(baseUrl) {
|
|
2076
2201
|
let res;
|
|
2077
2202
|
try {
|
|
2078
|
-
res = await
|
|
2203
|
+
res = await cliFetch(`${baseUrl}/debug`);
|
|
2079
2204
|
} catch {
|
|
2080
2205
|
fail2(
|
|
2081
2206
|
"connect_failed",
|
|
@@ -2098,7 +2223,8 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2098
2223
|
if (opts.list) {
|
|
2099
2224
|
await checkConnectivity2(baseUrl);
|
|
2100
2225
|
try {
|
|
2101
|
-
const res = await
|
|
2226
|
+
const res = await cliFetch(`${baseUrl}/plugs`);
|
|
2227
|
+
if (res.status === 401) fail2("unauthorized", unauthorizedHint());
|
|
2102
2228
|
const data = await res.json();
|
|
2103
2229
|
const raw = Array.isArray(data) ? data : data.plugs ?? [];
|
|
2104
2230
|
const plugs = raw.map((p) => ({
|
|
@@ -2122,7 +2248,7 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2122
2248
|
await checkConnectivity2(baseUrl);
|
|
2123
2249
|
if (opts.info) {
|
|
2124
2250
|
try {
|
|
2125
|
-
const res = await
|
|
2251
|
+
const res = await cliFetch(`${baseUrl}/debug/${plugName}`);
|
|
2126
2252
|
if (res.status === 404) {
|
|
2127
2253
|
fail2(
|
|
2128
2254
|
"plug_not_found",
|
|
@@ -2141,7 +2267,7 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2141
2267
|
let allEndpoints = null;
|
|
2142
2268
|
if (opts.endpoint) {
|
|
2143
2269
|
try {
|
|
2144
|
-
const res = await
|
|
2270
|
+
const res = await cliFetch(`${baseUrl}/debug/${plugName}`);
|
|
2145
2271
|
if (res.status === 404) {
|
|
2146
2272
|
fail2(
|
|
2147
2273
|
"plug_not_found",
|
|
@@ -2205,7 +2331,7 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2205
2331
|
if (extraHeaders) debugPayload["headers"] = extraHeaders;
|
|
2206
2332
|
let debugRes;
|
|
2207
2333
|
try {
|
|
2208
|
-
debugRes = await
|
|
2334
|
+
debugRes = await cliFetch(`${baseUrl}/debug/${plugName}`, {
|
|
2209
2335
|
method: "POST",
|
|
2210
2336
|
headers: { "Content-Type": "application/json" },
|
|
2211
2337
|
body: JSON.stringify(debugPayload)
|
|
@@ -2252,7 +2378,7 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2252
2378
|
} else {
|
|
2253
2379
|
if (!allEndpoints) {
|
|
2254
2380
|
try {
|
|
2255
|
-
const metaRes = await
|
|
2381
|
+
const metaRes = await cliFetch(`${baseUrl}/debug/${plugName}`);
|
|
2256
2382
|
const metaData = await metaRes.json();
|
|
2257
2383
|
allEndpoints = metaData.endpoints ?? null;
|
|
2258
2384
|
} catch {
|
|
@@ -2327,13 +2453,14 @@ function resolveWebhookOrigin(originFlag) {
|
|
|
2327
2453
|
async function checkConnectivity3(baseUrl) {
|
|
2328
2454
|
let res;
|
|
2329
2455
|
try {
|
|
2330
|
-
res = await
|
|
2456
|
+
res = await cliFetch(`${baseUrl}/plugs`);
|
|
2331
2457
|
} catch {
|
|
2332
2458
|
fail3(
|
|
2333
2459
|
"connect_failed",
|
|
2334
2460
|
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2335
2461
|
);
|
|
2336
2462
|
}
|
|
2463
|
+
if (res.status === 401) fail3("unauthorized", unauthorizedHint());
|
|
2337
2464
|
if (!res.ok) {
|
|
2338
2465
|
fail3(
|
|
2339
2466
|
"api_unavailable",
|
|
@@ -2355,11 +2482,11 @@ var wireCommand = new Command("wire").description(
|
|
|
2355
2482
|
const baseUrl = `http://localhost:${port}${opts.basePath}`;
|
|
2356
2483
|
await checkConnectivity3(baseUrl);
|
|
2357
2484
|
if (opts.list) {
|
|
2358
|
-
const plugsRes = await
|
|
2485
|
+
const plugsRes = await cliFetch(`${baseUrl}/plugs`);
|
|
2359
2486
|
const plugs = await plugsRes.json();
|
|
2360
2487
|
const wires = await Promise.all(
|
|
2361
2488
|
plugs.map(async (plug) => {
|
|
2362
|
-
const res = await
|
|
2489
|
+
const res = await cliFetch(`${baseUrl}/wires/${plug.name}`);
|
|
2363
2490
|
const data = await res.json();
|
|
2364
2491
|
return {
|
|
2365
2492
|
plugName: plug.name,
|
|
@@ -2380,7 +2507,7 @@ var wireCommand = new Command("wire").description(
|
|
|
2380
2507
|
}
|
|
2381
2508
|
const resolvedAction = opts.info ? "info" : action ?? "info";
|
|
2382
2509
|
if (resolvedAction === "info") {
|
|
2383
|
-
const res = await
|
|
2510
|
+
const res = await cliFetch(`${baseUrl}/wires/${plugName}`);
|
|
2384
2511
|
if (res.status === 404) {
|
|
2385
2512
|
fail3("plug_not_found", `Plug "${plugName}" not found.`);
|
|
2386
2513
|
}
|
|
@@ -2395,7 +2522,7 @@ var wireCommand = new Command("wire").description(
|
|
|
2395
2522
|
}
|
|
2396
2523
|
if (resolvedAction === "connect") {
|
|
2397
2524
|
const callbackUrl = opts.callbackUrl ?? `${resolveWebhookOrigin(opts.webhookOrigin)}/api/khotan/webhook/${plugName}`;
|
|
2398
|
-
const res = await
|
|
2525
|
+
const res = await cliFetch(`${baseUrl}/wires/${plugName}`, {
|
|
2399
2526
|
method: "POST",
|
|
2400
2527
|
headers: { "Content-Type": "application/json" },
|
|
2401
2528
|
body: JSON.stringify({ callbackUrl })
|
|
@@ -2419,7 +2546,7 @@ var wireCommand = new Command("wire").description(
|
|
|
2419
2546
|
if (resolvedAction === "disconnect") {
|
|
2420
2547
|
let wireId = opts.wireId;
|
|
2421
2548
|
if (!wireId) {
|
|
2422
|
-
const currentRes = await
|
|
2549
|
+
const currentRes = await cliFetch(`${baseUrl}/wires/${plugName}`);
|
|
2423
2550
|
const current = await currentRes.json();
|
|
2424
2551
|
wireId = current.wire?.id;
|
|
2425
2552
|
}
|
|
@@ -2429,7 +2556,7 @@ var wireCommand = new Command("wire").description(
|
|
|
2429
2556
|
`No active wire found for "${plugName}". Use --wire-id to override.`
|
|
2430
2557
|
);
|
|
2431
2558
|
}
|
|
2432
|
-
const res = await
|
|
2559
|
+
const res = await cliFetch(`${baseUrl}/wires/${plugName}`, {
|
|
2433
2560
|
method: "DELETE",
|
|
2434
2561
|
headers: { "Content-Type": "application/json" },
|
|
2435
2562
|
body: JSON.stringify({ wireId })
|
|
@@ -2487,9 +2614,10 @@ function resolveBaseUrl(opts) {
|
|
|
2487
2614
|
return `http://localhost:${resolvePort4(opts.port)}${opts.basePath}`;
|
|
2488
2615
|
}
|
|
2489
2616
|
async function fetchJson(url, init) {
|
|
2490
|
-
const res = await
|
|
2617
|
+
const res = await cliFetch(url, init);
|
|
2491
2618
|
const data = await res.json().catch(() => ({}));
|
|
2492
2619
|
if (!res.ok) {
|
|
2620
|
+
if (res.status === 401) fail4("unauthorized", unauthorizedHint());
|
|
2493
2621
|
fail4(
|
|
2494
2622
|
"request_failed",
|
|
2495
2623
|
data.error ?? `Request to ${url} failed with status ${res.status}`
|
|
@@ -2498,20 +2626,22 @@ async function fetchJson(url, init) {
|
|
|
2498
2626
|
return data;
|
|
2499
2627
|
}
|
|
2500
2628
|
async function checkConnectivity4(baseUrl) {
|
|
2629
|
+
let res;
|
|
2501
2630
|
try {
|
|
2502
|
-
|
|
2503
|
-
if (!res.ok) {
|
|
2504
|
-
fail4(
|
|
2505
|
-
"api_unavailable",
|
|
2506
|
-
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2507
|
-
);
|
|
2508
|
-
}
|
|
2631
|
+
res = await cliFetch(`${baseUrl}/flows`);
|
|
2509
2632
|
} catch {
|
|
2510
2633
|
fail4(
|
|
2511
2634
|
"connect_failed",
|
|
2512
2635
|
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2513
2636
|
);
|
|
2514
2637
|
}
|
|
2638
|
+
if (res.status === 401) fail4("unauthorized", unauthorizedHint());
|
|
2639
|
+
if (!res.ok) {
|
|
2640
|
+
fail4(
|
|
2641
|
+
"api_unavailable",
|
|
2642
|
+
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2515
2645
|
}
|
|
2516
2646
|
function parseJsonOption(value, label) {
|
|
2517
2647
|
if (!value) return void 0;
|
|
@@ -2660,9 +2790,10 @@ function resolveBaseUrl2(opts) {
|
|
|
2660
2790
|
return `http://localhost:${resolvePort5(opts.port)}${opts.basePath}`;
|
|
2661
2791
|
}
|
|
2662
2792
|
async function fetchJson2(url, init) {
|
|
2663
|
-
const res = await
|
|
2793
|
+
const res = await cliFetch(url, init);
|
|
2664
2794
|
const data = await res.json().catch(() => ({}));
|
|
2665
2795
|
if (!res.ok) {
|
|
2796
|
+
if (res.status === 401) fail5("unauthorized", unauthorizedHint());
|
|
2666
2797
|
fail5(
|
|
2667
2798
|
"request_failed",
|
|
2668
2799
|
data.error ?? `Request to ${url} failed with status ${res.status}`
|
|
@@ -2671,20 +2802,22 @@ async function fetchJson2(url, init) {
|
|
|
2671
2802
|
return data;
|
|
2672
2803
|
}
|
|
2673
2804
|
async function checkConnectivity5(baseUrl) {
|
|
2805
|
+
let res;
|
|
2674
2806
|
try {
|
|
2675
|
-
|
|
2676
|
-
if (!res.ok) {
|
|
2677
|
-
fail5(
|
|
2678
|
-
"api_unavailable",
|
|
2679
|
-
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2680
|
-
);
|
|
2681
|
-
}
|
|
2807
|
+
res = await cliFetch(`${baseUrl}/flows`);
|
|
2682
2808
|
} catch {
|
|
2683
2809
|
fail5(
|
|
2684
2810
|
"connect_failed",
|
|
2685
2811
|
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2686
2812
|
);
|
|
2687
2813
|
}
|
|
2814
|
+
if (res.status === 401) fail5("unauthorized", unauthorizedHint());
|
|
2815
|
+
if (!res.ok) {
|
|
2816
|
+
fail5(
|
|
2817
|
+
"api_unavailable",
|
|
2818
|
+
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2688
2821
|
}
|
|
2689
2822
|
function parseJsonObjectOption(value, label) {
|
|
2690
2823
|
if (!value) return void 0;
|
|
@@ -2842,10 +2975,14 @@ withApiOptions2(
|
|
|
2842
2975
|
).action(async (mappingId, opts) => {
|
|
2843
2976
|
const baseUrl = resolveBaseUrl2(opts);
|
|
2844
2977
|
await checkConnectivity5(baseUrl);
|
|
2845
|
-
const res = await
|
|
2846
|
-
|
|
2847
|
-
|
|
2978
|
+
const res = await cliFetch(
|
|
2979
|
+
`${baseUrl}/mappings/${encodeURIComponent(mappingId)}`,
|
|
2980
|
+
{
|
|
2981
|
+
method: "DELETE"
|
|
2982
|
+
}
|
|
2983
|
+
);
|
|
2848
2984
|
if (!res.ok) {
|
|
2985
|
+
if (res.status === 401) fail5("unauthorized", unauthorizedHint());
|
|
2849
2986
|
const data = await res.json().catch(() => ({}));
|
|
2850
2987
|
fail5(
|
|
2851
2988
|
"request_failed",
|