xapi-to 0.1.20 → 0.1.21

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
@@ -327,11 +327,52 @@ xapi-to register --referral-code xapito # register with an invit
327
327
  xapi-to register xapito # positional shorthand for --referral-code
328
328
  xapi-to register --force # replace an existing file-based key
329
329
  xapi-to balance # show USD balance
330
+ xapi-to usage <request-id> # finalized cost + balance-after receipt
331
+ xapi-to usage wait <request-id> --timeout 1m # poll until a streaming receipt is finalized
332
+ xapi-to earnings # spendable balance + provider earnings
333
+ xapi-to earnings list --status SETTLED --limit 20 # provider earning records
334
+ xapi-to earnings transfer 1 --idempotency-key reinvest-001 # reinvest settled earnings
330
335
  xapi-to topup # generate payment URL
331
336
  xapi-to topup --method stripe --amount 10 # stripe, $10
332
337
  xapi-to topup --method x402 # x402 (USDC on Base)
333
338
  ```
334
339
 
340
+ `earnings` summary/list require the `earnings:read` scope on the current key;
341
+ `earnings transfer` requires `earnings:transfer`. Transfers are one-way and
342
+ idempotent: reuse a key only when retrying the same amount.
343
+
344
+ ### Provider Management
345
+
346
+ Provider commands use scoped `XAPI-KEY` routes for service and release
347
+ management without a JWT exchange:
348
+
349
+ ```bash
350
+ xapi-to provider list
351
+ xapi-to provider create --file ./service.json
352
+ xapi-to provider update <service-id> --about-file ./ABOUT.md --website https://example.com
353
+ xapi-to provider versions <service-id>
354
+ xapi-to provider revision start <service-id> 1
355
+ xapi-to provider version update <service-id> <version-id> --file ./contract.json
356
+ xapi-to provider diff <service-id> 1
357
+ xapi-to provider publish <service-id> <revision-id> --changelog-file ./CHANGELOG.md
358
+ xapi-to provider metrics <service-id> --days 7
359
+ xapi-to provider events --after '<opaque-next-cursor>'
360
+ ```
361
+
362
+ Service usage tutorials are Skill packages. Scaffold one from the serving
363
+ contract, submit it for review, wait for publication, then link it:
364
+
365
+ ```bash
366
+ xapi-to provider skill scaffold <service-id> --output ./my-skill/SKILL.md
367
+ xapi-to skill submit --dir ./my-skill
368
+ xapi-to skill wait <submission-id> --timeout 10m
369
+ xapi-to provider skill link <service-id> <skill-id>
370
+ xapi-to provider skill fingerprint <service-id> --skill-version-id <version-id>
371
+ ```
372
+
373
+ Run `xapi-to provider --help` and `xapi-to skill --help` for the complete
374
+ lifecycle, rollback, deletion, GitHub import, scope, and safety options.
375
+
335
376
  ### Config
336
377
 
337
378
  ```bash
@@ -265,8 +265,25 @@ function parseRetryAfterMs(res) {
265
265
  const at = Date.parse(header);
266
266
  return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
267
267
  }
