xapi-to 0.1.21 → 0.1.22
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 +123 -1
- package/dist/{chunk-UEQCIJ7T.js → chunk-2YRWNREY.js} +2 -2
- package/dist/index.js +475 -33
- package/dist/openai-sandbox-client.js +1 -1
- package/examples/openai-gpt-live-text.mjs +128 -0
- package/examples/provider/openapi.json +34 -0
- package/package.json +1 -1
- package/skills/xapi/SKILL.md +42 -193
- package/skills/xapi/guides/binance_web3.md +210 -0
- package/skills/xapi/guides/blockpi.md +112 -0
- package/skills/xapi/guides/domains.md +189 -0
- package/skills/xapi/guides/provider.md +30 -0
- package/skills/xapi/guides/ws_gateway.md +64 -4
- package/src/client.ts +6 -2
package/dist/index.js
CHANGED
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
saveConfig,
|
|
50
50
|
scheme,
|
|
51
51
|
showConfig
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-2YRWNREY.js";
|
|
53
53
|
|
|
54
54
|
// src/codegen.ts
|
|
55
55
|
var TARGET_MAP = {
|
|
@@ -1192,24 +1192,97 @@ function bindingChangedAfter(binding, startedAtMs, existingBindingIds) {
|
|
|
1192
1192
|
if (!Number.isFinite(changedAt)) return !existingBindingIds.has(binding.id);
|
|
1193
1193
|
return changedAt >= startedAtMs;
|
|
1194
1194
|
}
|
|
1195
|
+
var POLL_DEADLINE = /* @__PURE__ */ Symbol("oauth poll deadline");
|
|
1196
|
+
function waitForPollInterval(ms, signal) {
|
|
1197
|
+
return new Promise((resolve4) => {
|
|
1198
|
+
if (signal.aborted) {
|
|
1199
|
+
resolve4(false);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
let timer;
|
|
1203
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1204
|
+
const onAbort = () => {
|
|
1205
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1206
|
+
cleanup();
|
|
1207
|
+
resolve4(false);
|
|
1208
|
+
};
|
|
1209
|
+
timer = setTimeout(() => {
|
|
1210
|
+
cleanup();
|
|
1211
|
+
resolve4(true);
|
|
1212
|
+
}, Math.max(0, ms));
|
|
1213
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1214
|
+
if (signal.aborted) onAbort();
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
function resolveOnPollAbort(operation, signal) {
|
|
1218
|
+
return new Promise((resolve4, reject) => {
|
|
1219
|
+
let settled = false;
|
|
1220
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1221
|
+
const onAbort = () => {
|
|
1222
|
+
if (settled) return;
|
|
1223
|
+
settled = true;
|
|
1224
|
+
cleanup();
|
|
1225
|
+
resolve4(POLL_DEADLINE);
|
|
1226
|
+
};
|
|
1227
|
+
const resolveOperation = (value) => {
|
|
1228
|
+
if (settled) return;
|
|
1229
|
+
settled = true;
|
|
1230
|
+
cleanup();
|
|
1231
|
+
resolve4(value);
|
|
1232
|
+
};
|
|
1233
|
+
const rejectOperation = (error) => {
|
|
1234
|
+
if (settled) return;
|
|
1235
|
+
settled = true;
|
|
1236
|
+
cleanup();
|
|
1237
|
+
reject(error);
|
|
1238
|
+
};
|
|
1239
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1240
|
+
operation.then(resolveOperation, rejectOperation);
|
|
1241
|
+
if (signal.aborted) {
|
|
1242
|
+
onAbort();
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1195
1247
|
async function pollForBinding(apiKeyId, providerId, jwtToken, startedAt, existingBindingIds = /* @__PURE__ */ new Set(), timeoutMs = 5 * 60 * 1e3, intervalMs = 3e3) {
|
|
1196
1248
|
const deadline = Date.now() + timeoutMs;
|
|
1197
1249
|
const isTTY = process.stdout.isTTY;
|
|
1198
1250
|
const startedAtMs = startedAt.getTime() - 5e3;
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1251
|
+
const controller = new AbortController();
|
|
1252
|
+
const deadlineTimer = setTimeout(
|
|
1253
|
+
() => controller.abort(),
|
|
1254
|
+
Math.max(0, deadline - Date.now())
|
|
1255
|
+
);
|
|
1256
|
+
try {
|
|
1257
|
+
while (Date.now() < deadline) {
|
|
1258
|
+
const remaining = deadline - Date.now();
|
|
1259
|
+
const intervalElapsed = await waitForPollInterval(
|
|
1260
|
+
Math.min(Math.max(0, intervalMs), remaining),
|
|
1261
|
+
controller.signal
|
|
1262
|
+
);
|
|
1263
|
+
if (!intervalElapsed || Date.now() >= deadline || controller.signal.aborted) break;
|
|
1264
|
+
try {
|
|
1265
|
+
const bindings = await resolveOnPollAbort(
|
|
1266
|
+
listOAuthBindings(jwtToken, XAPI_API_HOST, controller.signal),
|
|
1267
|
+
controller.signal
|
|
1268
|
+
);
|
|
1269
|
+
if (bindings === POLL_DEADLINE || Date.now() >= deadline) break;
|
|
1270
|
+
const match = Array.isArray(bindings) ? bindings.find(
|
|
1271
|
+
(b) => b.apiKeyId === apiKeyId && b.providerId === providerId && bindingChangedAfter(b, startedAtMs, existingBindingIds)
|
|
1272
|
+
) : null;
|
|
1273
|
+
if (match) return match;
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
if (controller.signal.aborted || Date.now() >= deadline) break;
|
|
1276
|
+
if (!isRetryableRequestError(e)) throw e;
|
|
1277
|
+
}
|
|
1278
|
+
if (isTTY) {
|
|
1279
|
+
const remaining2 = Math.ceil((deadline - Date.now()) / 1e3);
|
|
1280
|
+
process.stdout.write(`\r Waiting for authorization... (${remaining2}s remaining) `);
|
|
1281
|
+
}
|
|
1212
1282
|
}
|
|
1283
|
+
} finally {
|
|
1284
|
+
clearTimeout(deadlineTimer);
|
|
1285
|
+
controller.abort();
|
|
1213
1286
|
}
|
|
1214
1287
|
if (process.stdout.isTTY) process.stdout.write("\n");
|
|
1215
1288
|
return null;
|
|
@@ -1923,11 +1996,11 @@ COMMON
|
|
|
1923
1996
|
}
|
|
1924
1997
|
function validateFlags(flags, command, allowed = []) {
|
|
1925
1998
|
const valid = /* @__PURE__ */ new Set([...COMMON_FLAGS, ...allowed]);
|
|
1926
|
-
const unknown = Object.keys(flags).filter((
|
|
1999
|
+
const unknown = Object.keys(flags).filter((flag2) => !valid.has(flag2));
|
|
1927
2000
|
if (unknown.length) {
|
|
1928
|
-
err(`unknown flag${unknown.length > 1 ? "s" : ""} for sandbox ${command}: ${unknown.map((
|
|
2001
|
+
err(`unknown flag${unknown.length > 1 ? "s" : ""} for sandbox ${command}: ${unknown.map((flag2) => `--${flag2}`).join(", ")}`, {
|
|
1929
2002
|
hint: `run xapi-to sandbox ${command} --help`,
|
|
1930
|
-
validFlags: [...valid].sort().map((
|
|
2003
|
+
validFlags: [...valid].sort().map((flag2) => `--${flag2}`)
|
|
1931
2004
|
});
|
|
1932
2005
|
}
|
|
1933
2006
|
if (flags.format && !["json", "pretty", "table"].includes(flags.format)) {
|
|
@@ -2608,17 +2681,349 @@ async function sandboxRun(args, flags) {
|
|
|
2608
2681
|
}
|
|
2609
2682
|
|
|
2610
2683
|
// src/commands/provider.ts
|
|
2611
|
-
import { mkdir, open as open2, readFile as
|
|
2684
|
+
import { mkdir, open as open2, readFile as readFile3 } from "fs/promises";
|
|
2612
2685
|
import { dirname, resolve as resolve2 } from "path";
|
|
2686
|
+
|
|
2687
|
+
// src/commands/provider-onboarding.ts
|
|
2688
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2689
|
+
|
|
2690
|
+
// src/provider-client.ts
|
|
2691
|
+
function providerRequest(path, apiKey, method = "GET", body, timeoutMs = 3e4, retries = method === "GET" ? 2 : 0) {
|
|
2692
|
+
return request(`${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/api-services/agent/${path}`, {
|
|
2693
|
+
method,
|
|
2694
|
+
headers: {
|
|
2695
|
+
"Content-Type": "application/json",
|
|
2696
|
+
...apiKey ? { "XAPI-KEY": apiKey } : {}
|
|
2697
|
+
},
|
|
2698
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
2699
|
+
}, Math.min(timeoutMs, 3e4), retries);
|
|
2700
|
+
}
|
|
2701
|
+
function object(value) {
|
|
2702
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2703
|
+
}
|
|
2704
|
+
var SECRET_FIELD = /^(authConfig|privateHeaders|authorization|proxy-authorization|api[-_]?key|xapi[-_]?key|x-api-key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|token|cookie|set-cookie)$/i;
|
|
2705
|
+
var CONTRACT_FIELD = /* @__PURE__ */ new Set([
|
|
2706
|
+
"openApiSpec",
|
|
2707
|
+
"bodySchema",
|
|
2708
|
+
"schema",
|
|
2709
|
+
"schemas",
|
|
2710
|
+
"properties",
|
|
2711
|
+
"definitions",
|
|
2712
|
+
"$defs",
|
|
2713
|
+
"params",
|
|
2714
|
+
"pathParams",
|
|
2715
|
+
"responses",
|
|
2716
|
+
"securitySchemes"
|
|
2717
|
+
]);
|
|
2718
|
+
function isContract(key, value, parent) {
|
|
2719
|
+
if (CONTRACT_FIELD.has(key)) return true;
|
|
2720
|
+
const obj = object(value);
|
|
2721
|
+
if (typeof obj?.openapi === "string") return true;
|
|
2722
|
+
return parent === "headers" && !!obj && ("type" in obj || "schema" in obj || "$ref" in obj);
|
|
2723
|
+
}
|
|
2724
|
+
function collectProviderSecrets(value) {
|
|
2725
|
+
const secrets = [];
|
|
2726
|
+
function visit(item, sensitive = false, contract = false, parent) {
|
|
2727
|
+
if (typeof item === "string" && sensitive && item) secrets.push(item);
|
|
2728
|
+
else if (Array.isArray(item)) item.forEach((v) => visit(v, sensitive, contract, parent));
|
|
2729
|
+
else if (object(item)) {
|
|
2730
|
+
for (const [key, val] of Object.entries(item)) {
|
|
2731
|
+
const definition = !sensitive && (contract || isContract(key, val, parent));
|
|
2732
|
+
visit(val, sensitive || !definition && SECRET_FIELD.test(key), definition, key);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
visit(value);
|
|
2737
|
+
return secrets;
|
|
2738
|
+
}
|
|
2739
|
+
function redactProvider(value, knownSecrets = []) {
|
|
2740
|
+
const secrets = [.../* @__PURE__ */ new Set([...knownSecrets, ...collectProviderSecrets(value)])].filter(Boolean).sort((a, b) => b.length - a.length);
|
|
2741
|
+
function visit(item, contract = false, parent) {
|
|
2742
|
+
if (typeof item === "string") {
|
|
2743
|
+
return secrets.reduce((text, secret) => text.split(secret).join("[REDACTED]"), item);
|
|
2744
|
+
}
|
|
2745
|
+
if (Array.isArray(item)) return item.map((val) => visit(val, contract, parent));
|
|
2746
|
+
if (object(item)) return Object.fromEntries(Object.entries(item).map(([key, val]) => {
|
|
2747
|
+
const definition = contract || isContract(key, val, parent);
|
|
2748
|
+
return [key, !definition && SECRET_FIELD.test(key) ? "[REDACTED]" : visit(val, definition, key)];
|
|
2749
|
+
}));
|
|
2750
|
+
return item;
|
|
2751
|
+
}
|
|
2752
|
+
return visit(value);
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
// src/commands/provider-onboarding.ts
|
|
2756
|
+
var PROVIDER_ONBOARDING_HELP = `xapi-to provider - Import and publish your API services
|
|
2757
|
+
|
|
2758
|
+
COMMANDS
|
|
2759
|
+
spec-rules Read current OpenAPI rules (public)
|
|
2760
|
+
import --file openapi.json Create a DRAFT service from OpenAPI JSON
|
|
2761
|
+
--private-headers-file <path> Upstream credentials as a JSON object
|
|
2762
|
+
update <service-id> --revision <id> --file config.json
|
|
2763
|
+
--mode merge|replace PATCH merge (default) or PUT replacement
|
|
2764
|
+
--allow-new-endpoints Allow merge entries without IDs to create endpoints
|
|
2765
|
+
submit <service-id> --revision <id>
|
|
2766
|
+
--changelog <text> Submit for review; this alone is not publication
|
|
2767
|
+
review <service-id> --revision <id>
|
|
2768
|
+
Read latest review and previous attempts
|
|
2769
|
+
wait <service-id> --revision <id>
|
|
2770
|
+
--interval <duration> Poll interval (default: 2s; ms/s/m/h)
|
|
2771
|
+
--timeout <duration> Overall deadline (default: 10m; ms/s/m/h)
|
|
2772
|
+
--max-attempts <number> Optional cap, including transient failures
|
|
2773
|
+
|
|
2774
|
+
COMMON FLAGS
|
|
2775
|
+
--format json|pretty|table
|
|
2776
|
+
--help
|
|
2777
|
+
|
|
2778
|
+
Files must contain JSON objects; --file - reads stdin. Import reads a raw
|
|
2779
|
+
OpenAPI spec; update reads version configuration (not a raw OpenAPI spec).
|
|
2780
|
+
Merge updates to existing endpoints require their IDs. Use --mode replace
|
|
2781
|
+
to replace the endpoint list, or --allow-new-endpoints to intentionally add.
|
|
2782
|
+
Saving a draft configuration moves it to SANDBOX, ready for submit.
|
|
2783
|
+
|
|
2784
|
+
PERMISSIONS
|
|
2785
|
+
import: service:create (legacy allowRegister also accepted)
|
|
2786
|
+
list/get/review/wait: service:read; update: service:update
|
|
2787
|
+
submit: service:publish. Grant scopes in the xAPI Console API Keys settings.
|
|
2788
|
+
|
|
2789
|
+
wait succeeds only for PUBLISHED. Rejection, unpublished terminal states,
|
|
2790
|
+
manual review, invalid responses, and timeouts exit nonzero with details.
|
|
2791
|
+
Writes are not retried automatically. Credentials are redacted from output.
|
|
2792
|
+
`;
|
|
2793
|
+
var FLAGS = {
|
|
2794
|
+
"spec-rules": [],
|
|
2795
|
+
import: ["file", "private-headers-file"],
|
|
2796
|
+
update: ["revision", "file", "mode", "allow-new-endpoints"],
|
|
2797
|
+
submit: ["revision", "changelog"],
|
|
2798
|
+
review: ["revision"],
|
|
2799
|
+
wait: ["revision", "interval", "timeout", "max-attempts"]
|
|
2800
|
+
};
|
|
2801
|
+
var SCOPES = {
|
|
2802
|
+
import: "service:create",
|
|
2803
|
+
update: "service:update",
|
|
2804
|
+
submit: "service:publish",
|
|
2805
|
+
review: "service:read",
|
|
2806
|
+
wait: "service:read"
|
|
2807
|
+
};
|
|
2808
|
+
function flag(flags, name, required3 = false) {
|
|
2809
|
+
const value = flags[name];
|
|
2810
|
+
if (required3 && value === void 0 || value === "" || value === "true") {
|
|
2811
|
+
throw new Error(`--${name} requires a value`);
|
|
2812
|
+
}
|
|
2813
|
+
return value;
|
|
2814
|
+
}
|
|
2815
|
+
function duration(raw, name) {
|
|
2816
|
+
const match = /^(\d+)(ms|s|m|h)?$/.exec(raw);
|
|
2817
|
+
const units = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
|
|
2818
|
+
const ms = match ? Number(match[1]) * units[match[2] || "ms"] : NaN;
|
|
2819
|
+
if (!Number.isSafeInteger(ms) || ms <= 0 || ms > 2147483647) {
|
|
2820
|
+
throw new Error(`--${name} must be a positive duration (ms/s/m/h), at most 2147483647ms`);
|
|
2821
|
+
}
|
|
2822
|
+
return ms;
|
|
2823
|
+
}
|
|
2824
|
+
async function jsonFile(path) {
|
|
2825
|
+
let text;
|
|
2826
|
+
try {
|
|
2827
|
+
if (path === "-") {
|
|
2828
|
+
const chunks = [];
|
|
2829
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
2830
|
+
text = Buffer.concat(chunks).toString("utf8");
|
|
2831
|
+
} else text = await readFile2(path, "utf8");
|
|
2832
|
+
} catch {
|
|
2833
|
+
throw new Error("Could not read JSON input file");
|
|
2834
|
+
}
|
|
2835
|
+
let value;
|
|
2836
|
+
try {
|
|
2837
|
+
value = JSON.parse(text);
|
|
2838
|
+
} catch {
|
|
2839
|
+
throw new Error("Input must be valid JSON (JSON objects only; YAML is not supported)");
|
|
2840
|
+
}
|
|
2841
|
+
const result = object(value);
|
|
2842
|
+
if (!result) throw new Error("Input must be a JSON object");
|
|
2843
|
+
return result;
|
|
2844
|
+
}
|
|
2845
|
+
function segment(value) {
|
|
2846
|
+
const normalized = value.trim();
|
|
2847
|
+
if (!normalized || normalized === "." || normalized === "..") {
|
|
2848
|
+
throw new Error("Invalid service or revision ID");
|
|
2849
|
+
}
|
|
2850
|
+
return encodeURIComponent(normalized);
|
|
2851
|
+
}
|
|
2852
|
+
async function providerOnboarding(args, flags) {
|
|
2853
|
+
const [command, ...rest] = args;
|
|
2854
|
+
if (flags.help || !command) {
|
|
2855
|
+
console.log(PROVIDER_ONBOARDING_HELP);
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const secrets = [];
|
|
2859
|
+
const emit = (value) => output(redactProvider(value, secrets), flags.format);
|
|
2860
|
+
try {
|
|
2861
|
+
if (!Object.hasOwn(FLAGS, command)) throw new Error(`Unknown provider command: ${command}`);
|
|
2862
|
+
for (const key of Object.keys(flags)) {
|
|
2863
|
+
if (!["format", ...FLAGS[command]].includes(key)) throw new Error(`Unknown flag for provider ${command}: --${key}`);
|
|
2864
|
+
}
|
|
2865
|
+
const needsService = !["spec-rules", "import"].includes(command);
|
|
2866
|
+
if (rest.length !== (needsService ? 1 : 0)) throw new Error(`provider ${command} expects ${needsService ? "one service ID" : "no positional arguments"}`);
|
|
2867
|
+
const serviceId = rest[0];
|
|
2868
|
+
const base = serviceId ? `services/${segment(serviceId)}` : "services";
|
|
2869
|
+
const revisionId = flag(flags, "revision", ["update", "submit", "review", "wait"].includes(command));
|
|
2870
|
+
const revisionPath = revisionId ? `${base}/revisions/${segment(revisionId)}` : "";
|
|
2871
|
+
const cfg = getConfig();
|
|
2872
|
+
if (cfg.apiKey) secrets.push(cfg.apiKey);
|
|
2873
|
+
if (command !== "spec-rules") requireApiKey(cfg);
|
|
2874
|
+
const read = (path) => providerRequest(path, cfg.apiKey);
|
|
2875
|
+
if (command === "spec-rules") {
|
|
2876
|
+
emit(await providerRequest("spec-rules", void 0));
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (command === "import") {
|
|
2880
|
+
const file = flag(flags, "file", true);
|
|
2881
|
+
const headersFile = flag(flags, "private-headers-file");
|
|
2882
|
+
if (file === "-" && headersFile === "-") throw new Error("Only one input may read stdin");
|
|
2883
|
+
const spec = await jsonFile(file);
|
|
2884
|
+
const body = { openApiSpec: spec };
|
|
2885
|
+
if (headersFile) {
|
|
2886
|
+
body.privateHeaders = await jsonFile(headersFile);
|
|
2887
|
+
if (Object.values(body.privateHeaders).some((v) => typeof v !== "string")) {
|
|
2888
|
+
throw new Error("Private header values must be strings");
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
secrets.push(...collectProviderSecrets(body));
|
|
2892
|
+
const result = await providerRequest("register-api-service", cfg.apiKey, "POST", body);
|
|
2893
|
+
if (result?.success === false) {
|
|
2894
|
+
emit(result);
|
|
2895
|
+
process.exitCode = 1;
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
const service = object(result?.apiService);
|
|
2899
|
+
if (result?.success !== true || typeof service?.id !== "string") {
|
|
2900
|
+
throw new Error("Unexpected import response; creation may have succeeded. Check provider list before retrying");
|
|
2901
|
+
}
|
|
2902
|
+
const active = object(service.activeVersion);
|
|
2903
|
+
const revision = active ?? (Array.isArray(service.versions) ? object(service.versions[0]) : void 0);
|
|
2904
|
+
emit({
|
|
2905
|
+
...result,
|
|
2906
|
+
serviceId: service.id,
|
|
2907
|
+
revisionId: revision?.id ?? service.activeVersionId ?? null,
|
|
2908
|
+
state: revision?.state ?? service.status ?? null
|
|
2909
|
+
});
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
if (command === "update") {
|
|
2913
|
+
const mode = flag(flags, "mode") ?? "merge";
|
|
2914
|
+
if (!["merge", "replace"].includes(mode)) throw new Error("--mode must be merge or replace");
|
|
2915
|
+
if (flags["allow-new-endpoints"] !== void 0 && flags["allow-new-endpoints"] !== "true") {
|
|
2916
|
+
throw new Error("--allow-new-endpoints is a boolean flag");
|
|
2917
|
+
}
|
|
2918
|
+
const body = await jsonFile(flag(flags, "file", true));
|
|
2919
|
+
secrets.push(...collectProviderSecrets(body));
|
|
2920
|
+
if ("openapi" in body) throw new Error("update expects version configuration, not a raw OpenAPI spec");
|
|
2921
|
+
if (body.endpoints !== void 0) {
|
|
2922
|
+
if (!Array.isArray(body.endpoints) || body.endpoints.some((ep) => !object(ep))) throw new Error("endpoints must be an array of objects");
|
|
2923
|
+
if (mode === "merge" && !flags["allow-new-endpoints"] && body.endpoints.some((ep) => typeof ep.id !== "string" || !ep.id.trim())) {
|
|
2924
|
+
throw new Error("Merge endpoints require IDs. Use --mode replace for a full list, or --allow-new-endpoints to intentionally create endpoints");
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
const revision = await providerRequest(`${base}/versions/${segment(revisionId)}`, cfg.apiKey, mode === "merge" ? "PATCH" : "PUT", body);
|
|
2928
|
+
emit({ serviceId, revisionId, state: revision?.state ?? null, revision });
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
if (command === "submit") {
|
|
2932
|
+
const changelog = flag(flags, "changelog");
|
|
2933
|
+
if (changelog !== void 0 && changelog.length > 2e3) {
|
|
2934
|
+
throw new Error("--changelog must be at most 2000 characters");
|
|
2935
|
+
}
|
|
2936
|
+
const submission = await providerRequest(`${revisionPath}/submit`, cfg.apiKey, "POST", changelog ? { changelog } : {});
|
|
2937
|
+
emit({ serviceId, revisionId, submission });
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
if (command === "review") {
|
|
2941
|
+
emit(await read(`${revisionPath}/review`));
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
const intervalMs = duration(flag(flags, "interval") ?? "2s", "interval");
|
|
2945
|
+
const timeoutMs = duration(flag(flags, "timeout") ?? "10m", "timeout");
|
|
2946
|
+
const attemptsFlag = flag(flags, "max-attempts");
|
|
2947
|
+
const maxAttempts = attemptsFlag === void 0 ? Infinity : Number(attemptsFlag);
|
|
2948
|
+
if (attemptsFlag !== void 0 && (!/^\d+$/.test(attemptsFlag) || !Number.isSafeInteger(maxAttempts) || maxAttempts <= 0)) {
|
|
2949
|
+
throw new Error("--max-attempts must be a positive integer");
|
|
2950
|
+
}
|
|
2951
|
+
const deadline = Date.now() + timeoutMs;
|
|
2952
|
+
let attempts = 0;
|
|
2953
|
+
let last;
|
|
2954
|
+
while (true) {
|
|
2955
|
+
if (Date.now() >= deadline) {
|
|
2956
|
+
emit({ serviceId, revisionId, success: false, reason: "timeout", attempts, last });
|
|
2957
|
+
process.exitCode = 1;
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
let delay = intervalMs;
|
|
2961
|
+
let report;
|
|
2962
|
+
let received = false;
|
|
2963
|
+
attempts++;
|
|
2964
|
+
try {
|
|
2965
|
+
report = await providerRequest(`${revisionPath}/review`, cfg.apiKey, "GET", void 0, Math.max(1, deadline - Date.now()), 0);
|
|
2966
|
+
received = true;
|
|
2967
|
+
} catch (e) {
|
|
2968
|
+
if (!isRetryableRequestError(e)) throw e;
|
|
2969
|
+
if (e instanceof HttpError && e.retryAfterMs !== void 0) delay = Math.max(intervalMs, e.retryAfterMs);
|
|
2970
|
+
}
|
|
2971
|
+
if (Date.now() >= deadline) continue;
|
|
2972
|
+
if (received) {
|
|
2973
|
+
const revision = object(report?.revision);
|
|
2974
|
+
const state = revision?.state;
|
|
2975
|
+
if (revision?.id !== revisionId || !["DRAFT", "SANDBOX", "IN_REVIEW", "PUBLISHED", "SUSPENDED"].includes(String(state))) {
|
|
2976
|
+
throw new Error("Invalid review response: expected requested revision ID and known state");
|
|
2977
|
+
}
|
|
2978
|
+
last = report;
|
|
2979
|
+
const review = object(report?.review);
|
|
2980
|
+
const manual = review?.outcome === "pending_human" || review?.status === "PENDING_HUMAN";
|
|
2981
|
+
const rejected = review?.outcome === "rejected" || ["REJECTED", "AUTO_FAILED"].includes(String(review?.status));
|
|
2982
|
+
if (state === "PUBLISHED" || state !== "IN_REVIEW" || manual || rejected) {
|
|
2983
|
+
const success = state === "PUBLISHED";
|
|
2984
|
+
emit({ ...report, serviceId, revisionId, success, reason: success ? "published" : manual ? "manual_review_required" : rejected ? "rejected" : "not_published" });
|
|
2985
|
+
if (!success) process.exitCode = 1;
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (attempts >= maxAttempts) {
|
|
2990
|
+
emit({ serviceId, revisionId, success: false, reason: "max_attempts", attempts, last });
|
|
2991
|
+
process.exitCode = 1;
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
await new Promise((resolve4) => setTimeout(resolve4, Math.min(delay, Math.max(0, deadline - Date.now()))));
|
|
2995
|
+
}
|
|
2996
|
+
} catch (e) {
|
|
2997
|
+
let message;
|
|
2998
|
+
if (e instanceof HttpError) {
|
|
2999
|
+
message = `HTTP ${e.status}`;
|
|
3000
|
+
if (e.status === 403) message += `: requires ${SCOPES[command]}; check key permissions, service ownership, and IP restrictions in the xAPI Console`;
|
|
3001
|
+
else if (e.status === 401) message += ": invalid or expired API key";
|
|
3002
|
+
else if (e.status === 400) message += ": request rejected; check spec-rules, configuration, and revision state";
|
|
3003
|
+
else if (e.status === 409 && /"code"\s*:\s*"REVISION_NOT_EDITABLE"/.test(e.message)) {
|
|
3004
|
+
message += ": revision is not editable. Only DRAFT or SANDBOX can be updated; use a working revision for changes to a published API";
|
|
3005
|
+
}
|
|
3006
|
+
if (["import", "update", "submit"].includes(command)) message += ". No automatic retry was made; inspect provider list/get/review before retrying";
|
|
3007
|
+
} else message = e instanceof SyntaxError ? "Invalid JSON response from provider API" : e instanceof Error ? e.message : "Unknown error";
|
|
3008
|
+
err(`provider ${command} failed`, redactProvider(message, secrets));
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// src/commands/provider.ts
|
|
2613
3013
|
var READ_RETRIES3 = 2;
|
|
2614
3014
|
var BASE = "/api/api-services/agent";
|
|
2615
3015
|
var PROVIDER_HELP = `xapi-to provider - Manage provider services and their content
|
|
2616
3016
|
|
|
2617
3017
|
USAGE
|
|
3018
|
+
xapi-to provider spec-rules
|
|
3019
|
+
xapi-to provider import --file <openapi.json> [--private-headers-file <path>]
|
|
3020
|
+
xapi-to provider update <service-id> --revision <id> --file <contract.json>
|
|
3021
|
+
xapi-to provider submit <service-id> --revision <id> [--changelog <text>]
|
|
3022
|
+
xapi-to provider wait <service-id> --revision <id> [--interval 2s] [--timeout 10m]
|
|
2618
3023
|
xapi-to provider list
|
|
2619
3024
|
xapi-to provider get <service-id> [--version <version>]
|
|
2620
|
-
xapi-to provider create --file <service.json>
|
|
2621
|
-
xapi-to provider update <service-id> [metadata flags]
|
|
3025
|
+
xapi-to provider create --file <service.json> [rate-limit flags]
|
|
3026
|
+
xapi-to provider update <service-id> [metadata/rate-limit flags]
|
|
2622
3027
|
xapi-to provider versions <service-id>
|
|
2623
3028
|
xapi-to provider version update <service-id> <version-id> --file <contract.json> [--replace]
|
|
2624
3029
|
xapi-to provider major create <service-id>
|
|
@@ -2651,6 +3056,14 @@ METADATA FLAGS
|
|
|
2651
3056
|
--logo-url <url> Service logo URL
|
|
2652
3057
|
--category <category> Marketplace category
|
|
2653
3058
|
|
|
3059
|
+
SERVICE RATE-LIMIT FLAGS
|
|
3060
|
+
--rate-limit-requests <count> Allowed requests per period (1-1000000)
|
|
3061
|
+
--rate-limit-period-seconds <seconds> Period length in seconds (1-86400)
|
|
3062
|
+
--clear-rate-limit Disable the service rate limit
|
|
3063
|
+
|
|
3064
|
+
Set both numeric flags together. Limits apply only to PROXY services and use
|
|
3065
|
+
one shared quota for each user and service, including all API keys of that user.
|
|
3066
|
+
|
|
2654
3067
|
SCOPES
|
|
2655
3068
|
list/get/versions/review/diff/skill context: service:read
|
|
2656
3069
|
create: service:create
|
|
@@ -2688,6 +3101,26 @@ function positiveInt(raw, name, max) {
|
|
|
2688
3101
|
function boolFlag(flags, name) {
|
|
2689
3102
|
return ["true", "1", "yes"].includes((flags[name] || "").toLowerCase());
|
|
2690
3103
|
}
|
|
3104
|
+
function applyRateLimitFlags(body, flags) {
|
|
3105
|
+
const requests = flags["rate-limit-requests"];
|
|
3106
|
+
const periodSeconds = flags["rate-limit-period-seconds"];
|
|
3107
|
+
const clear = boolFlag(flags, "clear-rate-limit");
|
|
3108
|
+
if (clear && (requests !== void 0 || periodSeconds !== void 0)) {
|
|
3109
|
+
err("--clear-rate-limit cannot be combined with --rate-limit-requests or --rate-limit-period-seconds");
|
|
3110
|
+
}
|
|
3111
|
+
if (requests === void 0 !== (periodSeconds === void 0)) {
|
|
3112
|
+
err("--rate-limit-requests and --rate-limit-period-seconds must be provided together");
|
|
3113
|
+
}
|
|
3114
|
+
if (clear) {
|
|
3115
|
+
body.rateLimitConfig = null;
|
|
3116
|
+
} else if (requests !== void 0) {
|
|
3117
|
+
body.rateLimitConfig = {
|
|
3118
|
+
requests: positiveInt(requests, "--rate-limit-requests", 1e6),
|
|
3119
|
+
periodSeconds: positiveInt(periodSeconds, "--rate-limit-period-seconds", 86400)
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
return body;
|
|
3123
|
+
}
|
|
2691
3124
|
async function readText(path, flagName) {
|
|
2692
3125
|
if (path === "true") err(`${flagName} requires a path or - for stdin`);
|
|
2693
3126
|
if (path === "-") {
|
|
@@ -2697,7 +3130,7 @@ async function readText(path, flagName) {
|
|
|
2697
3130
|
}
|
|
2698
3131
|
return Buffer.concat(chunks).toString("utf8");
|
|
2699
3132
|
}
|
|
2700
|
-
return
|
|
3133
|
+
return readFile3(resolve2(path), "utf8");
|
|
2701
3134
|
}
|
|
2702
3135
|
async function textOption(flags, directName, fileName) {
|
|
2703
3136
|
const direct = flags[directName];
|
|
@@ -2711,11 +3144,12 @@ async function textOption(flags, directName, fileName) {
|
|
|
2711
3144
|
}
|
|
2712
3145
|
async function readJsonObject(path, flagName = "--file") {
|
|
2713
3146
|
if (!path || path === "true") err(`${flagName} requires a JSON file path or - for stdin`);
|
|
3147
|
+
const text = await readText(path, flagName);
|
|
2714
3148
|
let parsed;
|
|
2715
3149
|
try {
|
|
2716
|
-
parsed = JSON.parse(
|
|
2717
|
-
} catch
|
|
2718
|
-
err(`invalid JSON from ${flagName}`,
|
|
3150
|
+
parsed = JSON.parse(text);
|
|
3151
|
+
} catch {
|
|
3152
|
+
err(`invalid JSON from ${flagName}`, "Input must be valid JSON.");
|
|
2719
3153
|
}
|
|
2720
3154
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2721
3155
|
err(`${flagName} must contain a JSON object`);
|
|
@@ -2745,8 +3179,9 @@ async function metadataBody(flags) {
|
|
|
2745
3179
|
}
|
|
2746
3180
|
if (boolFlag(flags, "clear-about")) body.aboutMarkdown = null;
|
|
2747
3181
|
if (boolFlag(flags, "clear-website")) body.website = null;
|
|
3182
|
+
applyRateLimitFlags(body, flags);
|
|
2748
3183
|
if (Object.keys(body).length === 0) {
|
|
2749
|
-
err("no provider
|
|
3184
|
+
err("no provider settings supplied", "Pass --file or at least one metadata or rate-limit flag.");
|
|
2750
3185
|
}
|
|
2751
3186
|
return body;
|
|
2752
3187
|
}
|
|
@@ -2763,9 +3198,12 @@ async function writeExclusive(path, content, force) {
|
|
|
2763
3198
|
}
|
|
2764
3199
|
async function provider(args, flags) {
|
|
2765
3200
|
if (flags.help || args.length === 0) {
|
|
2766
|
-
console.log(PROVIDER_HELP);
|
|
3201
|
+
console.log(PROVIDER_HELP + "\n" + PROVIDER_ONBOARDING_HELP);
|
|
2767
3202
|
return;
|
|
2768
3203
|
}
|
|
3204
|
+
const onboardingCommand = ["spec-rules", "import", "submit", "wait"].includes(args[0]);
|
|
3205
|
+
const revisionAlias = ["update", "review"].includes(args[0]) && flags.revision !== void 0;
|
|
3206
|
+
if (onboardingCommand || revisionAlias) return providerOnboarding(args, flags);
|
|
2769
3207
|
const cfg = getConfig();
|
|
2770
3208
|
requireApiKey(cfg);
|
|
2771
3209
|
const apiKey = cfg.apiKey;
|
|
@@ -2784,14 +3222,17 @@ async function provider(args, flags) {
|
|
|
2784
3222
|
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
2785
3223
|
break;
|
|
2786
3224
|
}
|
|
2787
|
-
case "create":
|
|
3225
|
+
case "create": {
|
|
3226
|
+
const body = await readJsonObject(required(flags.file, "xapi-to provider create --file <service.json>"));
|
|
3227
|
+
applyRateLimitFlags(body, flags);
|
|
2788
3228
|
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE}/services`, {
|
|
2789
3229
|
method: "POST",
|
|
2790
|
-
body
|
|
3230
|
+
body
|
|
2791
3231
|
});
|
|
2792
3232
|
break;
|
|
3233
|
+
}
|
|
2793
3234
|
case "update": {
|
|
2794
|
-
const id = required(rest[0], "xapi-to provider update <service-id> [metadata flags]");
|
|
3235
|
+
const id = required(rest[0], "xapi-to provider update <service-id> [metadata/rate-limit flags]");
|
|
2795
3236
|
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), {
|
|
2796
3237
|
method: "PATCH",
|
|
2797
3238
|
body: await metadataBody(flags)
|
|
@@ -2930,14 +3371,15 @@ async function provider(args, flags) {
|
|
|
2930
3371
|
default:
|
|
2931
3372
|
err(`unknown provider command: ${command}`, 'Run "xapi-to provider --help".');
|
|
2932
3373
|
}
|
|
2933
|
-
output(result, flags.format);
|
|
3374
|
+
output(redactProvider(result, [apiKey]), flags.format);
|
|
2934
3375
|
} catch (error) {
|
|
2935
|
-
|
|
3376
|
+
const message = error instanceof HttpError ? `HTTP ${error.status}` : String(redactProvider(error instanceof Error ? error.message : "Unknown error", [apiKey]));
|
|
3377
|
+
err("provider request failed", message);
|
|
2936
3378
|
}
|
|
2937
3379
|
}
|
|
2938
3380
|
|
|
2939
3381
|
// src/commands/skill.ts
|
|
2940
|
-
import { readdir, readFile as
|
|
3382
|
+
import { readdir, readFile as readFile4 } from "fs/promises";
|
|
2941
3383
|
import { relative, resolve as resolve3, sep } from "path";
|
|
2942
3384
|
var READ_RETRIES4 = 2;
|
|
2943
3385
|
var MAX_FILES = 100;
|
|
@@ -3004,7 +3446,7 @@ async function collectInlineFiles(directory) {
|
|
|
3004
3446
|
if (files.length >= MAX_FILES) {
|
|
3005
3447
|
throw new Error(`skill package exceeds ${MAX_FILES} files`);
|
|
3006
3448
|
}
|
|
3007
|
-
const content = await
|
|
3449
|
+
const content = await readFile4(absolute);
|
|
3008
3450
|
if (content.byteLength > MAX_FILE_BYTES) {
|
|
3009
3451
|
throw new Error(`skill file ${entry.name} exceeds ${MAX_FILE_BYTES} bytes`);
|
|
3010
3452
|
}
|