@ingram-cloud/sdk 1.5.0 → 1.6.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
@@ -1,11 +1,10 @@
1
1
  # `@ingram-cloud/sdk`
2
2
 
3
- The Ingram Cloud `/v1` API wire contract in TypeScript — **Zod request/response
4
- schemas + SSE/webhook event types + JSON response types** plus a typed
5
- **management-plane client** built on it. The schemas are hand-authored and are
6
- the **source of truth for the wire**: the API imports the same schemas to
7
- validate requests and to emit its OpenAPI document, and the `IC*` response
8
- types are inferred from them.
3
+ The Ingram Cloud `/v1` API contract in TypeScript: Zod request/response
4
+ schemas, SSE/webhook event types, JSON response types, and a typed
5
+ management-plane client built on them. The schemas are hand-authored; the API
6
+ imports them to validate requests and to emit its OpenAPI document, and the
7
+ `IC*` response types are inferred from them.
9
8
 
10
9
  ```ts
11
10
  import { schemas } from "@ingram-cloud/sdk";
@@ -33,42 +32,42 @@ const smith = await ic.smiths.create({ external_id: "user-42" });
33
32
 
34
33
  ## Exports
35
34
 
36
- - `.` the `schemas` Zod map plus the SSE/webhook event types (`EVENT_TYPES`,
35
+ - `.`: the `schemas` Zod map plus the SSE/webhook event types (`EVENT_TYPES`,
37
36
  `webhookEvent`, `streamFrame`, …).
38
- - `./schemas` — just the Zod `schemas` map.
39
- - `./zod` the same schemas as individual named exports, one module per resource.
40
- - `./responses` the `IC*` TypeScript types for the JSON response bodies.
41
- Zod-free; `import type` these to stay dependency-light.
42
- - `./client` `IngramCloud`, the typed management-plane REST client. Method
43
- inputs are `z.input`-inferred from the same schemas the API validates with,
44
- so the client can't drift from the contract. Zod-free at runtime (type-only
45
- imports; transport is the global `fetch`). Auth is a pluggable token seam:
46
- a static bearer or a per-request minting function; smith-scoped calls made
47
- with a tenant token pass `{ smith }` (the `IC-Smith-Id` header). Non-2xx
48
- throws `ICError { status, code, requestId }`.
37
+ - `./schemas`: the Zod `schemas` map only.
38
+ - `./zod`: the same schemas as individual named exports, one module per resource.
39
+ - `./responses`: the `IC*` TypeScript types for the JSON response bodies.
40
+ Zod-free; `import type` these.
41
+ - `./client`: `IngramCloud`, the typed management-plane REST client. Method
42
+ inputs are `z.input`-inferred from the schemas the API validates with. Zod-free
43
+ at runtime (type-only imports; transport is the global `fetch`). Auth is a
44
+ static bearer or a per-request minting function. Smith-scoped calls made with
45
+ a tenant token pass `{ smith }`, sent as the `IC-Smith-Id` header. Non-2xx
46
+ throws `ICError { status, code, requestId }`. A 429 or 503 that names a
47
+ `Retry-After` is retried, up to four attempts and a minute's wait; a 402 is
48
+ never retried.
49
49
 
50
- The OpenAPI document is served by the API itself (`/openapi.json`), emitted from
51
- these schemas — it is no longer shipped as a file in this package.
50
+ The OpenAPI document is served by the API at `/openapi.json`, emitted from these
51
+ schemas.
52
52
 
53
- The client is the **management plane** only. The **data plane** stays on
54
- industry standards: chat rides the OpenAI-compatible surface use
55
- `@ingram-cloud/ai-sdk` and the standard `@ai-sdk/*` types for that. The
56
- native run stream is exposed raw (`smiths.runs.stream` returns the SSE
57
- `Response` unconsumed).
53
+ The client covers the management plane only. Chat goes through the
54
+ OpenAI-compatible surface: use `@ingram-cloud/ai-sdk` and the standard
55
+ `@ai-sdk/*` types. The native run stream is exposed raw: `smiths.runs.stream`
56
+ returns the SSE `Response` unconsumed.
58
57
 
59
58
  > Ships compiled ESM (`dist/`) alongside the TypeScript source (`ts/`). Node
60
- > and bundlers load `dist/` no transpile config needed. Types resolve straight
61
- > to the source, and Bun (the `bun` export condition) runs the source directly.
59
+ > and bundlers load `dist/`. Types resolve to the source, and Bun (the `bun`
60
+ > export condition) runs the source directly.
62
61
 
63
62
  ## Coverage
64
63
 
65
64
  Every resource's request bodies and non-streaming JSON responses are typed as
66
- precise Zod (one module per resource under `./zod`), and the `IC*` types are
67
- inferred from them. The **streaming/union** endpoints (`/runs` stream,
68
- `/chat/completions`, `/responses` — a stream *or* JSON from one handler), deployment
69
- **webhook acks**, and the OAuth **redirect** are not expressible as a single
70
- response schema, so they're not in the typed surface; the `{v:1}` webhook/feed
71
- envelope and the SSE run-stream frames are the hand-authored `./events` half.
65
+ Zod (one module per resource under `./zod`), with the `IC*` types inferred from
66
+ them. Not in the typed surface, because no single response schema expresses
67
+ them: the streaming/union endpoints (`/runs` stream, `/chat/completions`,
68
+ `/responses`, each a stream or JSON from one handler), deployment webhook acks,
69
+ and the OAuth redirect. The `{v:1}` webhook/feed envelope and the SSE
70
+ run-stream frames are the hand-authored `./events` half.
72
71
 
73
- The OpenAI-compatible stream chunks themselves are standard use the `@ai-sdk/*`
74
- types rather than redefining them here.
72
+ The OpenAI-compatible stream chunks are standard; use the `@ai-sdk/*` types for
73
+ them.
package/dist/client.js CHANGED
@@ -19,6 +19,61 @@ export class ICError extends Error {
19
19
  }
20
20
  }
21
21
  const enc = encodeURIComponent;
22
+ /** Attempts per request, including the first. Small on purpose: the server
23
+ * tells us when to come back, so this is a bound on pathological cases, not
24
+ * a backoff strategy. */
25
+ const MAX_ATTEMPTS = 4;
26
+ /** Longest we will sit out one `Retry-After`. A server (or an intermediary that
27
+ * never heard of this API) can name an hour; a client library must not silently
28
+ * block a caller for one. Past this we stop retrying and surface the refusal, so
29
+ * the caller decides. */
30
+ const MAX_WAIT_MS = 60_000;
31
+ /**
32
+ * How long this response says to wait, or null if it does not say — which is
33
+ * itself the answer: a 402 carries no `Retry-After` because the wallet will not
34
+ * refill because we asked twice.
35
+ *
36
+ * RFC 9110 allows both forms, and intermediaries do send the date one, so parse
37
+ * both. Anything unparseable is "no usable instruction", never a zero-delay
38
+ * hammer at an upstream that is already struggling.
39
+ */
40
+ function retryAfterMs(res, now) {
41
+ const raw = res.headers.get("retry-after")?.trim();
42
+ if (!raw)
43
+ return null;
44
+ const seconds = Number(raw);
45
+ const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(raw) - now;
46
+ if (!Number.isFinite(ms))
47
+ return null;
48
+ return Math.max(0, ms);
49
+ }
50
+ /** Retry only what retrying can fix, and only when told how long to wait. */
51
+ function retryDelay(res, now) {
52
+ if (res.status !== 429 && res.status !== 503)
53
+ return null;
54
+ const ms = retryAfterMs(res, now);
55
+ return ms === null || ms > MAX_WAIT_MS ? null : ms;
56
+ }
57
+ /** The `Retry-After` wait, abortable: a caller cancelling mid-wait should not
58
+ * sit out the rest of a Retry-After that can be tens of seconds — reject as
59
+ * soon as `signal` fires, the same way an aborted `transport()` call would. */
60
+ function sleep(ms, signal) {
61
+ if (!signal)
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ if (signal.aborted)
64
+ return Promise.reject(signal.reason ?? new Error("aborted"));
65
+ return new Promise((resolve, reject) => {
66
+ const timer = setTimeout(() => {
67
+ signal.removeEventListener("abort", onAbort);
68
+ resolve();
69
+ }, ms);
70
+ const onAbort = () => {
71
+ clearTimeout(timer);
72
+ reject(signal.reason ?? new Error("aborted"));
73
+ };
74
+ signal.addEventListener("abort", onAbort, { once: true });
75
+ });
76
+ }
22
77
  function qs(query) {
23
78
  if (!query)
24
79
  return "";
@@ -105,14 +160,23 @@ export class IngramCloud {
105
160
  ...(opts.smith ? { "ic-smith-id": opts.smith } : {}),
106
161
  ...opts.headers,
107
162
  };
108
- const res = await this.transport(`${this.base}/v1${path}${qs(opts.query)}`, {
109
- ...this.requestInit,
110
- method,
111
- headers,
112
- body: opts.rawBody ??
113
- (opts.body !== undefined ? JSON.stringify(opts.body) : undefined),
114
- signal: opts.signal,
115
- });
163
+ let res;
164
+ for (let attempt = 1;; attempt++) {
165
+ res = await this.transport(`${this.base}/v1${path}${qs(opts.query)}`, {
166
+ ...this.requestInit,
167
+ method,
168
+ headers,
169
+ body: opts.rawBody ??
170
+ (opts.body !== undefined ? JSON.stringify(opts.body) : undefined),
171
+ signal: opts.signal,
172
+ });
173
+ if (res.ok || attempt >= MAX_ATTEMPTS)
174
+ break;
175
+ const wait = retryDelay(res, Date.now());
176
+ if (wait === null)
177
+ break;
178
+ await sleep(wait, opts.signal);
179
+ }
116
180
  if (!res.ok) {
117
181
  const body = await res.text().catch(() => "");
118
182
  let code = `http_${res.status}`;
@@ -27,6 +27,15 @@ export const ApprovalOut = z
27
27
  tool: z.string().nullable(),
28
28
  /** The tool-call arguments awaiting a decision; `{}` when none. */
29
29
  args: z.record(z.string(), z.unknown()),
30
+ /** Set when the pause is a remote MCP tool asking the end-user for input
31
+ * (not a gate before a tool runs): the prompt and the JSON Schema the
32
+ * answer must match. Approve with `content` to answer it. */
33
+ elicitation: z
34
+ .object({
35
+ message: z.string(),
36
+ requested_schema: z.record(z.string(), z.unknown()),
37
+ })
38
+ .nullable(),
30
39
  /** pending | approved | rejected. */
31
40
  status: z.string(),
32
41
  actor: z.string().nullable(),
package/dist/zod/runs.js CHANGED
@@ -124,6 +124,9 @@ export const Submit = z
124
124
  result: z.record(z.string(), z.unknown()).nullish(),
125
125
  approval_id: z.string().nullish(),
126
126
  decision: z.string().nullish(),
127
+ /** The answer to an approval's `elicitation`, matching its
128
+ * `requested_schema`; with `decision: "approve"`. */
129
+ content: z.record(z.string(), z.unknown()).nullish(),
127
130
  actor: z.string().nullish(),
128
131
  reason: z.string().nullish(),
129
132
  stream: z.boolean().optional(),
package/package.json CHANGED
@@ -1,26 +1,29 @@
1
1
  {
2
2
  "name": "@ingram-cloud/sdk",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Typed wire contract + management-plane client for the Ingram Cloud API: Zod schemas to validate request/response bodies, TypeScript types for the JSON you read back, SSE/webhook event types, and a typed REST client.",
5
- "license": "MIT",
6
- "type": "module",
7
- "homepage": "https://cloud.ingram.tech",
8
5
  "keywords": [
9
- "ingram-cloud",
6
+ "api",
10
7
  "ingram",
8
+ "ingram-cloud",
9
+ "openapi",
11
10
  "sdk",
12
- "api",
13
11
  "wire",
14
- "zod",
15
- "openapi"
12
+ "zod"
16
13
  ],
14
+ "homepage": "https://github.com/ingram-technologies/ingram-cloud-sdks/tree/main/packages/sdk#readme",
15
+ "bugs": "https://github.com/ingram-technologies/ingram-cloud-sdks/issues",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ingram-technologies/ingram-cloud-sdks.git",
20
+ "directory": "packages/sdk"
21
+ },
17
22
  "files": [
18
23
  "ts",
19
24
  "dist"
20
25
  ],
21
- "publishConfig": {
22
- "access": "public"
23
- },
26
+ "type": "module",
24
27
  "exports": {
25
28
  ".": {
26
29
  "types": "./ts/index.ts",
@@ -48,18 +51,24 @@
48
51
  "default": "./dist/client.js"
49
52
  }
50
53
  },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
51
57
  "scripts": {
52
58
  "build": "tsc -p tsconfig.build.json",
53
59
  "typecheck": "tsc --noEmit",
54
- "lint": "nk lint",
55
- "format": "nk format",
56
- "prepublishOnly": "bun run build",
57
- "check": "nk check"
58
- },
59
- "devDependencies": {
60
- "@ingram-tech/nk-dev": "^0.10.0"
60
+ "lint": "oxlint",
61
+ "format": "oxfmt --write .",
62
+ "test": "vitest run",
63
+ "prepack": "bun run build"
61
64
  },
62
65
  "dependencies": {
63
66
  "zod": "^4.4.3"
67
+ },
68
+ "devDependencies": {
69
+ "vitest": "^4.1.10"
70
+ },
71
+ "engines": {
72
+ "node": ">=20"
64
73
  }
65
74
  }
package/ts/client.ts CHANGED
@@ -238,6 +238,61 @@ export interface PageOpts {
238
238
 
239
239
  const enc = encodeURIComponent;
240
240
 
241
+ /** Attempts per request, including the first. Small on purpose: the server
242
+ * tells us when to come back, so this is a bound on pathological cases, not
243
+ * a backoff strategy. */
244
+ const MAX_ATTEMPTS = 4;
245
+
246
+ /** Longest we will sit out one `Retry-After`. A server (or an intermediary that
247
+ * never heard of this API) can name an hour; a client library must not silently
248
+ * block a caller for one. Past this we stop retrying and surface the refusal, so
249
+ * the caller decides. */
250
+ const MAX_WAIT_MS = 60_000;
251
+
252
+ /**
253
+ * How long this response says to wait, or null if it does not say — which is
254
+ * itself the answer: a 402 carries no `Retry-After` because the wallet will not
255
+ * refill because we asked twice.
256
+ *
257
+ * RFC 9110 allows both forms, and intermediaries do send the date one, so parse
258
+ * both. Anything unparseable is "no usable instruction", never a zero-delay
259
+ * hammer at an upstream that is already struggling.
260
+ */
261
+ function retryAfterMs(res: Response, now: number): number | null {
262
+ const raw = res.headers.get("retry-after")?.trim();
263
+ if (!raw) return null;
264
+ const seconds = Number(raw);
265
+ const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(raw) - now;
266
+ if (!Number.isFinite(ms)) return null;
267
+ return Math.max(0, ms);
268
+ }
269
+
270
+ /** Retry only what retrying can fix, and only when told how long to wait. */
271
+ function retryDelay(res: Response, now: number): number | null {
272
+ if (res.status !== 429 && res.status !== 503) return null;
273
+ const ms = retryAfterMs(res, now);
274
+ return ms === null || ms > MAX_WAIT_MS ? null : ms;
275
+ }
276
+
277
+ /** The `Retry-After` wait, abortable: a caller cancelling mid-wait should not
278
+ * sit out the rest of a Retry-After that can be tens of seconds — reject as
279
+ * soon as `signal` fires, the same way an aborted `transport()` call would. */
280
+ function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
281
+ if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
282
+ if (signal.aborted) return Promise.reject(signal.reason ?? new Error("aborted"));
283
+ return new Promise((resolve, reject) => {
284
+ const timer = setTimeout(() => {
285
+ signal.removeEventListener("abort", onAbort);
286
+ resolve();
287
+ }, ms);
288
+ const onAbort = () => {
289
+ clearTimeout(timer);
290
+ reject(signal.reason ?? new Error("aborted"));
291
+ };
292
+ signal.addEventListener("abort", onAbort, { once: true });
293
+ });
294
+ }
295
+
241
296
  function qs(query: Query | undefined): string {
242
297
  if (!query) return "";
243
298
  const p = new URLSearchParams();
@@ -347,15 +402,22 @@ export class IngramCloud {
347
402
  ...(opts.smith ? { "ic-smith-id": opts.smith } : {}),
348
403
  ...opts.headers,
349
404
  };
350
- const res = await this.transport(`${this.base}/v1${path}${qs(opts.query)}`, {
351
- ...this.requestInit,
352
- method,
353
- headers,
354
- body:
355
- opts.rawBody ??
356
- (opts.body !== undefined ? JSON.stringify(opts.body) : undefined),
357
- signal: opts.signal,
358
- });
405
+ let res!: Response;
406
+ for (let attempt = 1; ; attempt++) {
407
+ res = await this.transport(`${this.base}/v1${path}${qs(opts.query)}`, {
408
+ ...this.requestInit,
409
+ method,
410
+ headers,
411
+ body:
412
+ opts.rawBody ??
413
+ (opts.body !== undefined ? JSON.stringify(opts.body) : undefined),
414
+ signal: opts.signal,
415
+ });
416
+ if (res.ok || attempt >= MAX_ATTEMPTS) break;
417
+ const wait = retryDelay(res, Date.now());
418
+ if (wait === null) break;
419
+ await sleep(wait, opts.signal);
420
+ }
359
421
  if (!res.ok) {
360
422
  const body = await res.text().catch(() => "");
361
423
  let code = `http_${res.status}`;
@@ -28,6 +28,15 @@ export const ApprovalOut = z
28
28
  tool: z.string().nullable(),
29
29
  /** The tool-call arguments awaiting a decision; `{}` when none. */
30
30
  args: z.record(z.string(), z.unknown()),
31
+ /** Set when the pause is a remote MCP tool asking the end-user for input
32
+ * (not a gate before a tool runs): the prompt and the JSON Schema the
33
+ * answer must match. Approve with `content` to answer it. */
34
+ elicitation: z
35
+ .object({
36
+ message: z.string(),
37
+ requested_schema: z.record(z.string(), z.unknown()),
38
+ })
39
+ .nullable(),
31
40
  /** pending | approved | rejected. */
32
41
  status: z.string(),
33
42
  actor: z.string().nullable(),
package/ts/zod/runs.ts CHANGED
@@ -134,6 +134,9 @@ export const Submit = z
134
134
  result: z.record(z.string(), z.unknown()).nullish(),
135
135
  approval_id: z.string().nullish(),
136
136
  decision: z.string().nullish(),
137
+ /** The answer to an approval's `elicitation`, matching its
138
+ * `requested_schema`; with `decision: "approve"`. */
139
+ content: z.record(z.string(), z.unknown()).nullish(),
137
140
  actor: z.string().nullish(),
138
141
  reason: z.string().nullish(),
139
142
  stream: z.boolean().optional(),
@@ -1 +0,0 @@
1
- {"version":1,"sessions":{"409c438b-72a6-412d-9a44-013a81c0f846":{"updatedAt":1784658629665,"files":{"/home/adys/src/cloud.ingram.tech/sdk/ts/client.ts":{"editCount":2,"findings":[]}}}}}