268
- function sleep(ms) {
269
- return new Promise((resolve2) => setTimeout(resolve2, ms));
268
+ function abortError(signal) {
269
+ const reason = signal?.reason;
270
+ return reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError");
271
+ }
272
+ function sleep(ms, signal) {
273
+ if (signal?.aborted) return Promise.reject(abortError(signal));
274
+ return new Promise((resolve2, reject) => {
275
+ let timer;
276
+ const onAbort = () => {
277
+ if (timer !== void 0) clearTimeout(timer);
278
+ signal?.removeEventListener("abort", onAbort);
279
+ reject(abortError(signal));
280
+ };
281
+ timer = setTimeout(() => {
282
+ signal?.removeEventListener("abort", onAbort);
283
+ resolve2();
284
+ }, ms);
285
+ signal?.addEventListener("abort", onAbort, { once: true });
286
+ });
270
287
  }
271
288
  async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
272
289
  assertAllowedHost(url);
@@ -294,7 +311,7 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0
294
311
  if (isRetryableStatus(res.status) && attempt < retries) {
295
312
  await res.text().catch(() => "");
296
313
  clearTimeout(timer);
297
- await sleep(backoffDelayMs(attempt, retryAfterMs));
314
+ await sleep(backoffDelayMs(attempt, retryAfterMs), callerSignal);
298
315
  attempt++;
299
316
  continue;
300
317
  }
@@ -327,7 +344,7 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0
327
344
  if (timedOut) {
328
345
  const timeoutError = new RequestTimeoutError(timeoutMs);
329
346
  if (attempt < retries) {
330
- await sleep(backoffDelayMs(attempt));
347
+ await sleep(backoffDelayMs(attempt), callerSignal);
331
348
  attempt++;
332
349
  continue;
333
350
  }
@@ -335,7 +352,7 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0
335
352
  }
336
353
  if (isRetryableNetworkError(e) && attempt < retries) {
337
354
  clearTimeout(timer);
338
- await sleep(backoffDelayMs(attempt));
355
+ await sleep(backoffDelayMs(attempt), callerSignal);
339
356
  attempt++;
340
357
  continue;
341
358
  }
@@ -351,6 +368,21 @@ function headers(apiKey) {
351
368
  if (apiKey) h["XAPI-Key"] = apiKey;
352
369
  return h;
353
370
  }
371
+ function apiKeyApiRequest(apiHost, apiKey, path, options = {}) {
372
+ const method = options.method ?? "GET";
373
+ const requestHeaders = { "XAPI-KEY": apiKey };
374
+ if (options.body !== void 0) requestHeaders["Content-Type"] = "application/json";
375
+ return request(
376
+ `${scheme(apiHost)}://${apiHost}${path}`,
377
+ {
378
+ method,
379
+ headers: requestHeaders,
380
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
381
+ },
382
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
383
+ options.retries ?? 0
384
+ );
385
+ }
354
386
  function baseUrl(opts) {
355
387
  return `${scheme(opts.actionHost)}://${opts.actionHost}`;
356
388
  }
@@ -794,24 +826,43 @@ var sandboxAudit = (opts, id, kind, page = 1, pageSize = 100) => sandboxRequest(
794
826
  );
795
827
  async function sandboxWait(opts, id, wanted, timeoutMs = 3e5, intervalMs = 2e3, signal) {
796
828
  const deadline = Date.now() + timeoutMs;
829
+ const deadlineController = new AbortController();
830
+ const abortFromCaller = () => deadlineController.abort();
831
+ const deadlineTimer = setTimeout(() => deadlineController.abort(), Math.max(0, timeoutMs));
832
+ if (signal?.aborted) deadlineController.abort();
833
+ else signal?.addEventListener("abort", abortFromCaller, { once: true });
797
834
  let last;
798
- while (Date.now() < deadline) {
799
- if (signal?.aborted) throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(" or ")}`);
800
- last = await sandboxGet(opts, id, signal);
801
- const state = String(last.observedState || "");
802
- if (wanted.includes(state)) return last;
803
- if (["FAILED", "TERMINATED"].includes(state) && !wanted.includes(state)) {
804
- throw new Error(`sandbox ${id} entered ${state} while waiting for ${wanted.join(" or ")}`);
835
+ try {
836
+ while (Date.now() < deadline) {
837
+ if (signal?.aborted) throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(" or ")}`);
838
+ try {
839
+ last = await sandboxGet(opts, id, deadlineController.signal);
840
+ } catch (error) {
841
+ if (signal?.aborted) {
842
+ throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(" or ")}`);
843
+ }
844
+ if (Date.now() >= deadline) break;
845
+ throw error;
846
+ }
847
+ if (Date.now() >= deadline) break;
848
+ const state = String(last.observedState || "");
849
+ if (wanted.includes(state)) return last;
850
+ if (["FAILED", "TERMINATED"].includes(state) && !wanted.includes(state)) {
851
+ throw new Error(`sandbox ${id} entered ${state} while waiting for ${wanted.join(" or ")}`);
852
+ }
853
+ await new Promise((resolve2) => {
854
+ const done = () => {
855
+ clearTimeout(timer);
856
+ signal?.removeEventListener("abort", done);
857
+ resolve2();
858
+ };
859
+ const timer = setTimeout(done, Math.min(intervalMs, Math.max(0, deadline - Date.now())));
860
+ signal?.addEventListener("abort", done, { once: true });
861
+ });
805
862
  }
806
- await new Promise((resolve2) => {
807
- const done = () => {
808
- clearTimeout(timer);
809
- signal?.removeEventListener("abort", done);
810
- resolve2();
811
- };
812
- const timer = setTimeout(done, Math.min(intervalMs, Math.max(0, deadline - Date.now())));
813
- signal?.addEventListener("abort", done, { once: true });
814
- });
863
+ } finally {
864
+ clearTimeout(deadlineTimer);
865
+ signal?.removeEventListener("abort", abortFromCaller);
815
866
  }
816
867
  throw new Error(
817
868
  `sandbox ${id} did not enter ${wanted.join(" or ")} within ${timeoutMs}ms (last state: ${last?.observedState || "unknown"})`
@@ -835,6 +886,7 @@ export {
835
886
  HttpError,
836
887
  isRetryableRequestError,
837
888
  request,
889
+ apiKeyApiRequest,
838
890
  actionList,
839
891
  actionSearch,
840
892
  actionCategories,