@zeyos/client 0.1.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +458 -0
- package/agents/README.md +66 -0
- package/agents/shared/business-app-benchmarks.md +111 -0
- package/agents/shared/zeyos-entity-map.md +142 -0
- package/agents/shared/zeyos-entity-reference.md +570 -0
- package/agents/shared/zeyos-query-patterns.md +89 -0
- package/agents/zeyos-account-intelligence/SKILL.md +34 -0
- package/agents/zeyos-account-intelligence/agents/openai.yaml +4 -0
- package/agents/zeyos-account-intelligence/references/workflows.md +84 -0
- package/agents/zeyos-billing-insights/SKILL.md +41 -0
- package/agents/zeyos-billing-insights/agents/openai.yaml +4 -0
- package/agents/zeyos-billing-insights/references/workflows.md +106 -0
- package/agents/zeyos-campaign-and-outreach/SKILL.md +44 -0
- package/agents/zeyos-campaign-and-outreach/agents/openai.yaml +4 -0
- package/agents/zeyos-campaign-and-outreach/references/workflows.md +100 -0
- package/agents/zeyos-collaboration-and-activity/SKILL.md +37 -0
- package/agents/zeyos-collaboration-and-activity/agents/openai.yaml +4 -0
- package/agents/zeyos-collaboration-and-activity/references/workflows.md +104 -0
- package/agents/zeyos-collections-and-dunning/SKILL.md +46 -0
- package/agents/zeyos-collections-and-dunning/agents/openai.yaml +4 -0
- package/agents/zeyos-collections-and-dunning/references/workflows.md +132 -0
- package/agents/zeyos-commerce-and-inventory/SKILL.md +38 -0
- package/agents/zeyos-commerce-and-inventory/agents/openai.yaml +4 -0
- package/agents/zeyos-commerce-and-inventory/references/workflows.md +101 -0
- package/agents/zeyos-mail-operations/SKILL.md +35 -0
- package/agents/zeyos-mail-operations/agents/openai.yaml +4 -0
- package/agents/zeyos-mail-operations/references/workflows.md +110 -0
- package/agents/zeyos-notes-and-sops/SKILL.md +31 -0
- package/agents/zeyos-notes-and-sops/agents/openai.yaml +4 -0
- package/agents/zeyos-notes-and-sops/references/workflows.md +85 -0
- package/agents/zeyos-platform-and-schema/SKILL.md +37 -0
- package/agents/zeyos-platform-and-schema/agents/openai.yaml +4 -0
- package/agents/zeyos-platform-and-schema/references/workflows.md +97 -0
- package/agents/zeyos-work-management/SKILL.md +45 -0
- package/agents/zeyos-work-management/agents/openai.yaml +4 -0
- package/agents/zeyos-work-management/references/workflows.md +148 -0
- package/docs/01-api-reference/01-data-retrieval.md +601 -0
- package/docs/01-api-reference/02-authentication.md +288 -0
- package/docs/01-api-reference/03-resources.md +270 -0
- package/docs/01-api-reference/04-schema.md +539 -0
- package/docs/01-api-reference/_category_.json +9 -0
- package/docs/02-javascript-client/01-getting-started.md +146 -0
- package/docs/02-javascript-client/02-authentication.md +287 -0
- package/docs/02-javascript-client/03-making-requests.md +572 -0
- package/docs/02-javascript-client/04-practical-guide.md +348 -0
- package/docs/02-javascript-client/_category_.json +9 -0
- package/docs/03-cli/01-getting-started.md +219 -0
- package/docs/03-cli/02-commands.md +407 -0
- package/docs/03-cli/03-configuration.md +220 -0
- package/docs/03-cli/_category_.json +9 -0
- package/docs/04-agent-workflows/00-coding-agents.md +35 -0
- package/docs/04-agent-workflows/01-agent-quickstart.md +147 -0
- package/docs/04-agent-workflows/02-agent-recipes.md +109 -0
- package/docs/04-agent-workflows/03-cli-coverage-and-escalation.md +65 -0
- package/docs/04-agent-workflows/_category_.json +9 -0
- package/docs/04-sample-apps/01-kanban.md +89 -0
- package/docs/04-sample-apps/02-crm.md +81 -0
- package/docs/04-sample-apps/03-dashboard.md +80 -0
- package/docs/04-sample-apps/_category_.json +9 -0
- package/docs/05-tutorials/00-application-developers.md +43 -0
- package/docs/05-tutorials/01-integration-architecture.md +60 -0
- package/docs/05-tutorials/02-build-your-own-zeyos-frontend.md +517 -0
- package/docs/05-tutorials/03-server-side-integrations.md +185 -0
- package/docs/05-tutorials/_category_.json +9 -0
- package/docs/intro.md +197 -0
- package/openapi/api.json +24308 -0
- package/openapi/auth.json +415 -0
- package/openapi/dbref.json +56223 -0
- package/openapi/oauth2.json +781 -0
- package/openapi/sdk.json +949 -0
- package/openapi/views.txt +642 -0
- package/package.json +49 -0
- package/samples/crm/README.md +28 -0
- package/samples/crm/index.html +327 -0
- package/samples/crm/js/api.js +208 -0
- package/samples/crm/js/auth.js +61 -0
- package/samples/crm/js/main.js +545 -0
- package/samples/crm/js/state.js +90 -0
- package/samples/crm/js/ui.js +51 -0
- package/samples/dashboard/README.md +28 -0
- package/samples/dashboard/index.html +280 -0
- package/samples/dashboard/js/api.js +197 -0
- package/samples/dashboard/js/auth.js +59 -0
- package/samples/dashboard/js/main.js +382 -0
- package/samples/dashboard/js/state.js +81 -0
- package/samples/dashboard/js/ui.js +48 -0
- package/samples/kanban/README.md +28 -0
- package/samples/kanban/index.html +263 -0
- package/samples/kanban/js/api.js +152 -0
- package/samples/kanban/js/auth.js +59 -0
- package/samples/kanban/js/constants.js +40 -0
- package/samples/kanban/js/kanban.js +246 -0
- package/samples/kanban/js/main.js +362 -0
- package/samples/kanban/js/modals.js +474 -0
- package/samples/kanban/js/settings.js +82 -0
- package/samples/kanban/js/state.js +118 -0
- package/samples/kanban/js/ui.js +49 -0
- package/scripts/generate-client.mjs +344 -0
- package/src/generated/operations.js +9772 -0
- package/src/generated/schema.js +8982 -0
- package/src/index.js +85 -0
- package/src/runtime/client.js +1208 -0
- package/src/runtime/error.js +29 -0
- package/src/runtime/http.js +174 -0
- package/src/runtime/request-shape.js +35 -0
- package/src/runtime/schema.js +206 -0
- package/src/runtime/suggest.js +74 -0
- package/src/runtime/token-store.js +105 -0
|
@@ -0,0 +1,1208 @@
|
|
|
1
|
+
import { GENERATED, SERVICES, SERVICE_KEYS } from '../generated/operations.js';
|
|
2
|
+
import { SCHEMA } from '../generated/schema.js';
|
|
3
|
+
import { ZeyosApiError, ZeyosValidationError } from './error.js';
|
|
4
|
+
import { buildUrl, httpRequest } from './http.js';
|
|
5
|
+
import {
|
|
6
|
+
OBJECT_CONTROL_KEYS as OBJECT_CONTROL_KEY_LIST,
|
|
7
|
+
REQUEST_CONTROL_KEYS
|
|
8
|
+
} from './request-shape.js';
|
|
9
|
+
import { createSchema } from './schema.js';
|
|
10
|
+
import { suggestClosest } from './suggest.js';
|
|
11
|
+
import { MemoryTokenStore, normalizeTokenSet, tokenResponseToTokenSet } from './token-store.js';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_RETRY = Object.freeze({
|
|
14
|
+
maxRetries: 2,
|
|
15
|
+
retryOn: Object.freeze([429, 503]),
|
|
16
|
+
baseDelayMs: 300,
|
|
17
|
+
maxDelayMs: 10000
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function normalizeRetry(retry) {
|
|
21
|
+
if (retry === false || retry === null) {
|
|
22
|
+
return { maxRetries: 0, retryOn: new Set(), baseDelayMs: 0, maxDelayMs: 0 };
|
|
23
|
+
}
|
|
24
|
+
const cfg = retry && typeof retry === 'object' ? retry : {};
|
|
25
|
+
const retryOn = Array.isArray(cfg.retryOn) ? cfg.retryOn : DEFAULT_RETRY.retryOn;
|
|
26
|
+
return {
|
|
27
|
+
maxRetries: Number.isInteger(cfg.maxRetries) && cfg.maxRetries >= 0 ? cfg.maxRetries : DEFAULT_RETRY.maxRetries,
|
|
28
|
+
retryOn: new Set(retryOn),
|
|
29
|
+
baseDelayMs: Number(cfg.baseDelayMs) > 0 ? Number(cfg.baseDelayMs) : DEFAULT_RETRY.baseDelayMs,
|
|
30
|
+
maxDelayMs: Number(cfg.maxDelayMs) > 0 ? Number(cfg.maxDelayMs) : DEFAULT_RETRY.maxDelayMs
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function abortableDelay(ms, signal) {
|
|
35
|
+
// Respect an already-aborted signal even when there is no delay to wait on,
|
|
36
|
+
// so a zero-delay retry (e.g. `Retry-After: 0`) does not fire another request
|
|
37
|
+
// after the caller has aborted.
|
|
38
|
+
if (signal?.aborted) {
|
|
39
|
+
return Promise.reject(signal.reason ?? new Error('Aborted'));
|
|
40
|
+
}
|
|
41
|
+
if (!(ms > 0)) return Promise.resolve();
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
if (signal?.aborted) {
|
|
44
|
+
reject(signal.reason ?? new Error('Aborted'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
49
|
+
resolve();
|
|
50
|
+
}, ms);
|
|
51
|
+
function onAbort() {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
reject(signal.reason ?? new Error('Aborted'));
|
|
54
|
+
}
|
|
55
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Honor a Retry-After header (seconds or HTTP-date), else exponential backoff
|
|
60
|
+
// with jitter, capped at maxDelayMs.
|
|
61
|
+
function computeRetryDelay(response, attempt, retryConfig) {
|
|
62
|
+
const header = response.headers?.['retry-after'];
|
|
63
|
+
// An empty or whitespace-only header carries no delay directive — `Number('')`
|
|
64
|
+
// is 0, which would otherwise short-circuit to a zero delay instead of the
|
|
65
|
+
// exponential backoff below.
|
|
66
|
+
const trimmedHeader = typeof header === 'string' ? header.trim() : header;
|
|
67
|
+
if (trimmedHeader != null && trimmedHeader !== '') {
|
|
68
|
+
const seconds = Number(trimmedHeader);
|
|
69
|
+
if (Number.isFinite(seconds)) {
|
|
70
|
+
return Math.min(retryConfig.maxDelayMs, Math.max(0, seconds * 1000));
|
|
71
|
+
}
|
|
72
|
+
const dateMs = Date.parse(trimmedHeader);
|
|
73
|
+
if (Number.isFinite(dateMs)) {
|
|
74
|
+
return Math.min(retryConfig.maxDelayMs, Math.max(0, dateMs - Date.now()));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const exp = retryConfig.baseDelayMs * Math.pow(2, attempt);
|
|
78
|
+
const jitter = Math.random() * retryConfig.baseDelayMs;
|
|
79
|
+
return Math.min(retryConfig.maxDelayMs, exp + jitter);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const AUTH_SCHEME_MAP = Object.freeze({
|
|
83
|
+
oauth: 'bearer',
|
|
84
|
+
token: 'bearer',
|
|
85
|
+
session: 'session',
|
|
86
|
+
basic: 'basic'
|
|
87
|
+
});
|
|
88
|
+
const PLATFORM_PRESETS = Object.freeze({
|
|
89
|
+
live: 'https://cloud.zeyos.com'
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const VALID_AUTH_MODES = new Set(['auto', 'oauth', 'session', 'none']);
|
|
93
|
+
const RESERVED_INPUT_KEYS = new Set(REQUEST_CONTROL_KEYS);
|
|
94
|
+
|
|
95
|
+
// Reserved keys that act as control *containers* and are only meaningful when
|
|
96
|
+
// object-valued. A scalar value for one of these (most commonly `query: 'term'`
|
|
97
|
+
// for ZeyOS full-text search) is a payload field, not a control directive, so it
|
|
98
|
+
// must not disable body inference or be excluded from the inferred body.
|
|
99
|
+
const OBJECT_CONTROL_KEYS = new Set(OBJECT_CONTROL_KEY_LIST);
|
|
100
|
+
|
|
101
|
+
function isObject(value) {
|
|
102
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isPlainObject(value) {
|
|
106
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function cloneValue(value) {
|
|
110
|
+
if (!Array.isArray(value) && !isPlainObject(value)) {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
return structuredClone(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function trimTrailingSlash(value) {
|
|
117
|
+
return String(value).replace(/\/+$/, '');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function toBase64(value) {
|
|
121
|
+
if (typeof Buffer !== 'undefined') {
|
|
122
|
+
return Buffer.from(value, 'utf8').toString('base64');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof btoa === 'function') {
|
|
126
|
+
return btoa(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error('No base64 encoder available in this runtime.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeAuthMode(value, fallback = 'auto') {
|
|
133
|
+
if (value && VALID_AUTH_MODES.has(value)) {
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
return fallback;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parsePlatformUrl(value) {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = new URL(value);
|
|
142
|
+
const segments = parsed.pathname
|
|
143
|
+
.split('/')
|
|
144
|
+
.map((part) => part.trim())
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
|
|
147
|
+
const instance = segments.length === 1 ? decodeURIComponent(segments[0]) : null;
|
|
148
|
+
return {
|
|
149
|
+
origin: parsed.origin,
|
|
150
|
+
instance
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function normalizePlatform(platform) {
|
|
158
|
+
if (!platform) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (typeof platform === 'string') {
|
|
163
|
+
if (PLATFORM_PRESETS[platform]) {
|
|
164
|
+
return {
|
|
165
|
+
origin: PLATFORM_PRESETS[platform],
|
|
166
|
+
instance: null
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const parsed = parsePlatformUrl(platform);
|
|
171
|
+
if (parsed) {
|
|
172
|
+
return parsed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
origin: platform,
|
|
177
|
+
instance: null
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!isObject(platform)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const preset = typeof platform.preset === 'string' ? platform.preset : null;
|
|
186
|
+
const directOrigin = typeof platform.origin === 'string' ? platform.origin : null;
|
|
187
|
+
const directUrl = typeof platform.url === 'string' ? platform.url : null;
|
|
188
|
+
const parsedUrl = directUrl ? parsePlatformUrl(directUrl) : null;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
origin: directOrigin ?? parsedUrl?.origin ?? (preset && PLATFORM_PRESETS[preset] ? PLATFORM_PRESETS[preset] : null),
|
|
192
|
+
instance: typeof platform.instance === 'string' ? platform.instance : parsedUrl?.instance ?? null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function mergeHeaders(...sources) {
|
|
197
|
+
const merged = new Headers();
|
|
198
|
+
for (const source of sources) {
|
|
199
|
+
if (!source) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const headers = source instanceof Headers ? source : new Headers(source);
|
|
203
|
+
for (const [key, value] of headers.entries()) {
|
|
204
|
+
merged.set(key, value);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return merged;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isSuccessfulHttpStatus(status) {
|
|
211
|
+
return Number.isInteger(status) && status >= 200 && status < 400;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function securitySchemesFromOperation(operation) {
|
|
215
|
+
const security = Array.isArray(operation.security) ? operation.security : [];
|
|
216
|
+
if (security.length === 0) {
|
|
217
|
+
return ['none'];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const schemes = [];
|
|
221
|
+
for (const requirement of security) {
|
|
222
|
+
const keys = Object.keys(requirement || {});
|
|
223
|
+
if (keys.length === 0) {
|
|
224
|
+
if (!schemes.includes('none')) {
|
|
225
|
+
schemes.push('none');
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const key of keys) {
|
|
231
|
+
const mapped = AUTH_SCHEME_MAP[key];
|
|
232
|
+
if (mapped && !schemes.includes(mapped)) {
|
|
233
|
+
schemes.push(mapped);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return schemes.length > 0 ? schemes : ['none'];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function shouldInferBody(operation, input) {
|
|
242
|
+
if (!Array.isArray(operation.requestContentTypes) || operation.requestContentTypes.length === 0) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const key of RESERVED_INPUT_KEYS) {
|
|
247
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// Object-container control keys only disable inference when actually
|
|
251
|
+
// object-valued; a scalar (e.g. query: 'acme') is a payload field.
|
|
252
|
+
if (OBJECT_CONTROL_KEYS.has(key) && !isObject(input[key])) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function prepareOperationInput(operation, inputValue) {
|
|
262
|
+
const input = isObject(inputValue) ? inputValue : {};
|
|
263
|
+
const pathParams = isObject(input.path) ? { ...input.path } : {};
|
|
264
|
+
const query = isObject(input.query) ? { ...input.query } : {};
|
|
265
|
+
const headers = isObject(input.headers) ? { ...input.headers } : {};
|
|
266
|
+
const consumedInputKeys = new Set(RESERVED_INPUT_KEYS);
|
|
267
|
+
// Scalar object-container keys (e.g. query: 'acme') are payload fields, so do
|
|
268
|
+
// not pre-exclude them from the inferred body. A declared path/query/header
|
|
269
|
+
// parameter of the same name is still routed correctly by the loops below.
|
|
270
|
+
for (const key of OBJECT_CONTROL_KEYS) {
|
|
271
|
+
if (Object.prototype.hasOwnProperty.call(input, key) && !isObject(input[key])) {
|
|
272
|
+
consumedInputKeys.delete(key);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const name of operation.parameterNames.path) {
|
|
277
|
+
if (!Object.prototype.hasOwnProperty.call(pathParams, name) && Object.prototype.hasOwnProperty.call(input, name)) {
|
|
278
|
+
pathParams[name] = input[name];
|
|
279
|
+
}
|
|
280
|
+
if (Object.prototype.hasOwnProperty.call(input, name)) {
|
|
281
|
+
consumedInputKeys.add(name);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const name of operation.parameterNames.query) {
|
|
286
|
+
if (!Object.prototype.hasOwnProperty.call(query, name) && Object.prototype.hasOwnProperty.call(input, name)) {
|
|
287
|
+
query[name] = input[name];
|
|
288
|
+
}
|
|
289
|
+
if (Object.prototype.hasOwnProperty.call(input, name)) {
|
|
290
|
+
consumedInputKeys.add(name);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const name of operation.parameterNames.header) {
|
|
295
|
+
if (!Object.prototype.hasOwnProperty.call(headers, name) && Object.prototype.hasOwnProperty.call(input, name)) {
|
|
296
|
+
headers[name] = input[name];
|
|
297
|
+
}
|
|
298
|
+
if (Object.prototype.hasOwnProperty.call(input, name)) {
|
|
299
|
+
consumedInputKeys.add(name);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let body;
|
|
304
|
+
if (Object.prototype.hasOwnProperty.call(input, 'body')) {
|
|
305
|
+
body = input.body;
|
|
306
|
+
} else if (Object.prototype.hasOwnProperty.call(input, 'data')) {
|
|
307
|
+
body = input.data;
|
|
308
|
+
} else if (shouldInferBody(operation, input)) {
|
|
309
|
+
body = {};
|
|
310
|
+
for (const [key, value] of Object.entries(input)) {
|
|
311
|
+
if (!consumedInputKeys.has(key)) {
|
|
312
|
+
body[key] = value;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (Object.keys(body).length === 0) {
|
|
316
|
+
body = undefined;
|
|
317
|
+
}
|
|
318
|
+
} else if (Array.isArray(operation.requestContentTypes) && operation.requestContentTypes.length > 0) {
|
|
319
|
+
// Body inference was skipped because a reserved control key is present in the
|
|
320
|
+
// input (shouldInferBody returned false). Any remaining input fields that are
|
|
321
|
+
// not reserved control keys and not path/query/header parameters would have
|
|
322
|
+
// become the request body, but are now silently dropped. Surface that clearly
|
|
323
|
+
// instead of sending a request that omits the caller's payload.
|
|
324
|
+
const collidingReservedKeys = [];
|
|
325
|
+
for (const key of RESERVED_INPUT_KEYS) {
|
|
326
|
+
if (Object.prototype.hasOwnProperty.call(input, key)) {
|
|
327
|
+
collidingReservedKeys.push(key);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const orphanedFields = Object.keys(input).filter((key) => !consumedInputKeys.has(key));
|
|
332
|
+
|
|
333
|
+
if (orphanedFields.length > 0) {
|
|
334
|
+
const operationLabel = operation.operationId || `${operation.method} ${operation.path}`;
|
|
335
|
+
throw new ZeyosApiError(
|
|
336
|
+
`${operationLabel}: payload field(s) ${orphanedFields.map((field) => `"${field}"`).join(', ')} ` +
|
|
337
|
+
`would be dropped because the reserved key(s) ${collidingReservedKeys
|
|
338
|
+
.map((key) => `"${key}"`)
|
|
339
|
+
.join(', ')} disabled body inference. ` +
|
|
340
|
+
'Wrap payload fields in an explicit `body: { ... }` (or `data: { ... }`).',
|
|
341
|
+
{
|
|
342
|
+
operationId: operation.operationId,
|
|
343
|
+
method: operation.method,
|
|
344
|
+
url: operation.path
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
pathParams,
|
|
352
|
+
query,
|
|
353
|
+
headers,
|
|
354
|
+
body,
|
|
355
|
+
bodyType: input.bodyType,
|
|
356
|
+
auth: input.auth,
|
|
357
|
+
signal: input.signal,
|
|
358
|
+
raw: input.raw,
|
|
359
|
+
baseUrl: input.baseUrl
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function chooseBodyType(serviceKey, operation, prepared, fallbackBodyType) {
|
|
364
|
+
const body = prepared.body;
|
|
365
|
+
if (body == null) {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const explicitType = prepared.bodyType ?? fallbackBodyType;
|
|
370
|
+
if (explicitType) {
|
|
371
|
+
return explicitType;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const contentTypes = operation.requestContentTypes || [];
|
|
375
|
+
|
|
376
|
+
if ((serviceKey === 'oauth2' || serviceKey === 'legacyAuth') && contentTypes.includes('application/x-www-form-urlencoded')) {
|
|
377
|
+
return 'form';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (contentTypes.includes('application/json')) {
|
|
381
|
+
return 'json';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (contentTypes.includes('application/x-www-form-urlencoded')) {
|
|
385
|
+
return 'form';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function createApiError(response, { serviceKey, operation, method, url }) {
|
|
392
|
+
const operationDescription = operation.operationId ? `${serviceKey}.${operation.operationId}` : `${serviceKey} request`;
|
|
393
|
+
const message = `${operationDescription} failed with HTTP ${response.status}`;
|
|
394
|
+
|
|
395
|
+
return new ZeyosApiError(message, {
|
|
396
|
+
status: response.status,
|
|
397
|
+
statusText: response.statusText,
|
|
398
|
+
headers: response.headers,
|
|
399
|
+
body: response.data,
|
|
400
|
+
method,
|
|
401
|
+
url,
|
|
402
|
+
operationId: operation.operationId,
|
|
403
|
+
service: serviceKey
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function normalizeRequestAuth(auth) {
|
|
408
|
+
if (!auth) {
|
|
409
|
+
return {};
|
|
410
|
+
}
|
|
411
|
+
if (typeof auth === 'string') {
|
|
412
|
+
return { mode: auth };
|
|
413
|
+
}
|
|
414
|
+
return auth;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getBasicCredentials({ body, requestAuth, oauthConfig }) {
|
|
418
|
+
const bodyObject = isObject(body) ? body : {};
|
|
419
|
+
|
|
420
|
+
const clientId =
|
|
421
|
+
requestAuth.clientId ??
|
|
422
|
+
requestAuth.client_id ??
|
|
423
|
+
bodyObject.client_id ??
|
|
424
|
+
bodyObject.clientId ??
|
|
425
|
+
oauthConfig.clientId ??
|
|
426
|
+
oauthConfig.client_id;
|
|
427
|
+
|
|
428
|
+
const clientSecret =
|
|
429
|
+
requestAuth.clientSecret ??
|
|
430
|
+
requestAuth.client_secret ??
|
|
431
|
+
bodyObject.client_secret ??
|
|
432
|
+
bodyObject.clientSecret ??
|
|
433
|
+
oauthConfig.clientSecret ??
|
|
434
|
+
oauthConfig.client_secret;
|
|
435
|
+
|
|
436
|
+
if (!clientId || !clientSecret) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
clientId,
|
|
442
|
+
clientSecret
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function resolveBaseUrl({ services, serviceKey, config, explicitBaseUrl }) {
|
|
447
|
+
if (explicitBaseUrl) {
|
|
448
|
+
return trimTrailingSlash(explicitBaseUrl);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (isObject(config.baseUrls) && typeof config.baseUrls[serviceKey] === 'string') {
|
|
452
|
+
return trimTrailingSlash(config.baseUrls[serviceKey]);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const service = services[serviceKey];
|
|
456
|
+
if (!service) {
|
|
457
|
+
throw new Error(`Unknown service key: ${serviceKey}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const template = service.server?.urlTemplate || '';
|
|
461
|
+
const defaults = isObject(service.server?.defaultVariables) ? service.server.defaultVariables : {};
|
|
462
|
+
const platform = normalizePlatform(config.platform);
|
|
463
|
+
const platformInstance = platform?.instance ?? config.instance ?? defaults.INSTANCE;
|
|
464
|
+
|
|
465
|
+
if (platform?.origin) {
|
|
466
|
+
const pathTemplate = service.server?.basePathTemplate || '';
|
|
467
|
+
const pathVariables = {
|
|
468
|
+
...defaults,
|
|
469
|
+
INSTANCE: platformInstance
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const resolvedPath = pathTemplate.replace(/\{([^}]+)\}/g, (_, token) => {
|
|
473
|
+
if (!Object.prototype.hasOwnProperty.call(pathVariables, token)) {
|
|
474
|
+
return `{${token}}`;
|
|
475
|
+
}
|
|
476
|
+
return encodeURIComponent(String(pathVariables[token]));
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const normalizedOrigin = trimTrailingSlash(platform.origin);
|
|
480
|
+
const normalizedPath = resolvedPath.startsWith('/') ? resolvedPath : `/${resolvedPath}`;
|
|
481
|
+
return trimTrailingSlash(`${normalizedOrigin}${normalizedPath}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const variables = {
|
|
485
|
+
...defaults,
|
|
486
|
+
INSTANCE: platformInstance
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const resolved = template.replace(/\{([^}]+)\}/g, (_, token) => {
|
|
490
|
+
if (!Object.prototype.hasOwnProperty.call(variables, token)) {
|
|
491
|
+
return `{${token}}`;
|
|
492
|
+
}
|
|
493
|
+
return encodeURIComponent(String(variables[token]));
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
return trimTrailingSlash(resolved);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function resolveAuthCandidates({ mode, schemes, tokenSet, sessionEnabled }) {
|
|
500
|
+
const has = (scheme) => schemes.includes(scheme);
|
|
501
|
+
|
|
502
|
+
if (mode === 'none') {
|
|
503
|
+
return [{ type: 'none' }];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (mode === 'oauth') {
|
|
507
|
+
if (has('basic')) {
|
|
508
|
+
return [{ type: 'basic' }];
|
|
509
|
+
}
|
|
510
|
+
if (has('bearer')) {
|
|
511
|
+
return [{ type: 'bearer' }];
|
|
512
|
+
}
|
|
513
|
+
if (has('none')) {
|
|
514
|
+
return [{ type: 'none' }];
|
|
515
|
+
}
|
|
516
|
+
throw new Error('OAuth mode cannot satisfy the operation security requirements.');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (mode === 'session') {
|
|
520
|
+
if (has('session')) {
|
|
521
|
+
return [{ type: 'session' }];
|
|
522
|
+
}
|
|
523
|
+
if (has('none')) {
|
|
524
|
+
return [{ type: 'none' }];
|
|
525
|
+
}
|
|
526
|
+
throw new Error('Session mode cannot satisfy the operation security requirements.');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const candidates = [];
|
|
530
|
+
if (has('basic')) {
|
|
531
|
+
candidates.push({ type: 'basic' });
|
|
532
|
+
}
|
|
533
|
+
if (has('bearer') && tokenSet?.accessToken) {
|
|
534
|
+
candidates.push({ type: 'bearer' });
|
|
535
|
+
}
|
|
536
|
+
if (has('session') && sessionEnabled) {
|
|
537
|
+
candidates.push({ type: 'session' });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (candidates.length === 0) {
|
|
541
|
+
if (has('bearer')) {
|
|
542
|
+
candidates.push({ type: 'bearer' });
|
|
543
|
+
} else if (has('session') && sessionEnabled) {
|
|
544
|
+
candidates.push({ type: 'session' });
|
|
545
|
+
} else {
|
|
546
|
+
candidates.push({ type: 'none' });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return candidates;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function canRefreshAccessToken({ mode, operation, tokenSet, oauthConfig }) {
|
|
554
|
+
if (mode !== 'auto' && mode !== 'oauth') {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (oauthConfig.autoRefresh === false) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!tokenSet?.refreshToken) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (operation.operationId === 'getToken') {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return Boolean(oauthConfig.clientId && oauthConfig.clientSecret);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function isAccessTokenExpired(tokenSet, skewSeconds = 60) {
|
|
574
|
+
if (!tokenSet?.accessToken || tokenSet.expiresAt == null) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
const expiresAt = Number(tokenSet.expiresAt);
|
|
578
|
+
if (!Number.isFinite(expiresAt)) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
const now = Math.floor(Date.now() / 1000);
|
|
582
|
+
return expiresAt <= now + skewSeconds;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function createZeyosClient(rawConfig = {}) {
|
|
586
|
+
const config = isObject(rawConfig) ? rawConfig : {};
|
|
587
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
588
|
+
|
|
589
|
+
if (typeof fetchImpl !== 'function') {
|
|
590
|
+
throw new Error('Fetch implementation is required (pass config.fetch or run in an environment with global fetch).');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const authConfig = isObject(config.auth) ? config.auth : {};
|
|
594
|
+
const oauthConfig = isObject(authConfig.oauth) ? authConfig.oauth : {};
|
|
595
|
+
const sessionConfig = isObject(authConfig.session) ? authConfig.session : {};
|
|
596
|
+
|
|
597
|
+
const defaultMode = normalizeAuthMode(authConfig.mode, 'auto');
|
|
598
|
+
const sessionEnabled = sessionConfig.enabled !== false;
|
|
599
|
+
const sessionCredentials = sessionConfig.credentials ?? 'include';
|
|
600
|
+
|
|
601
|
+
const providedTokenStore = oauthConfig.tokenStore;
|
|
602
|
+
const tokenStore =
|
|
603
|
+
providedTokenStore && typeof providedTokenStore.get === 'function' && typeof providedTokenStore.set === 'function'
|
|
604
|
+
? providedTokenStore
|
|
605
|
+
: new MemoryTokenStore(oauthConfig.token ?? null);
|
|
606
|
+
|
|
607
|
+
const defaultHeaders = isObject(config.headers) ? config.headers : {};
|
|
608
|
+
const retryConfig = normalizeRetry(config.retry);
|
|
609
|
+
const schemaApi = createSchema({ services: SERVICES, schema: SCHEMA });
|
|
610
|
+
const validateByDefault = config.validate === true;
|
|
611
|
+
const operationLookup = new Map();
|
|
612
|
+
|
|
613
|
+
for (const [serviceKey, service] of Object.entries(SERVICES)) {
|
|
614
|
+
for (const operation of service.operations) {
|
|
615
|
+
operationLookup.set(`${serviceKey}.${operation.operationId}`, operation);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function getTokenSet() {
|
|
620
|
+
return normalizeTokenSet(await tokenStore.get());
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function setTokenSet(tokenSet) {
|
|
624
|
+
await tokenStore.set(normalizeTokenSet(tokenSet));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function clearTokenSet() {
|
|
628
|
+
await tokenStore.set(null);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function getSessionCookieHeader() {
|
|
632
|
+
const cookieSource = sessionConfig.cookie;
|
|
633
|
+
const rawCookie = typeof cookieSource === 'function' ? await cookieSource() : cookieSource;
|
|
634
|
+
|
|
635
|
+
if (!rawCookie) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const cookieValue = String(rawCookie);
|
|
640
|
+
if (cookieValue.includes('=')) {
|
|
641
|
+
return cookieValue;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return `ZEYOSID=${cookieValue}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function sendRequestOnce({ serviceKey, operation, prepared, requestAuth, tokenSet, candidate, requestOptions }) {
|
|
648
|
+
const body = cloneValue(prepared.body);
|
|
649
|
+
const authHeaders = {};
|
|
650
|
+
let credentials;
|
|
651
|
+
|
|
652
|
+
if (candidate.type === 'bearer') {
|
|
653
|
+
const accessToken = requestAuth.accessToken ?? requestAuth.access_token ?? tokenSet?.accessToken;
|
|
654
|
+
if (!accessToken) {
|
|
655
|
+
throw new Error('Missing access token for bearer-authenticated request.');
|
|
656
|
+
}
|
|
657
|
+
authHeaders.authorization = `Bearer ${accessToken}`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (candidate.type === 'basic') {
|
|
661
|
+
const credentialsPair = getBasicCredentials({ body, requestAuth, oauthConfig });
|
|
662
|
+
if (!credentialsPair) {
|
|
663
|
+
throw new Error('Missing client_id/client_secret for basic-authenticated request.');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
authHeaders.authorization = `Basic ${toBase64(`${credentialsPair.clientId}:${credentialsPair.clientSecret}`)}`;
|
|
667
|
+
|
|
668
|
+
if (isObject(body)) {
|
|
669
|
+
if (!Object.prototype.hasOwnProperty.call(body, 'client_id')) {
|
|
670
|
+
body.client_id = credentialsPair.clientId;
|
|
671
|
+
}
|
|
672
|
+
if (!Object.prototype.hasOwnProperty.call(body, 'client_secret')) {
|
|
673
|
+
body.client_secret = credentialsPair.clientSecret;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (candidate.type === 'session') {
|
|
679
|
+
credentials = sessionCredentials;
|
|
680
|
+
const cookieHeader = await getSessionCookieHeader();
|
|
681
|
+
if (cookieHeader) {
|
|
682
|
+
authHeaders.cookie = cookieHeader;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const bodyType = chooseBodyType(serviceKey, operation, { ...prepared, body }, requestOptions?.bodyType);
|
|
687
|
+
const headers = mergeHeaders(defaultHeaders, prepared.headers, authHeaders);
|
|
688
|
+
|
|
689
|
+
if (!headers.has('accept')) {
|
|
690
|
+
headers.set('accept', 'application/json, text/plain;q=0.9, */*;q=0.8');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const url = buildUrl(
|
|
694
|
+
resolveBaseUrl({ services: SERVICES, serviceKey, config, explicitBaseUrl: prepared.baseUrl ?? requestOptions?.baseUrl }),
|
|
695
|
+
operation.path,
|
|
696
|
+
prepared.pathParams,
|
|
697
|
+
prepared.query
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
const signal = prepared.signal ?? requestOptions?.signal;
|
|
701
|
+
|
|
702
|
+
let response;
|
|
703
|
+
for (let attempt = 0; ; attempt++) {
|
|
704
|
+
response = await httpRequest({
|
|
705
|
+
fetchImpl,
|
|
706
|
+
url,
|
|
707
|
+
method: operation.method,
|
|
708
|
+
headers,
|
|
709
|
+
body,
|
|
710
|
+
bodyType,
|
|
711
|
+
signal,
|
|
712
|
+
credentials
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
if (attempt >= retryConfig.maxRetries || !retryConfig.retryOn.has(response.status)) {
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
await abortableDelay(computeRetryDelay(response, attempt, retryConfig), signal);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (!isSuccessfulHttpStatus(response.status)) {
|
|
722
|
+
throw createApiError(response, {
|
|
723
|
+
serviceKey,
|
|
724
|
+
operation,
|
|
725
|
+
method: operation.method,
|
|
726
|
+
url
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
...response,
|
|
732
|
+
data: operation.method === 'HEAD' ? true : response.data
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function refreshAccessToken(currentTokenSet, requestAuth = {}, requestOptions = {}) {
|
|
737
|
+
const refreshToken = requestAuth.refreshToken ?? requestAuth.refresh_token ?? currentTokenSet?.refreshToken;
|
|
738
|
+
if (!refreshToken) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const credentials = getBasicCredentials({ body: {}, requestAuth, oauthConfig });
|
|
743
|
+
if (!credentials) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const tokenOperation = operationLookup.get('oauth2.getToken');
|
|
748
|
+
if (!tokenOperation) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const prepared = {
|
|
753
|
+
pathParams: {},
|
|
754
|
+
query: {},
|
|
755
|
+
headers: {},
|
|
756
|
+
body: {
|
|
757
|
+
grant_type: 'refresh_token',
|
|
758
|
+
refresh_token: refreshToken,
|
|
759
|
+
client_id: credentials.clientId,
|
|
760
|
+
client_secret: credentials.clientSecret
|
|
761
|
+
},
|
|
762
|
+
bodyType: 'form',
|
|
763
|
+
signal: requestOptions.signal,
|
|
764
|
+
baseUrl: requestOptions.baseUrl
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const response = await sendRequestOnce({
|
|
768
|
+
serviceKey: 'oauth2',
|
|
769
|
+
operation: tokenOperation,
|
|
770
|
+
prepared,
|
|
771
|
+
requestAuth,
|
|
772
|
+
tokenSet: currentTokenSet,
|
|
773
|
+
candidate: { type: 'basic' },
|
|
774
|
+
requestOptions: { ...requestOptions, bodyType: 'form' }
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const nextTokenSet = tokenResponseToTokenSet(response.data);
|
|
778
|
+
if (!nextTokenSet) {
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (!nextTokenSet.refreshToken && currentTokenSet?.refreshToken) {
|
|
783
|
+
nextTokenSet.refreshToken = currentTokenSet.refreshToken;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
await tokenStore.set(nextTokenSet);
|
|
787
|
+
return nextTokenSet;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function executeOperation({ serviceKey, operation, prepared, requestOptions = {} }) {
|
|
791
|
+
const requestAuth = normalizeRequestAuth(prepared.auth ?? requestOptions.auth);
|
|
792
|
+
const mode = normalizeAuthMode(requestAuth.mode, defaultMode);
|
|
793
|
+
const schemes = securitySchemesFromOperation(operation);
|
|
794
|
+
let tokenSet = await getTokenSet();
|
|
795
|
+
|
|
796
|
+
if (
|
|
797
|
+
schemes.includes('bearer') &&
|
|
798
|
+
isAccessTokenExpired(tokenSet) &&
|
|
799
|
+
canRefreshAccessToken({ mode, operation, tokenSet, oauthConfig })
|
|
800
|
+
) {
|
|
801
|
+
try {
|
|
802
|
+
const refreshed = await refreshAccessToken(tokenSet, requestAuth, requestOptions);
|
|
803
|
+
if (refreshed?.accessToken) {
|
|
804
|
+
tokenSet = refreshed;
|
|
805
|
+
}
|
|
806
|
+
} catch {
|
|
807
|
+
// Fall back to the normal request path; a 401 can still trigger refresh.
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const candidates = resolveAuthCandidates({
|
|
812
|
+
mode,
|
|
813
|
+
schemes,
|
|
814
|
+
tokenSet,
|
|
815
|
+
sessionEnabled
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const raw = requestOptions.raw ?? prepared.raw ?? false;
|
|
819
|
+
let lastError;
|
|
820
|
+
|
|
821
|
+
for (const candidate of candidates) {
|
|
822
|
+
try {
|
|
823
|
+
const response = await sendRequestOnce({
|
|
824
|
+
serviceKey,
|
|
825
|
+
operation,
|
|
826
|
+
prepared,
|
|
827
|
+
requestAuth,
|
|
828
|
+
tokenSet,
|
|
829
|
+
candidate,
|
|
830
|
+
requestOptions
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
return raw ? response : response.data;
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (!(error instanceof ZeyosApiError) || error.status !== 401) {
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (candidate.type === 'bearer' && canRefreshAccessToken({ mode, operation, tokenSet, oauthConfig })) {
|
|
840
|
+
try {
|
|
841
|
+
const refreshed = await refreshAccessToken(tokenSet, requestAuth, requestOptions);
|
|
842
|
+
if (refreshed?.accessToken) {
|
|
843
|
+
tokenSet = refreshed;
|
|
844
|
+
const retryResponse = await sendRequestOnce({
|
|
845
|
+
serviceKey,
|
|
846
|
+
operation,
|
|
847
|
+
prepared,
|
|
848
|
+
requestAuth,
|
|
849
|
+
tokenSet,
|
|
850
|
+
candidate,
|
|
851
|
+
requestOptions
|
|
852
|
+
});
|
|
853
|
+
return raw ? retryResponse : retryResponse.data;
|
|
854
|
+
}
|
|
855
|
+
} catch (refreshError) {
|
|
856
|
+
lastError = refreshError;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
lastError = error;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (lastError) {
|
|
866
|
+
throw lastError;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
throw new Error('Unable to execute request due to missing authentication candidates.');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function bindService(serviceKey) {
|
|
873
|
+
const service = SERVICES[serviceKey];
|
|
874
|
+
if (!service) {
|
|
875
|
+
return Object.freeze({});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const namespace = {};
|
|
879
|
+
const operationIds = service.operations.map((operation) => operation.operationId);
|
|
880
|
+
|
|
881
|
+
for (const operation of service.operations) {
|
|
882
|
+
namespace[operation.operationId] = async (input, requestOptions) => {
|
|
883
|
+
if (validateByDefault || requestOptions?.validate === true) {
|
|
884
|
+
const result = schemaApi.validate(operation.operationId, input);
|
|
885
|
+
if (!result.valid) {
|
|
886
|
+
throw new ZeyosValidationError(
|
|
887
|
+
`${operation.operationId}: ${result.errors.map((entry) => entry.message).join(' ')}`,
|
|
888
|
+
{ operationId: operation.operationId, errors: result.errors }
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const prepared = prepareOperationInput(operation, input);
|
|
893
|
+
return executeOperation({ serviceKey, operation, prepared, requestOptions });
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Return a helpful "did you mean ...?" error when an agent calls an
|
|
898
|
+
// operation that does not exist (e.g. listDunning vs listDunningNotices),
|
|
899
|
+
// instead of an opaque "x is not a function" TypeError.
|
|
900
|
+
return new Proxy(Object.freeze(namespace), {
|
|
901
|
+
get(target, prop, receiver) {
|
|
902
|
+
if (typeof prop !== 'string' || prop === 'then' || prop in target) {
|
|
903
|
+
return Reflect.get(target, prop, receiver);
|
|
904
|
+
}
|
|
905
|
+
// Async so an unknown operation rejects like a real operation call
|
|
906
|
+
// would, rather than throwing synchronously before `.catch()`/`await`.
|
|
907
|
+
return async () => {
|
|
908
|
+
const suggestion = suggestClosest(prop, operationIds);
|
|
909
|
+
throw new ZeyosApiError(
|
|
910
|
+
`Unknown operation '${serviceKey}.${prop}'.` +
|
|
911
|
+
(suggestion
|
|
912
|
+
? ` Did you mean '${suggestion}'?`
|
|
913
|
+
: ' Use client.schema.operationIds() to list valid operations.'),
|
|
914
|
+
{ operationId: prop, service: serviceKey }
|
|
915
|
+
);
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async function request(input = {}, requestOptions = {}) {
|
|
922
|
+
if (!isObject(input)) {
|
|
923
|
+
throw new Error('client.request input must be an object.');
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const serviceKey = input.service;
|
|
927
|
+
if (!serviceKey || typeof serviceKey !== 'string') {
|
|
928
|
+
throw new Error('client.request requires a service key.');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (input.operationId) {
|
|
932
|
+
const operation = operationLookup.get(`${serviceKey}.${input.operationId}`);
|
|
933
|
+
if (!operation) {
|
|
934
|
+
const candidates = (SERVICES[serviceKey]?.operations ?? []).map((entry) => entry.operationId);
|
|
935
|
+
const suggestion = suggestClosest(input.operationId, candidates);
|
|
936
|
+
throw new ZeyosApiError(
|
|
937
|
+
`Unknown operation: ${serviceKey}.${input.operationId}.` + (suggestion ? ` Did you mean '${suggestion}'?` : ''),
|
|
938
|
+
{ operationId: input.operationId, service: serviceKey }
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const prepared = {
|
|
943
|
+
pathParams: isObject(input.pathParams) ? input.pathParams : {},
|
|
944
|
+
query: isObject(input.query) ? input.query : {},
|
|
945
|
+
headers: isObject(input.headers) ? input.headers : {},
|
|
946
|
+
body: input.body,
|
|
947
|
+
bodyType: input.bodyType,
|
|
948
|
+
auth: input.auth,
|
|
949
|
+
signal: input.signal,
|
|
950
|
+
raw: input.raw,
|
|
951
|
+
baseUrl: input.baseUrl
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
return executeOperation({ serviceKey, operation, prepared, requestOptions });
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (!input.path || !input.method) {
|
|
958
|
+
throw new Error('client.request requires method and path when operationId is not provided.');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const operation = {
|
|
962
|
+
operationId: 'request',
|
|
963
|
+
method: String(input.method).toUpperCase(),
|
|
964
|
+
path: String(input.path),
|
|
965
|
+
security: Array.isArray(input.security) ? input.security : [],
|
|
966
|
+
requestContentTypes: Array.isArray(input.requestContentTypes) ? input.requestContentTypes : ['application/json'],
|
|
967
|
+
parameterNames: {
|
|
968
|
+
path: [],
|
|
969
|
+
query: [],
|
|
970
|
+
header: []
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
const prepared = {
|
|
975
|
+
pathParams: isObject(input.pathParams) ? input.pathParams : {},
|
|
976
|
+
query: isObject(input.query) ? input.query : {},
|
|
977
|
+
headers: isObject(input.headers) ? input.headers : {},
|
|
978
|
+
body: input.body,
|
|
979
|
+
bodyType: input.bodyType,
|
|
980
|
+
auth: input.auth,
|
|
981
|
+
signal: input.signal,
|
|
982
|
+
raw: input.raw,
|
|
983
|
+
baseUrl: input.baseUrl
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
return executeOperation({ serviceKey, operation, prepared, requestOptions });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const api = bindService('api');
|
|
990
|
+
const oauth2Operations = bindService('oauth2');
|
|
991
|
+
const legacyAuth = bindService('legacyAuth');
|
|
992
|
+
|
|
993
|
+
function buildAuthorizationUrl(options = {}) {
|
|
994
|
+
const clientId = options.clientId ?? options.client_id ?? oauthConfig.clientId;
|
|
995
|
+
const redirectUri = options.redirectUri ?? options.redirect_uri;
|
|
996
|
+
|
|
997
|
+
if (!clientId) {
|
|
998
|
+
throw new Error('buildAuthorizationUrl requires clientId (or auth.oauth.clientId in client config).');
|
|
999
|
+
}
|
|
1000
|
+
if (!redirectUri) {
|
|
1001
|
+
throw new Error('buildAuthorizationUrl requires redirectUri.');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const query = {
|
|
1005
|
+
client_id: clientId,
|
|
1006
|
+
redirect_uri: redirectUri,
|
|
1007
|
+
response_type: 'code',
|
|
1008
|
+
scope: options.scope ?? options.options?.scope,
|
|
1009
|
+
response_mode: options.responseMode ?? options.response_mode,
|
|
1010
|
+
code_challenge: options.codeChallenge ?? options.code_challenge,
|
|
1011
|
+
code_challenge_method: options.codeChallengeMethod ?? options.code_challenge_method,
|
|
1012
|
+
state: options.state
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
if (query.code_challenge && !query.code_challenge_method) {
|
|
1016
|
+
query.code_challenge_method = 'S256';
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return buildUrl(
|
|
1020
|
+
resolveBaseUrl({ services: SERVICES, serviceKey: 'oauth2', config, explicitBaseUrl: options.baseUrl }),
|
|
1021
|
+
'/authorize',
|
|
1022
|
+
{},
|
|
1023
|
+
query
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function parseAuthorizationCallback(callbackUrl) {
|
|
1028
|
+
const url =
|
|
1029
|
+
callbackUrl instanceof URL
|
|
1030
|
+
? callbackUrl
|
|
1031
|
+
: (() => {
|
|
1032
|
+
try {
|
|
1033
|
+
return new URL(String(callbackUrl));
|
|
1034
|
+
} catch {
|
|
1035
|
+
return new URL(String(callbackUrl), 'http://localhost');
|
|
1036
|
+
}
|
|
1037
|
+
})();
|
|
1038
|
+
|
|
1039
|
+
const params = url.searchParams;
|
|
1040
|
+
|
|
1041
|
+
return {
|
|
1042
|
+
code: params.get('code'),
|
|
1043
|
+
state: params.get('state'),
|
|
1044
|
+
error: params.get('error'),
|
|
1045
|
+
errorDescription: params.get('error_description'),
|
|
1046
|
+
errorUri: params.get('error_uri'),
|
|
1047
|
+
isError: params.has('error')
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async function storeTokenResponse(tokenResponse, store = true) {
|
|
1052
|
+
const tokenSet = tokenResponseToTokenSet(tokenResponse);
|
|
1053
|
+
if (store && tokenSet) {
|
|
1054
|
+
await tokenStore.set(tokenSet);
|
|
1055
|
+
}
|
|
1056
|
+
return tokenSet || tokenResponse;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
async function exchangeAuthorizationCode(options = {}, requestOptions = {}) {
|
|
1060
|
+
const clientId = options.clientId ?? options.client_id ?? oauthConfig.clientId;
|
|
1061
|
+
const clientSecret = options.clientSecret ?? options.client_secret ?? oauthConfig.clientSecret;
|
|
1062
|
+
const code = options.code;
|
|
1063
|
+
|
|
1064
|
+
if (!code) {
|
|
1065
|
+
throw new Error('exchangeAuthorizationCode requires code.');
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const tokenResponse = await request(
|
|
1069
|
+
{
|
|
1070
|
+
service: 'oauth2',
|
|
1071
|
+
operationId: 'getToken',
|
|
1072
|
+
body: {
|
|
1073
|
+
grant_type: 'authorization_code',
|
|
1074
|
+
code,
|
|
1075
|
+
code_verifier: options.codeVerifier ?? options.code_verifier,
|
|
1076
|
+
redirect_uri: options.redirectUri ?? options.redirect_uri,
|
|
1077
|
+
client_id: clientId,
|
|
1078
|
+
client_secret: clientSecret
|
|
1079
|
+
},
|
|
1080
|
+
auth: {
|
|
1081
|
+
mode: 'oauth',
|
|
1082
|
+
clientId,
|
|
1083
|
+
clientSecret
|
|
1084
|
+
},
|
|
1085
|
+
bodyType: 'form',
|
|
1086
|
+
raw: false,
|
|
1087
|
+
baseUrl: options.baseUrl
|
|
1088
|
+
},
|
|
1089
|
+
requestOptions
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
return storeTokenResponse(tokenResponse, options.store !== false);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async function refreshToken(options = {}, requestOptions = {}) {
|
|
1096
|
+
const clientId = options.clientId ?? options.client_id ?? oauthConfig.clientId;
|
|
1097
|
+
const clientSecret = options.clientSecret ?? options.client_secret ?? oauthConfig.clientSecret;
|
|
1098
|
+
|
|
1099
|
+
const tokenSet = await getTokenSet();
|
|
1100
|
+
const refreshTokenValue = options.refreshToken ?? options.refresh_token ?? tokenSet?.refreshToken;
|
|
1101
|
+
|
|
1102
|
+
if (!refreshTokenValue) {
|
|
1103
|
+
throw new Error('refreshToken requires refreshToken or a stored token with refreshToken.');
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const tokenResponse = await request(
|
|
1107
|
+
{
|
|
1108
|
+
service: 'oauth2',
|
|
1109
|
+
operationId: 'getToken',
|
|
1110
|
+
body: {
|
|
1111
|
+
grant_type: 'refresh_token',
|
|
1112
|
+
refresh_token: refreshTokenValue,
|
|
1113
|
+
client_id: clientId,
|
|
1114
|
+
client_secret: clientSecret
|
|
1115
|
+
},
|
|
1116
|
+
auth: {
|
|
1117
|
+
mode: 'oauth',
|
|
1118
|
+
clientId,
|
|
1119
|
+
clientSecret
|
|
1120
|
+
},
|
|
1121
|
+
bodyType: 'form',
|
|
1122
|
+
baseUrl: options.baseUrl
|
|
1123
|
+
},
|
|
1124
|
+
requestOptions
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
return storeTokenResponse(tokenResponse, options.store !== false);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function revokeToken(options = {}, requestOptions = {}) {
|
|
1131
|
+
const clientId = options.clientId ?? options.client_id ?? oauthConfig.clientId;
|
|
1132
|
+
const clientSecret = options.clientSecret ?? options.client_secret ?? oauthConfig.clientSecret;
|
|
1133
|
+
|
|
1134
|
+
return request(
|
|
1135
|
+
{
|
|
1136
|
+
service: 'oauth2',
|
|
1137
|
+
operationId: 'revokeToken',
|
|
1138
|
+
body: {
|
|
1139
|
+
token: options.token,
|
|
1140
|
+
client_id: clientId,
|
|
1141
|
+
client_secret: clientSecret
|
|
1142
|
+
},
|
|
1143
|
+
auth: {
|
|
1144
|
+
mode: 'oauth',
|
|
1145
|
+
clientId,
|
|
1146
|
+
clientSecret
|
|
1147
|
+
},
|
|
1148
|
+
bodyType: 'form',
|
|
1149
|
+
baseUrl: options.baseUrl
|
|
1150
|
+
},
|
|
1151
|
+
requestOptions
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async function introspectToken(options = {}, requestOptions = {}) {
|
|
1156
|
+
const clientId = options.clientId ?? options.client_id ?? oauthConfig.clientId;
|
|
1157
|
+
const clientSecret = options.clientSecret ?? options.client_secret ?? oauthConfig.clientSecret;
|
|
1158
|
+
|
|
1159
|
+
return request(
|
|
1160
|
+
{
|
|
1161
|
+
service: 'oauth2',
|
|
1162
|
+
operationId: 'introspectToken',
|
|
1163
|
+
body: {
|
|
1164
|
+
token: options.token,
|
|
1165
|
+
client_id: clientId,
|
|
1166
|
+
client_secret: clientSecret
|
|
1167
|
+
},
|
|
1168
|
+
auth: {
|
|
1169
|
+
mode: 'oauth',
|
|
1170
|
+
clientId,
|
|
1171
|
+
clientSecret
|
|
1172
|
+
},
|
|
1173
|
+
bodyType: 'form',
|
|
1174
|
+
baseUrl: options.baseUrl
|
|
1175
|
+
},
|
|
1176
|
+
requestOptions
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const oauth2 = Object.freeze({
|
|
1181
|
+
...oauth2Operations,
|
|
1182
|
+
buildAuthorizationUrl,
|
|
1183
|
+
parseAuthorizationCallback,
|
|
1184
|
+
exchangeAuthorizationCode,
|
|
1185
|
+
refreshToken,
|
|
1186
|
+
revokeToken,
|
|
1187
|
+
introspectToken
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
const client = {
|
|
1191
|
+
api,
|
|
1192
|
+
oauth2,
|
|
1193
|
+
legacyAuth,
|
|
1194
|
+
request,
|
|
1195
|
+
schema: schemaApi,
|
|
1196
|
+
auth: {
|
|
1197
|
+
getTokenSet,
|
|
1198
|
+
setTokenSet,
|
|
1199
|
+
clearTokenSet
|
|
1200
|
+
},
|
|
1201
|
+
metadata: {
|
|
1202
|
+
generatedAt: GENERATED.generatedAt,
|
|
1203
|
+
services: SERVICE_KEYS
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
return Object.freeze(client);
|
|
1208
|
+
}
|