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 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
- async function shopifyProductsWorkflow(ctx: InflowContext) {
106
- "use workflow";
107
-
108
- async function syncProducts() {
109
- "use step";
110
- const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
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
- const response = await shopifyPlug.get<{ data?: Array<Record<string, unknown>> }>("/products");
115
- 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 : [];
116
154
 
117
- await snapshotCache.set("latest", records);
155
+ await snapshotCache.set("latest", records);
118
156
 
119
- return {
120
- extracted: records.length,
121
- transformed: records.length,
122
- created: records.length,
123
- metadata: { previousCount: previous.length },
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
- return syncProducts();
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, path15) {
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: path15,
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, `${path15}[]`);
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: path15,
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, path15) {
1887
+ function diffObjectSchema(expected, actual, path16) {
1806
1888
  if (actual.type !== "object") {
1807
1889
  return [
1808
1890
  {
1809
- path: path15,
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 = path15 === "$" ? `$.${key}` : `${path15}.${key}`;
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 = path15 === "$" ? `$.${key}` : `${path15}.${key}`;
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/variables/${plug.name}`);
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 fetch(`${baseUrl}/variables/${plugName}`);
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 fetch(`${baseUrl}/variables/${plugName}`, {
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 fetch(`${baseUrl}/variables/${plugName}`, {
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 fetch(`${baseUrl}/debug`);
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/debug/${plugName}`);
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 fetch(`${baseUrl}/debug/${plugName}`);
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 fetch(`${baseUrl}/debug/${plugName}`, {
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 fetch(`${baseUrl}/debug/${plugName}`);
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/wires/${plug.name}`);
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 fetch(`${baseUrl}/wires/${plugName}`);
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 fetch(`${baseUrl}/wires/${plugName}`, {
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 fetch(`${baseUrl}/wires/${plugName}`);
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 fetch(`${baseUrl}/wires/${plugName}`, {
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 fetch(url, init);
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
- const res = await fetch(`${baseUrl}/flows`);
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 fetch(url, init);
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
- const res = await fetch(`${baseUrl}/flows`);
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 fetch(`${baseUrl}/mappings/${encodeURIComponent(mappingId)}`, {
2846
- method: "DELETE"
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",