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/dist/index.js CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  saveConfig,
50
50
  scheme,
51
51
  showConfig
52
- } from "./chunk-UEQCIJ7T.js";
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
- while (Date.now() < deadline) {
1200
- await new Promise((r) => setTimeout(r, intervalMs));
1201
- try {
1202
- const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
1203
- const match = Array.isArray(bindings) ? bindings.find(
1204
- (b) => b.apiKeyId === apiKeyId && b.providerId === providerId && bindingChangedAfter(b, startedAtMs, existingBindingIds)
1205
- ) : null;
1206
- if (match) return match;
1207
- } catch {
1208
- }
1209
- if (isTTY) {
1210
- const remaining = Math.ceil((deadline - Date.now()) / 1e3);
1211
- process.stdout.write(`\r Waiting for authorization... (${remaining}s remaining) `);
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((flag) => !valid.has(flag));
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((flag) => `--${flag}`).join(", ")}`, {
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((flag) => `--${flag}`)
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 readFile2 } from "fs/promises";
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 readFile2(resolve2(path), "utf8");
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(await readText(path, flagName));
2717
- } catch (error) {
2718
- err(`invalid JSON from ${flagName}`, error.message);
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 metadata supplied", "Pass --file or at least one metadata flag.");
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: await readJsonObject(required(flags.file, "xapi-to provider create --file <service.json>"))
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
- err("provider request failed", error.message);
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 readFile3 } from "fs/promises";
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 readFile3(absolute);
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
  }
@@ -7,7 +7,7 @@ import {
7
7
  sandboxQuote,
8
8
  sandboxStateAction,
9
9
  sandboxWait
10
- } from "./chunk-UEQCIJ7T.js";
10
+ } from "./chunk-2YRWNREY.js";
11
11
 
12
12
  // src/openai-sandbox-client.ts
13
13
  import { randomUUID } from "crypto";