@zeyos/client 0.1.1 → 0.3.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 +35 -0
- package/agents/README.md +18 -6
- package/agents/shared/zeyos-agent-operating-guide.md +112 -0
- package/agents/shared/zeyos-query-patterns.md +12 -1
- package/agents/zeyos/SKILL.md +95 -0
- package/agents/zeyos-account-intelligence/SKILL.md +1 -1
- package/agents/zeyos-account-intelligence/references/workflows.md +7 -0
- package/agents/zeyos-billing-insights/SKILL.md +6 -1
- package/agents/zeyos-billing-insights/references/workflows.md +32 -3
- package/agents/zeyos-campaign-and-outreach/SKILL.md +1 -1
- package/agents/zeyos-campaign-and-outreach/references/workflows.md +8 -0
- package/agents/zeyos-collaboration-and-activity/SKILL.md +1 -1
- package/agents/zeyos-collaboration-and-activity/references/workflows.md +9 -0
- package/agents/zeyos-collections-and-dunning/SKILL.md +1 -1
- package/agents/zeyos-commerce-and-inventory/SKILL.md +1 -1
- package/agents/zeyos-commerce-and-inventory/references/workflows.md +7 -0
- package/agents/zeyos-mail-operations/SKILL.md +8 -2
- package/agents/zeyos-mail-operations/references/workflows.md +19 -2
- package/agents/zeyos-notes-and-sops/SKILL.md +1 -1
- package/agents/zeyos-platform-and-schema/SKILL.md +2 -2
- package/agents/zeyos-platform-and-schema/references/workflows.md +24 -0
- package/agents/zeyos-time-tracking/SKILL.md +48 -0
- package/agents/zeyos-time-tracking/references/workflows.md +230 -0
- package/agents/zeyos-work-management/SKILL.md +6 -3
- package/agents/zeyos-work-management/references/workflows.md +54 -4
- package/docs/02-javascript-client/03-making-requests.md +46 -1
- package/docs/03-cli/01-getting-started.md +7 -0
- package/docs/03-cli/02-commands.md +63 -1
- package/docs/03-cli/03-configuration.md +37 -5
- package/docs/04-agent-workflows/01-agent-quickstart.md +24 -0
- package/docs/04-agent-workflows/03-cli-coverage-and-escalation.md +3 -2
- package/package.json +6 -3
- package/src/runtime/client.js +226 -18
|
@@ -14,6 +14,7 @@ These options work with any command:
|
|
|
14
14
|
|--------|-------------|
|
|
15
15
|
| `--json` | Output as formatted JSON |
|
|
16
16
|
| `--yaml` | Output as YAML |
|
|
17
|
+
| `--profile <name>` | Use a named credential profile for this command |
|
|
17
18
|
| `--no-color` | Disable ANSI color output |
|
|
18
19
|
| `-h`, `--help` | Show help for a command |
|
|
19
20
|
|
|
@@ -101,6 +102,36 @@ zeyos whoami --show-token --json # explicitly include the current access token
|
|
|
101
102
|
|
|
102
103
|
---
|
|
103
104
|
|
|
105
|
+
## profile
|
|
106
|
+
|
|
107
|
+
Manage named credential profiles and switch between ZeyOS instances. See [Configuration → Profiles](./03-configuration.md#profiles) for the full model.
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
zeyos profile <list|current|use|add|remove> [options]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Command | Description |
|
|
114
|
+
|---------|-------------|
|
|
115
|
+
| `profile list` | List all profiles; the active one is marked `*`, with token status |
|
|
116
|
+
| `profile current` | Show which profile resolves right now, and why (flag/env/pin/active) |
|
|
117
|
+
| `profile use <name>` | Make `<name>` the active profile (global) |
|
|
118
|
+
| `profile use <name> --local` | Pin `<name>` to the current project (`.zeyos/profile`) |
|
|
119
|
+
| `profile add <name> [opts]` | Create/update a profile (`--base-url`, `--client-id`, `--secret`, or `--from-current`) |
|
|
120
|
+
| `profile remove <name>` | Delete a profile |
|
|
121
|
+
|
|
122
|
+
**Examples:**
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
zeyos profile add dev --base-url https://zeyos.cms-it.de/dev
|
|
126
|
+
zeyos profile add prod --from-current # snapshot current credentials
|
|
127
|
+
zeyos login --profile prod # authenticate into & activate a profile
|
|
128
|
+
zeyos profile use dev # switch active profile
|
|
129
|
+
zeyos profile use prod --local # pin to this project
|
|
130
|
+
zeyos list tickets --profile dev # one-off override on any command
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
104
135
|
## list
|
|
105
136
|
|
|
106
137
|
Query and list records for a resource with filtering, sorting, and pagination.
|
|
@@ -113,6 +144,7 @@ zeyos list <resource> [options]
|
|
|
113
144
|
|--------|-------------|
|
|
114
145
|
| `--fields <fields>` | Field selection — comma-separated, JSON object, or JSON array (see below) |
|
|
115
146
|
| `--filter <json>` | Filter criteria — JSON object |
|
|
147
|
+
| `--filter-file <path>` | Read filter criteria from a JSON file |
|
|
116
148
|
| `--sort <fields>` | Sort fields, comma-separated (prefix `+` asc, `-` desc) |
|
|
117
149
|
| `--limit <n>` | Maximum records to return (default: `50`) |
|
|
118
150
|
| `--offset <n>` | Skip the first n records |
|
|
@@ -140,6 +172,9 @@ zeyos list tickets
|
|
|
140
172
|
# Custom filters
|
|
141
173
|
zeyos list tickets --filter '{"status":1,"priority":3}'
|
|
142
174
|
|
|
175
|
+
# Custom filters from a file
|
|
176
|
+
zeyos list tickets --filter-file ./filters/open-tickets.json
|
|
177
|
+
|
|
143
178
|
# Specify fields with aliases
|
|
144
179
|
zeyos list accounts --fields '{"Name":"lastname","City":"contact.city"}'
|
|
145
180
|
|
|
@@ -179,6 +214,7 @@ zeyos count <resource> [options]
|
|
|
179
214
|
| Option | Description |
|
|
180
215
|
|--------|-------------|
|
|
181
216
|
| `--filter <json>` | Filter criteria — JSON object |
|
|
217
|
+
| `--filter-file <path>` | Read filter criteria from a JSON file |
|
|
182
218
|
| `--json` | Output as `{"count": N}` |
|
|
183
219
|
| `--yaml` | YAML output |
|
|
184
220
|
|
|
@@ -193,6 +229,9 @@ zeyos count tickets
|
|
|
193
229
|
zeyos count tickets --filter '{"status":1}'
|
|
194
230
|
# → 12
|
|
195
231
|
|
|
232
|
+
# Filtered count using a JSON file
|
|
233
|
+
zeyos count tickets --filter-file ./filters/open-tickets.json
|
|
234
|
+
|
|
196
235
|
# JSON output for scripting
|
|
197
236
|
zeyos count accounts --json
|
|
198
237
|
# → {"count": 156}
|
|
@@ -261,6 +300,7 @@ zeyos create <resource> [--data <json>] [--field value ...]
|
|
|
261
300
|
| Option | Description |
|
|
262
301
|
|--------|-------------|
|
|
263
302
|
| `--data <json>` | Complete record as a JSON string |
|
|
303
|
+
| `--data-file <path>` | Read the complete record from a JSON file |
|
|
264
304
|
| `--<field> <value>` | Set individual field (any unknown flag becomes a field) |
|
|
265
305
|
|
|
266
306
|
**Examples:**
|
|
@@ -269,6 +309,9 @@ zeyos create <resource> [--data <json>] [--field value ...]
|
|
|
269
309
|
# Using --data JSON
|
|
270
310
|
zeyos create ticket --data '{"name":"Fix login bug","status":0,"priority":3}'
|
|
271
311
|
|
|
312
|
+
# Using a JSON file
|
|
313
|
+
zeyos create ticket --data-file ./ticket.json
|
|
314
|
+
|
|
272
315
|
# Using individual field flags
|
|
273
316
|
zeyos create ticket --name "Fix login bug" --status 0 --priority 3
|
|
274
317
|
|
|
@@ -301,6 +344,9 @@ zeyos update <resource> <id> [--data <json>] [--field value ...]
|
|
|
301
344
|
# Using --data JSON
|
|
302
345
|
zeyos update ticket 42 --data '{"status":4}'
|
|
303
346
|
|
|
347
|
+
# Using a JSON file
|
|
348
|
+
zeyos update ticket 42 --data-file ./ticket-update.json
|
|
349
|
+
|
|
304
350
|
# Using field flags
|
|
305
351
|
zeyos update ticket 42 --status 4 --priority 2
|
|
306
352
|
|
|
@@ -348,7 +394,10 @@ List all curated CLI resources and their operations. This is the authoritative b
|
|
|
348
394
|
zeyos resources
|
|
349
395
|
```
|
|
350
396
|
|
|
351
|
-
Shows a table of all CLI-supported resource types and available operations.
|
|
397
|
+
Shows a table of all CLI-supported resource types and available operations. Operational
|
|
398
|
+
workflows can use `actionstep` / `actionsteps` / `time-entries` for follow-ups and
|
|
399
|
+
effort records. Read-only platform schema definitions are available as `customfield` /
|
|
400
|
+
`customfields` with `list`, `get`, and therefore `count` support.
|
|
352
401
|
|
|
353
402
|
---
|
|
354
403
|
|
|
@@ -368,6 +417,19 @@ Foreign keys are shown as `→ <table>`, and enum fields list their valid values
|
|
|
368
417
|
|
|
369
418
|
---
|
|
370
419
|
|
|
420
|
+
## doctor
|
|
421
|
+
|
|
422
|
+
Check local CLI readiness for coding agents. This runs offline and never prints tokens or client secrets.
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
zeyos doctor agent
|
|
426
|
+
zeyos doctor agent --json
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
The report includes the CLI version, configured base URL and instance, whether auth values are present through environment/local/global config, and whether the curated resource registry can be loaded.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
371
433
|
## skills
|
|
372
434
|
|
|
373
435
|
Discover and install the bundled ZeyOS agent skill packs into any coding agent, so the agent (Claude Code, Codex, opencode, Factory Droid, pi, …) operates against ZeyOS with the right conventions out of the box.
|
|
@@ -12,15 +12,47 @@ These settings apply to the CLI's curated resource registry. If you need a resou
|
|
|
12
12
|
|
|
13
13
|
## Credential Cascade
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The CLI first decides **which credential set** is the base, then lets environment credential variables field-override on top. The base is chosen by the first match in this order:
|
|
16
16
|
|
|
17
17
|
| Priority | Source | Location |
|
|
18
18
|
|----------|--------|----------|
|
|
19
|
-
| 1 (highest) |
|
|
20
|
-
| 2 |
|
|
21
|
-
| 3
|
|
19
|
+
| 1 (highest) | `--profile <name>` flag | named profile (per command) |
|
|
20
|
+
| 2 | `ZEYOS_PROFILE` env var | named profile |
|
|
21
|
+
| 3 | Project pin | `.zeyos/profile` (walks up from CWD) |
|
|
22
|
+
| 4 | Local config file | `.zeyos/auth.json` (walks up from CWD) |
|
|
23
|
+
| 5 | Global active profile | `~/.config/zeyos/profiles.json` (`active`) |
|
|
24
|
+
| 6 (lowest) | Global config file | `~/.config/zeyos/credentials.json` |
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
On top of the chosen base, the credential **environment variables** (`ZEYOS_BASE_URL`, `ZEYOS_TOKEN`, …) always override individual fields — so you can keep `baseUrl` and client credentials in a profile while overriding just the token via `ZEYOS_TOKEN` in CI.
|
|
27
|
+
|
|
28
|
+
If you have never created a profile, nothing changes: the CLI falls through to the legacy local `.zeyos/auth.json` and global `credentials.json` exactly as before.
|
|
29
|
+
|
|
30
|
+
## Profiles
|
|
31
|
+
|
|
32
|
+
Profiles let you store several ZeyOS instances (e.g. `dev`, `prod`, `client-x`) and switch between them without re-running `login`. Each profile is a full credential set (URL, OAuth app, tokens) kept in `~/.config/zeyos/profiles.json`, with one marked **active**.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Create profiles (connection params now; tokens via login)
|
|
36
|
+
zeyos profile add dev --base-url https://zeyos.cms-it.de/dev
|
|
37
|
+
zeyos profile add prod --base-url https://cloud.zeyos.com/acme --client-id app --secret "$SECRET"
|
|
38
|
+
|
|
39
|
+
# Authenticate into a profile (also makes it active)
|
|
40
|
+
zeyos login --profile prod
|
|
41
|
+
|
|
42
|
+
# See and switch profiles
|
|
43
|
+
zeyos profile list # active marked with *, shows token status
|
|
44
|
+
zeyos profile use dev # switch the global active profile
|
|
45
|
+
zeyos profile current # what resolves right now, and why
|
|
46
|
+
|
|
47
|
+
# Per-project: pin a profile so cd-ing into a repo selects its instance
|
|
48
|
+
cd ~/work/acme && zeyos profile use prod --local # writes ./.zeyos/profile
|
|
49
|
+
|
|
50
|
+
# One-off override on any command (beats env, pin, and active)
|
|
51
|
+
zeyos whoami --profile dev
|
|
52
|
+
zeyos list tickets --profile prod
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`zeyos profile add <name> --from-current` snapshots whatever credentials are in effect (including tokens) into a new profile — handy for adopting an existing `.zeyos/auth.json` setup. Add `.zeyos/profile` to `.gitignore` alongside `.zeyos/auth.json`.
|
|
24
56
|
|
|
25
57
|
## Environment Variables
|
|
26
58
|
|
|
@@ -107,6 +107,28 @@ Count matching records:
|
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
zeyos count tickets --filter '{"visibility":0,"status":4}' --json
|
|
110
|
+
zeyos count customfields --json
|
|
111
|
+
zeyos count actionsteps --filter '{"status":0}' --json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Summarize actionstep effort/time-entry evidence:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
zeyos list actionsteps \
|
|
118
|
+
--fields ID,name,status,date,duedate,effort,ticket,task,account \
|
|
119
|
+
--filter '{"status":3}' \
|
|
120
|
+
--limit 100 \
|
|
121
|
+
--json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Inspect ticket-linked mail without sending anything:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
zeyos list messages \
|
|
128
|
+
--fields ID,date,mailbox,subject,sender_email,to_email,ticket,reference,messageid \
|
|
129
|
+
--filter '{"ticket":42}' \
|
|
130
|
+
--sort +date \
|
|
131
|
+
--json
|
|
110
132
|
```
|
|
111
133
|
|
|
112
134
|
## Write Data
|
|
@@ -145,4 +167,6 @@ zeyos delete ticket 42 --force
|
|
|
145
167
|
- Include `visibility: 0` in filters for normal business views.
|
|
146
168
|
- Prefer `--data '<json>'` over many separate flags in automation.
|
|
147
169
|
- Run `zeyos resources` before assuming a resource is CLI-supported.
|
|
170
|
+
- Use `actionsteps.effort` for time-entry totals; do not infer booked time from task assignment.
|
|
171
|
+
- Draft e-mail text in the response unless the user explicitly asks for a real ZeyOS draft record; never send mail from an agent workflow.
|
|
148
172
|
- Escalate to [`@zeyos/client`](./03-cli-coverage-and-escalation.md) when the CLI resource registry is not enough.
|
|
@@ -13,10 +13,11 @@ The command `zeyos resources` is the source of truth for CLI-supported resource
|
|
|
13
13
|
|
|
14
14
|
| Resource | Operations |
|
|
15
15
|
|----------|------------|
|
|
16
|
-
| `account`, `appointment`, `campaign`, `contact`, `document`, `event`, `file`, `invitation`, `item`, `message`, `note`, `opportunity`, `payment`, `project`, `storage`, `task`, `ticket`, `transaction` | `list`, `get`, `create`, `update`, `delete` |
|
|
16
|
+
| `account`, `actionstep`, `appointment`, `campaign`, `contact`, `document`, `event`, `file`, `invitation`, `item`, `message`, `note`, `opportunity`, `payment`, `project`, `storage`, `task`, `ticket`, `transaction` | `list`, `get`, `create`, `update`, `delete` |
|
|
17
|
+
| `customfield` / `customfields` | `list`, `get` |
|
|
17
18
|
| `group`, `user` | `list`, `get` |
|
|
18
19
|
|
|
19
|
-
Plural names and common aliases such as `tickets`, `docs`, `invoice`, and `crm` are resolved by the CLI, but the underlying coverage boundary is still the registry above.
|
|
20
|
+
Plural names and common aliases such as `tickets`, `actionsteps`, `time-entries`, `docs`, `invoice`, and `crm` are resolved by the CLI, but the underlying coverage boundary is still the registry above.
|
|
20
21
|
|
|
21
22
|
## What the CLI Does Not Try to Cover
|
|
22
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeyos/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Dependency-light JavaScript client for ZeyOS OpenAPI services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"scripts/generate-client.mjs",
|
|
32
32
|
"openapi",
|
|
33
33
|
"docs",
|
|
34
|
-
"samples",
|
|
34
|
+
"samples/crm",
|
|
35
|
+
"samples/dashboard",
|
|
36
|
+
"samples/kanban",
|
|
35
37
|
"agents",
|
|
36
38
|
"README.md",
|
|
37
39
|
"CHANGELOG.md",
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
"generate": "node scripts/generate-client.mjs",
|
|
45
47
|
"test": "node scripts/test.mjs",
|
|
46
48
|
"test:cli-integration": "node --test cli/test/integration.mjs",
|
|
47
|
-
"test:agent-protocol": "node test/agent-protocol/harness/run.mjs"
|
|
49
|
+
"test:agent-protocol": "node test/agent-protocol/harness/run.mjs",
|
|
50
|
+
"test:agent-loop": "node test/agent-protocol/harness/loop.mjs"
|
|
48
51
|
}
|
|
49
52
|
}
|
package/src/runtime/client.js
CHANGED
|
@@ -56,8 +56,14 @@ function abortableDelay(ms, signal) {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
//
|
|
60
|
-
|
|
59
|
+
// Exponential backoff with jitter, capped at maxDelayMs.
|
|
60
|
+
function backoffDelay(attempt, retryConfig) {
|
|
61
|
+
const exp = retryConfig.baseDelayMs * Math.pow(2, attempt);
|
|
62
|
+
const jitter = Math.random() * retryConfig.baseDelayMs;
|
|
63
|
+
return Math.min(retryConfig.maxDelayMs, exp + jitter);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Honor a Retry-After header (seconds or HTTP-date), else exponential backoff.
|
|
61
67
|
function computeRetryDelay(response, attempt, retryConfig) {
|
|
62
68
|
const header = response.headers?.['retry-after'];
|
|
63
69
|
// An empty or whitespace-only header carries no delay directive — `Number('')`
|
|
@@ -74,9 +80,88 @@ function computeRetryDelay(response, attempt, retryConfig) {
|
|
|
74
80
|
return Math.min(retryConfig.maxDelayMs, Math.max(0, dateMs - Date.now()));
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
return backoffDelay(attempt, retryConfig);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Operations that are safe to transparently retry on a network error / timeout:
|
|
87
|
+
// HTTP GET/HEAD, plus ZeyOS read queries (list/count/search) which are POST but
|
|
88
|
+
// side-effect-free. Writes (create/update/delete) are never auto-retried, so a
|
|
89
|
+
// dropped connection can't cause a duplicate mutation.
|
|
90
|
+
function isReadOperation(operation) {
|
|
91
|
+
const method = operation?.method;
|
|
92
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return /^(list|count|search|exists|get)/i.test(operation?.operationId || '');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build a one-line, human-readable summary from a server error body so the thrown
|
|
99
|
+
// error message says *why* (e.g. an unknown-filter 400), not just the status code.
|
|
100
|
+
// The full body remains available on error.body.
|
|
101
|
+
function summarizeErrorBody(body, maxLength = 200) {
|
|
102
|
+
let text = '';
|
|
103
|
+
if (typeof body === 'string') {
|
|
104
|
+
text = body.trim();
|
|
105
|
+
} else if (body && typeof body === 'object') {
|
|
106
|
+
const candidate =
|
|
107
|
+
body.message ?? body.error_description ?? body.error ?? body.detail ?? body.title;
|
|
108
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
109
|
+
text = candidate.trim();
|
|
110
|
+
} else {
|
|
111
|
+
try {
|
|
112
|
+
text = JSON.stringify(body);
|
|
113
|
+
} catch {
|
|
114
|
+
text = '';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!text) {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Run fetch with an optional per-attempt timeout, composed with the caller's
|
|
125
|
+
// AbortSignal (Node 18 compatible — no AbortSignal.any). A timeout aborts the
|
|
126
|
+
// internal controller only, so `externalSignal.aborted` stays false and the
|
|
127
|
+
// caller can distinguish a timeout (retryable for reads) from a user abort.
|
|
128
|
+
async function fetchWithTimeout(httpRequestImpl, requestArgs, externalSignal, timeoutMs) {
|
|
129
|
+
if (!(timeoutMs > 0)) {
|
|
130
|
+
return httpRequestImpl({ ...requestArgs, signal: externalSignal });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
let timedOut = false;
|
|
135
|
+
const onExternalAbort = () => controller.abort(externalSignal.reason);
|
|
136
|
+
|
|
137
|
+
if (externalSignal) {
|
|
138
|
+
if (externalSignal.aborted) {
|
|
139
|
+
controller.abort(externalSignal.reason);
|
|
140
|
+
} else {
|
|
141
|
+
externalSignal.addEventListener('abort', onExternalAbort, { once: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
timedOut = true;
|
|
147
|
+
controller.abort();
|
|
148
|
+
}, timeoutMs);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
return await httpRequestImpl({ ...requestArgs, signal: controller.signal });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (timedOut) {
|
|
154
|
+
const timeoutError = new Error(`Request timed out after ${timeoutMs}ms`);
|
|
155
|
+
timeoutError.name = 'TimeoutError';
|
|
156
|
+
timeoutError.code = 'ETIMEDOUT';
|
|
157
|
+
timeoutError.isTimeout = true;
|
|
158
|
+
throw timeoutError;
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
} finally {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
externalSignal?.removeEventListener?.('abort', onExternalAbort);
|
|
164
|
+
}
|
|
80
165
|
}
|
|
81
166
|
|
|
82
167
|
const AUTH_SCHEME_MAP = Object.freeze({
|
|
@@ -390,7 +475,8 @@ function chooseBodyType(serviceKey, operation, prepared, fallbackBodyType) {
|
|
|
390
475
|
|
|
391
476
|
function createApiError(response, { serviceKey, operation, method, url }) {
|
|
392
477
|
const operationDescription = operation.operationId ? `${serviceKey}.${operation.operationId}` : `${serviceKey} request`;
|
|
393
|
-
const
|
|
478
|
+
const detail = summarizeErrorBody(response.data);
|
|
479
|
+
const message = `${operationDescription} failed with HTTP ${response.status}${detail ? `: ${detail}` : ''}`;
|
|
394
480
|
|
|
395
481
|
return new ZeyosApiError(message, {
|
|
396
482
|
status: response.status,
|
|
@@ -606,6 +692,10 @@ export function createZeyosClient(rawConfig = {}) {
|
|
|
606
692
|
|
|
607
693
|
const defaultHeaders = isObject(config.headers) ? config.headers : {};
|
|
608
694
|
const retryConfig = normalizeRetry(config.retry);
|
|
695
|
+
const defaultTimeoutMs = Number(config.timeoutMs) > 0 ? Number(config.timeoutMs) : 0;
|
|
696
|
+
// Whether to transparently retry network errors / timeouts. `undefined` (default)
|
|
697
|
+
// means "auto": retry only read operations. `true`/`false` force the behavior.
|
|
698
|
+
const defaultRetryOnNetworkError = typeof config.retryOnNetworkError === 'boolean' ? config.retryOnNetworkError : undefined;
|
|
609
699
|
const schemaApi = createSchema({ services: SERVICES, schema: SCHEMA });
|
|
610
700
|
const validateByDefault = config.validate === true;
|
|
611
701
|
const operationLookup = new Map();
|
|
@@ -616,6 +706,10 @@ export function createZeyosClient(rawConfig = {}) {
|
|
|
616
706
|
}
|
|
617
707
|
}
|
|
618
708
|
|
|
709
|
+
// Single-flight token refresh: shared across all concurrent operations so an
|
|
710
|
+
// expired token triggers exactly one getToken call (see refreshAccessTokenOnce).
|
|
711
|
+
let refreshInFlight = null;
|
|
712
|
+
|
|
619
713
|
async function getTokenSet() {
|
|
620
714
|
return normalizeTokenSet(await tokenStore.get());
|
|
621
715
|
}
|
|
@@ -644,6 +738,15 @@ export function createZeyosClient(rawConfig = {}) {
|
|
|
644
738
|
return `ZEYOSID=${cookieValue}`;
|
|
645
739
|
}
|
|
646
740
|
|
|
741
|
+
// Resolve whether a network error / timeout may be retried for this call.
|
|
742
|
+
// Explicit per-request / client config wins; otherwise auto = read ops only.
|
|
743
|
+
function resolveNetworkRetry(operation, requestOptions) {
|
|
744
|
+
const explicit = requestOptions?.retryOnNetworkError ?? defaultRetryOnNetworkError;
|
|
745
|
+
if (explicit === true) return true;
|
|
746
|
+
if (explicit === false) return false;
|
|
747
|
+
return isReadOperation(operation);
|
|
748
|
+
}
|
|
749
|
+
|
|
647
750
|
async function sendRequestOnce({ serviceKey, operation, prepared, requestAuth, tokenSet, candidate, requestOptions }) {
|
|
648
751
|
const body = cloneValue(prepared.body);
|
|
649
752
|
const authHeaders = {};
|
|
@@ -698,19 +801,27 @@ export function createZeyosClient(rawConfig = {}) {
|
|
|
698
801
|
);
|
|
699
802
|
|
|
700
803
|
const signal = prepared.signal ?? requestOptions?.signal;
|
|
804
|
+
const timeoutMs = Number(requestOptions?.timeoutMs ?? defaultTimeoutMs) || 0;
|
|
805
|
+
const networkRetryAllowed = resolveNetworkRetry(operation, requestOptions);
|
|
806
|
+
const requestArgs = { fetchImpl, url, method: operation.method, headers, body, bodyType, credentials };
|
|
701
807
|
|
|
702
808
|
let response;
|
|
703
809
|
for (let attempt = 0; ; attempt++) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
810
|
+
try {
|
|
811
|
+
response = await fetchWithTimeout(httpRequest, requestArgs, signal, timeoutMs);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
// A caller-initiated abort must never be retried — propagate immediately.
|
|
814
|
+
// (A timeout aborts only the internal controller, so signal.aborted is false.)
|
|
815
|
+
if (signal?.aborted) {
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
818
|
+
// Network error or timeout: retry only safe (read) operations, within budget.
|
|
819
|
+
if (!networkRetryAllowed || attempt >= retryConfig.maxRetries) {
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
await abortableDelay(backoffDelay(attempt, retryConfig), signal);
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
714
825
|
|
|
715
826
|
if (attempt >= retryConfig.maxRetries || !retryConfig.retryOn.has(response.status)) {
|
|
716
827
|
break;
|
|
@@ -787,7 +898,50 @@ export function createZeyosClient(rawConfig = {}) {
|
|
|
787
898
|
return nextTokenSet;
|
|
788
899
|
}
|
|
789
900
|
|
|
901
|
+
// Single-flight wrapper: when several operations notice an expired token at the
|
|
902
|
+
// same time (e.g. `Promise.all([...])`), they must share ONE refresh. Without
|
|
903
|
+
// this each fires its own getToken — redundant load, and a hard failure when the
|
|
904
|
+
// server rotates refresh tokens (all but the first present a stale refresh token).
|
|
905
|
+
function refreshAccessTokenOnce(currentTokenSet, requestAuth, requestOptions) {
|
|
906
|
+
if (refreshInFlight) {
|
|
907
|
+
return refreshInFlight;
|
|
908
|
+
}
|
|
909
|
+
refreshInFlight = Promise.resolve()
|
|
910
|
+
.then(() => refreshAccessToken(currentTokenSet, requestAuth, requestOptions))
|
|
911
|
+
.finally(() => {
|
|
912
|
+
refreshInFlight = null;
|
|
913
|
+
});
|
|
914
|
+
return refreshInFlight;
|
|
915
|
+
}
|
|
916
|
+
|
|
790
917
|
async function executeOperation({ serviceKey, operation, prepared, requestOptions = {} }) {
|
|
918
|
+
// Dry run: resolve the route + payload exactly as they would be sent, but
|
|
919
|
+
// return that descriptor instead of performing any network request or token
|
|
920
|
+
// work. Powers `zeyos … --query` and is handy for debugging/testing.
|
|
921
|
+
if (requestOptions.dryRun || prepared.dryRun) {
|
|
922
|
+
const baseUrl = resolveBaseUrl({
|
|
923
|
+
services: SERVICES,
|
|
924
|
+
serviceKey,
|
|
925
|
+
config,
|
|
926
|
+
explicitBaseUrl: prepared.baseUrl ?? requestOptions.baseUrl
|
|
927
|
+
});
|
|
928
|
+
const url = buildUrl(baseUrl, operation.path, prepared.pathParams, prepared.query);
|
|
929
|
+
const bodyType = chooseBodyType(serviceKey, operation, prepared, requestOptions?.bodyType);
|
|
930
|
+
return {
|
|
931
|
+
dryRun: true,
|
|
932
|
+
service: serviceKey,
|
|
933
|
+
operationId: operation.operationId,
|
|
934
|
+
method: operation.method,
|
|
935
|
+
url,
|
|
936
|
+
path: operation.path,
|
|
937
|
+
pathParams: prepared.pathParams,
|
|
938
|
+
query: prepared.query,
|
|
939
|
+
headers: prepared.headers,
|
|
940
|
+
body: prepared.body,
|
|
941
|
+
bodyType
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
791
945
|
const requestAuth = normalizeRequestAuth(prepared.auth ?? requestOptions.auth);
|
|
792
946
|
const mode = normalizeAuthMode(requestAuth.mode, defaultMode);
|
|
793
947
|
const schemes = securitySchemesFromOperation(operation);
|
|
@@ -799,7 +953,7 @@ export function createZeyosClient(rawConfig = {}) {
|
|
|
799
953
|
canRefreshAccessToken({ mode, operation, tokenSet, oauthConfig })
|
|
800
954
|
) {
|
|
801
955
|
try {
|
|
802
|
-
const refreshed = await
|
|
956
|
+
const refreshed = await refreshAccessTokenOnce(tokenSet, requestAuth, requestOptions);
|
|
803
957
|
if (refreshed?.accessToken) {
|
|
804
958
|
tokenSet = refreshed;
|
|
805
959
|
}
|
|
@@ -838,7 +992,7 @@ export function createZeyosClient(rawConfig = {}) {
|
|
|
838
992
|
|
|
839
993
|
if (candidate.type === 'bearer' && canRefreshAccessToken({ mode, operation, tokenSet, oauthConfig })) {
|
|
840
994
|
try {
|
|
841
|
-
const refreshed = await
|
|
995
|
+
const refreshed = await refreshAccessTokenOnce(tokenSet, requestAuth, requestOptions);
|
|
842
996
|
if (refreshed?.accessToken) {
|
|
843
997
|
tokenSet = refreshed;
|
|
844
998
|
const retryResponse = await sendRequestOnce({
|
|
@@ -990,6 +1144,58 @@ export function createZeyosClient(rawConfig = {}) {
|
|
|
990
1144
|
const oauth2Operations = bindService('oauth2');
|
|
991
1145
|
const legacyAuth = bindService('legacyAuth');
|
|
992
1146
|
|
|
1147
|
+
// Async-iterate every record from a list operation, paging by `offset` until a
|
|
1148
|
+
// short/empty page (or `opts.max`) is reached — removing the manual offset
|
|
1149
|
+
// bookkeeping the 1000/10000 list caps otherwise force on callers.
|
|
1150
|
+
//
|
|
1151
|
+
// for await (const ticket of client.paginate('listTickets', { filters: { visibility: 0 } })) { … }
|
|
1152
|
+
//
|
|
1153
|
+
async function* paginate(operationId, input = {}, opts = {}) {
|
|
1154
|
+
const op = operationLookup.get(`api.${operationId}`);
|
|
1155
|
+
if (!op) {
|
|
1156
|
+
const candidates = (SERVICES.api?.operations ?? []).map((entry) => entry.operationId);
|
|
1157
|
+
const suggestion = suggestClosest(operationId, candidates);
|
|
1158
|
+
throw new ZeyosApiError(
|
|
1159
|
+
`Unknown list operation: api.${operationId}.` + (suggestion ? ` Did you mean '${suggestion}'?` : ''),
|
|
1160
|
+
{ operationId, service: 'api' }
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const requested = Number(opts.pageSize) > 0 ? Number(opts.pageSize)
|
|
1165
|
+
: Number(input.limit) > 0 ? Number(input.limit) : 1000;
|
|
1166
|
+
// Clamp to the server's max page size: a larger request would be silently
|
|
1167
|
+
// capped server-side, making the short-page terminator stop early and drop rows.
|
|
1168
|
+
const pageSize = Math.min(requested, 10000);
|
|
1169
|
+
const max = Number(opts.max) > 0 ? Number(opts.max) : Infinity;
|
|
1170
|
+
let offset = Number(input.offset) > 0 ? Number(input.offset) : 0;
|
|
1171
|
+
let yielded = 0;
|
|
1172
|
+
|
|
1173
|
+
for (;;) {
|
|
1174
|
+
const page = await api[operationId]({ ...input, limit: pageSize, offset }, opts.requestOptions);
|
|
1175
|
+
const rows = Array.isArray(page) ? page : Array.isArray(page?.data) ? page.data : [];
|
|
1176
|
+
for (const row of rows) {
|
|
1177
|
+
yield row;
|
|
1178
|
+
yielded += 1;
|
|
1179
|
+
if (yielded >= max) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (rows.length < pageSize) {
|
|
1184
|
+
return; // last (short or empty) page
|
|
1185
|
+
}
|
|
1186
|
+
offset += pageSize;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Eager convenience: collect up to `opts.max` records into an array.
|
|
1191
|
+
async function collect(operationId, input = {}, opts = {}) {
|
|
1192
|
+
const out = [];
|
|
1193
|
+
for await (const row of paginate(operationId, input, opts)) {
|
|
1194
|
+
out.push(row);
|
|
1195
|
+
}
|
|
1196
|
+
return out;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
993
1199
|
function buildAuthorizationUrl(options = {}) {
|
|
994
1200
|
const clientId = options.clientId ?? options.client_id ?? oauthConfig.clientId;
|
|
995
1201
|
const redirectUri = options.redirectUri ?? options.redirect_uri;
|
|
@@ -1192,6 +1398,8 @@ export function createZeyosClient(rawConfig = {}) {
|
|
|
1192
1398
|
oauth2,
|
|
1193
1399
|
legacyAuth,
|
|
1194
1400
|
request,
|
|
1401
|
+
paginate,
|
|
1402
|
+
collect,
|
|
1195
1403
|
schema: schemaApi,
|
|
1196
1404
|
auth: {
|
|
1197
1405
|
getTokenSet,
|