@axivoy/api 0.0.0 → 1.0.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 +16 -2
- package/dist/auth.d.ts +13 -0
- package/dist/auth.js +41 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.js +93 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +29 -0
- package/dist/generated/schema.d.ts +26161 -0
- package/dist/generated/schema.js +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/webhooks.d.ts +41 -0
- package/dist/webhooks.js +51 -0
- package/package.json +29 -3
package/README.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @axivoy/api
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed client for the [Axivoy API](https://app.axivoy.ai/developers): every published operation, webhook payload types, refusals as errors and webhook signature checks. Generated from the public OpenAPI reference; no runtime dependencies.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```typescript
|
|
6
|
+
import { clientCredentials, createAxivoyClient } from '@axivoy/api';
|
|
7
|
+
|
|
8
|
+
const axivoy = createAxivoyClient({
|
|
9
|
+
token: clientCredentials({ clientId: process.env.AXIVOY_CLIENT_ID!, clientSecret: process.env.AXIVOY_CLIENT_SECRET! }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const page = await axivoy.call('loads.list', { limit: 25 });
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Guide: [Client libraries](https://app.axivoy.ai/developers/guides/client-libraries). Node 20+, Deno, Bun and browsers.
|
|
16
|
+
|
|
17
|
+
## Maintainers
|
|
18
|
+
|
|
19
|
+
Source lives in the Axivoy TMS repository under `clients/typescript` and is released by the `client-libraries` workflow when the reference changes. `npm run generate` rebuilds `src/generated/schema.ts` from `docs/api/openapi-public.json`; `npm run build`, `npm test`; `npm run smoke` runs against a live install (see `scripts/smoke.mjs`).
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DEFAULT_TOKEN_URL = "https://app.axivoy.ai/oauth/token";
|
|
2
|
+
export interface ClientCredentialsOptions {
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
/** Defaults to `https://app.axivoy.ai/oauth/token`. */
|
|
6
|
+
tokenUrl?: string;
|
|
7
|
+
fetch?: typeof fetch;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A token source for an API key: exchanges its client ID and secret for an
|
|
11
|
+
* access token and reuses it until a minute before it expires.
|
|
12
|
+
*/
|
|
13
|
+
export declare function clientCredentials(options: ClientCredentialsOptions): () => Promise<string>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AxivoyError } from './errors.js';
|
|
2
|
+
export const DEFAULT_TOKEN_URL = 'https://app.axivoy.ai/oauth/token';
|
|
3
|
+
/**
|
|
4
|
+
* A token source for an API key: exchanges its client ID and secret for an
|
|
5
|
+
* access token and reuses it until a minute before it expires.
|
|
6
|
+
*/
|
|
7
|
+
export function clientCredentials(options) {
|
|
8
|
+
const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
9
|
+
let cached = null;
|
|
10
|
+
let pending = null;
|
|
11
|
+
const exchange = async () => {
|
|
12
|
+
const response = await fetcher(options.tokenUrl ?? DEFAULT_TOKEN_URL, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
15
|
+
body: new URLSearchParams({
|
|
16
|
+
grant_type: 'client_credentials',
|
|
17
|
+
client_id: options.clientId,
|
|
18
|
+
client_secret: options.clientSecret,
|
|
19
|
+
}).toString(),
|
|
20
|
+
});
|
|
21
|
+
const body = (await response.json().catch(() => ({})));
|
|
22
|
+
if (!response.ok || typeof body.access_token !== 'string') {
|
|
23
|
+
throw new AxivoyError(response.status, {
|
|
24
|
+
code: typeof body.error === 'string' ? body.error : 'token_failed',
|
|
25
|
+
message: typeof body.error_description === 'string' ? body.error_description : `Token request failed with HTTP ${response.status}.`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const lifetime = typeof body.expires_in === 'number' ? body.expires_in : 3600;
|
|
29
|
+
cached = { token: body.access_token, expiresAt: Date.now() + Math.max(0, lifetime - 60) * 1000 };
|
|
30
|
+
return body.access_token;
|
|
31
|
+
};
|
|
32
|
+
return async () => {
|
|
33
|
+
if (cached !== null && Date.now() < cached.expiresAt) {
|
|
34
|
+
return cached.token;
|
|
35
|
+
}
|
|
36
|
+
pending ??= exchange().finally(() => {
|
|
37
|
+
pending = null;
|
|
38
|
+
});
|
|
39
|
+
return pending;
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { paths } from './generated/schema.js';
|
|
2
|
+
type OperationPath = Extract<keyof paths, `/ops/${string}`>;
|
|
3
|
+
/** Every operation key the public reference publishes, such as `loads.list`. */
|
|
4
|
+
export type OperationKey = OperationPath extends `/ops/${infer Key}` ? Key : never;
|
|
5
|
+
type Post<K extends OperationKey> = paths[`/ops/${K}`] extends {
|
|
6
|
+
post: infer P;
|
|
7
|
+
} ? P : never;
|
|
8
|
+
/** The JSON body an operation takes. */
|
|
9
|
+
export type OperationInput<K extends OperationKey> = Post<K> extends {
|
|
10
|
+
requestBody?: {
|
|
11
|
+
content: {
|
|
12
|
+
'application/json': infer Body;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
} ? Body : Record<string, never>;
|
|
16
|
+
/** The `result` an operation answers with. */
|
|
17
|
+
export type OperationResult<K extends OperationKey> = Post<K> extends {
|
|
18
|
+
responses: {
|
|
19
|
+
200: {
|
|
20
|
+
content: {
|
|
21
|
+
'application/json': {
|
|
22
|
+
result: infer Result;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
} ? Result : unknown;
|
|
28
|
+
/** The dry-run answer: what would happen, with nothing written. */
|
|
29
|
+
export interface DryRunResult {
|
|
30
|
+
outcome: 'would_run' | 'would_propose';
|
|
31
|
+
input: Record<string, unknown>;
|
|
32
|
+
simulation?: unknown;
|
|
33
|
+
}
|
|
34
|
+
export type TokenSource = string | (() => string | Promise<string>);
|
|
35
|
+
export interface AxivoyClientOptions {
|
|
36
|
+
/** An access token, or a function returning a fresh one (OAuth or client credentials). */
|
|
37
|
+
token: TokenSource;
|
|
38
|
+
/** Defaults to `https://api.axivoy.ai/v1`. */
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
/** Retries after a network error, 429, 409 `in_flight` or 5xx. Defaults to 3. */
|
|
41
|
+
maxRetries?: number;
|
|
42
|
+
/** Replace `fetch` (tests, proxies). */
|
|
43
|
+
fetch?: typeof fetch;
|
|
44
|
+
}
|
|
45
|
+
export interface CallOptions {
|
|
46
|
+
/** Same key and body replays the first answer. Generated per call when omitted, and kept across retries. */
|
|
47
|
+
idempotencyKey?: string;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
export declare const DEFAULT_BASE_URL = "https://api.axivoy.ai/v1";
|
|
51
|
+
export declare class AxivoyClient {
|
|
52
|
+
private readonly options;
|
|
53
|
+
private readonly baseUrl;
|
|
54
|
+
private readonly maxRetries;
|
|
55
|
+
private readonly fetcher;
|
|
56
|
+
constructor(options: AxivoyClientOptions);
|
|
57
|
+
/**
|
|
58
|
+
* Runs one operation. Resolves with its `result`; rejects with
|
|
59
|
+
* `AxivoyError` when refused and `AxivoyProposal` when sent for approval.
|
|
60
|
+
*/
|
|
61
|
+
call<K extends OperationKey>(key: K, input?: OperationInput<K>, options?: CallOptions): Promise<OperationResult<K>>;
|
|
62
|
+
/** Validates and authorizes without running anything (`Axivoy-Dry-Run`). */
|
|
63
|
+
dryRun<K extends OperationKey>(key: K, input?: OperationInput<K>, options?: CallOptions): Promise<DryRunResult>;
|
|
64
|
+
private post;
|
|
65
|
+
private token;
|
|
66
|
+
}
|
|
67
|
+
export declare function createAxivoyClient(options: AxivoyClientOptions): AxivoyClient;
|
|
68
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { AxivoyError, AxivoyProposal } from './errors.js';
|
|
2
|
+
export const DEFAULT_BASE_URL = 'https://api.axivoy.ai/v1';
|
|
3
|
+
const RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
4
|
+
export class AxivoyClient {
|
|
5
|
+
options;
|
|
6
|
+
baseUrl;
|
|
7
|
+
maxRetries;
|
|
8
|
+
fetcher;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
12
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
13
|
+
this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Runs one operation. Resolves with its `result`; rejects with
|
|
17
|
+
* `AxivoyError` when refused and `AxivoyProposal` when sent for approval.
|
|
18
|
+
*/
|
|
19
|
+
async call(key, input, options = {}) {
|
|
20
|
+
const body = await this.post(key, input, options, false);
|
|
21
|
+
return body.result;
|
|
22
|
+
}
|
|
23
|
+
/** Validates and authorizes without running anything (`Axivoy-Dry-Run`). */
|
|
24
|
+
async dryRun(key, input, options = {}) {
|
|
25
|
+
const body = await this.post(key, input, options, true);
|
|
26
|
+
return body.dry_run;
|
|
27
|
+
}
|
|
28
|
+
async post(key, input, options, dryRun) {
|
|
29
|
+
const idempotencyKey = options.idempotencyKey ?? globalThis.crypto.randomUUID();
|
|
30
|
+
const payload = JSON.stringify(input ?? {});
|
|
31
|
+
for (let attempt = 0;; attempt++) {
|
|
32
|
+
const headers = {
|
|
33
|
+
Authorization: `Bearer ${await this.token()}`,
|
|
34
|
+
Accept: 'application/json',
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Idempotency-Key': idempotencyKey,
|
|
37
|
+
};
|
|
38
|
+
if (dryRun) {
|
|
39
|
+
headers['Axivoy-Dry-Run'] = 'true';
|
|
40
|
+
}
|
|
41
|
+
let response;
|
|
42
|
+
try {
|
|
43
|
+
response = await this.fetcher(`${this.baseUrl}/ops/${encodeURIComponent(key)}`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers,
|
|
46
|
+
body: payload,
|
|
47
|
+
signal: options.signal,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (options.signal?.aborted || attempt >= this.maxRetries) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
await sleep(backoff(attempt));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const body = (await response.json().catch(() => ({})));
|
|
58
|
+
if (response.status === 200) {
|
|
59
|
+
return body;
|
|
60
|
+
}
|
|
61
|
+
if (response.status === 202 && body.proposal) {
|
|
62
|
+
throw new AxivoyProposal(body.proposal);
|
|
63
|
+
}
|
|
64
|
+
const error = (body.error ?? { code: 'unexpected', message: `HTTP ${response.status}` });
|
|
65
|
+
const retryable = RETRY_STATUSES.has(response.status) || (response.status === 409 && error.reason === 'in_flight');
|
|
66
|
+
if (!retryable || attempt >= this.maxRetries) {
|
|
67
|
+
throw new AxivoyError(response.status, error);
|
|
68
|
+
}
|
|
69
|
+
await sleep(retryAfter(response) ?? backoff(attempt));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async token() {
|
|
73
|
+
const source = this.options.token;
|
|
74
|
+
return typeof source === 'function' ? await source() : source;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function createAxivoyClient(options) {
|
|
78
|
+
return new AxivoyClient(options);
|
|
79
|
+
}
|
|
80
|
+
function backoff(attempt) {
|
|
81
|
+
return Math.min(8000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250);
|
|
82
|
+
}
|
|
83
|
+
function retryAfter(response) {
|
|
84
|
+
const header = response.headers.get('Retry-After');
|
|
85
|
+
if (header === null) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const seconds = Number(header);
|
|
89
|
+
return Number.isFinite(seconds) ? Math.min(60_000, seconds * 1000) : null;
|
|
90
|
+
}
|
|
91
|
+
function sleep(ms) {
|
|
92
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The `error` body every refused call answers with. */
|
|
2
|
+
export interface AxivoyErrorBody {
|
|
3
|
+
code: string;
|
|
4
|
+
reason?: string;
|
|
5
|
+
message: string;
|
|
6
|
+
fields?: Record<string, string[]>;
|
|
7
|
+
}
|
|
8
|
+
/** A refused call: `status` is the HTTP status, `code` the stable refusal code. */
|
|
9
|
+
export declare class AxivoyError extends Error {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly reason: string | undefined;
|
|
13
|
+
readonly fields: Record<string, string[]>;
|
|
14
|
+
constructor(status: number, body: AxivoyErrorBody);
|
|
15
|
+
}
|
|
16
|
+
/** The proposal envelope: nothing ran yet; a person approves it in Axivoy. */
|
|
17
|
+
export interface ProposalBody {
|
|
18
|
+
id: string;
|
|
19
|
+
status: 'proposed';
|
|
20
|
+
approval_url?: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when a write was sent for approval (HTTP 202). Check its outcome
|
|
25
|
+
* later with `approvals.list` and `proposal_id`.
|
|
26
|
+
*/
|
|
27
|
+
export declare class AxivoyProposal extends Error {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly approvalUrl: string | undefined;
|
|
30
|
+
constructor(body: ProposalBody);
|
|
31
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A refused call: `status` is the HTTP status, `code` the stable refusal code. */
|
|
2
|
+
export class AxivoyError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
reason;
|
|
6
|
+
fields;
|
|
7
|
+
constructor(status, body) {
|
|
8
|
+
super(body.message);
|
|
9
|
+
this.name = 'AxivoyError';
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.code = body.code;
|
|
12
|
+
this.reason = body.reason;
|
|
13
|
+
this.fields = body.fields ?? {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when a write was sent for approval (HTTP 202). Check its outcome
|
|
18
|
+
* later with `approvals.list` and `proposal_id`.
|
|
19
|
+
*/
|
|
20
|
+
export class AxivoyProposal extends Error {
|
|
21
|
+
id;
|
|
22
|
+
approvalUrl;
|
|
23
|
+
constructor(body) {
|
|
24
|
+
super(body.message);
|
|
25
|
+
this.name = 'AxivoyProposal';
|
|
26
|
+
this.id = body.id;
|
|
27
|
+
this.approvalUrl = body.approval_url;
|
|
28
|
+
}
|
|
29
|
+
}
|