@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 +34 -35
- package/dist/client.js +72 -8
- package/dist/zod/approvals.js +9 -0
- package/dist/zod/runs.js +3 -0
- package/package.json +27 -18
- package/ts/client.ts +71 -9
- package/ts/zod/approvals.ts +9 -0
- package/ts/zod/runs.ts +3 -0
- package/ts/zod/.impeccable/hook.cache.json +0 -1
package/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# `@ingram-cloud/sdk`
|
|
2
2
|
|
|
3
|
-
The Ingram Cloud `/v1` API
|
|
4
|
-
schemas
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
-
|
|
35
|
+
- `.`: the `schemas` Zod map plus the SSE/webhook event types (`EVENT_TYPES`,
|
|
37
36
|
`webhookEvent`, `streamFrame`, …).
|
|
38
|
-
- `./schemas
|
|
39
|
-
- `./zod
|
|
40
|
-
- `./responses
|
|
41
|
-
Zod-free; `import type` these
|
|
42
|
-
- `./client
|
|
43
|
-
inputs are `z.input`-inferred from the
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
a
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
51
|
-
|
|
50
|
+
The OpenAPI document is served by the API at `/openapi.json`, emitted from these
|
|
51
|
+
schemas.
|
|
52
52
|
|
|
53
|
-
The client
|
|
54
|
-
|
|
55
|
-
`@
|
|
56
|
-
|
|
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
|
|
61
|
-
>
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
74
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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}`;
|
package/dist/zod/approvals.js
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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": "
|
|
55
|
-
"format": "
|
|
56
|
-
"
|
|
57
|
-
"
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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}`;
|
package/ts/zod/approvals.ts
CHANGED
|
@@ -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":[]}}}}}
|