@deepdockai/cli 0.0.0-stage → 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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +261 -2
- package/SECURITY.md +7 -0
- package/dist/deepdock.js +13417 -0
- package/package.json +46 -4
- package/skill/SKILL.md +109 -0
- package/skill/references/auth-and-keys.md +102 -0
- package/skill/references/calling-actions.md +111 -0
- package/skill/references/cli.md +97 -0
- package/skill/references/clients/deepdock.py +163 -0
- package/skill/references/clients/deepdock.ts +244 -0
- package/skill/references/concepts.md +95 -0
- package/skill/references/discovery.md +78 -0
- package/skill/references/errors.md +110 -0
- package/skill/references/example-integration.md +159 -0
- package/skill/references/idempotency.md +97 -0
- package/skill/references/testing.md +80 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Deepdock /v1 client. No dependencies beyond fetch (Node 18+, Bun, Deno, browsers).
|
|
3
|
+
*
|
|
4
|
+
* It does the four things the OpenAPI document cannot tell you: the bearer header, the envelope
|
|
5
|
+
* unwrap, the Idempotency-Key header with replay detection, and a typed error carrying the code.
|
|
6
|
+
* Everything action-specific (paths, fields, methods) comes from the live OpenAPI document.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class DeepdockError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
readonly status: number,
|
|
12
|
+
readonly code: string,
|
|
13
|
+
message: string,
|
|
14
|
+
readonly requestId?: string,
|
|
15
|
+
readonly systems?: string[],
|
|
16
|
+
) {
|
|
17
|
+
super(message)
|
|
18
|
+
this.name = 'DeepdockError'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** No answer, or not Deepdock's envelope (timeout, network, a proxy page): a write may still have landed. */
|
|
23
|
+
export class DeepdockTransportError extends Error {
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super(message)
|
|
26
|
+
this.name = 'DeepdockTransportError'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ErrorBody = { code: string; message: string; requestId?: string; systems?: string[] }
|
|
31
|
+
type Envelope<T> = { data: T } | { error: ErrorBody }
|
|
32
|
+
|
|
33
|
+
export type Page<Item> = { items: Item[]; nextCursor: string | null }
|
|
34
|
+
export type WriteResult<T> = { data: T; replayed: boolean }
|
|
35
|
+
export type WriteMethod = 'POST' | 'PATCH'
|
|
36
|
+
|
|
37
|
+
export type Device = {
|
|
38
|
+
id: string
|
|
39
|
+
label: string | null
|
|
40
|
+
hostname: string
|
|
41
|
+
os: string
|
|
42
|
+
clientVersion: string
|
|
43
|
+
status: 'connected' | 'offline' | 'inactive'
|
|
44
|
+
lastSeenAt: string | null
|
|
45
|
+
claimedAt: string
|
|
46
|
+
systems: string[]
|
|
47
|
+
updateAvailable?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ActionState =
|
|
51
|
+
| { state: 'active' }
|
|
52
|
+
| { state: 'paused'; reason: string; pausedAt: string }
|
|
53
|
+
|
|
54
|
+
export type DeviceSystem = {
|
|
55
|
+
label: string | null
|
|
56
|
+
version: string
|
|
57
|
+
status: 'running' | 'stopped' | 'missing'
|
|
58
|
+
actions: Record<string, ActionState>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type DeepdockOptions = {
|
|
62
|
+
base?: string
|
|
63
|
+
/** Above Deepdock's 20 s device wait, so you receive device_offline instead of timing out first. */
|
|
64
|
+
timeoutMs?: number
|
|
65
|
+
fetch?: typeof fetch
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class Deepdock {
|
|
69
|
+
private readonly base: string
|
|
70
|
+
private readonly timeoutMs: number
|
|
71
|
+
private readonly fetchImpl: typeof fetch
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
private readonly apiKey: string,
|
|
75
|
+
{ base = 'https://api.deepdock.ai', timeoutMs = 30_000, fetch: fetchImpl = fetch }: DeepdockOptions = {},
|
|
76
|
+
) {
|
|
77
|
+
this.base = base.replace(/\/$/, '')
|
|
78
|
+
this.timeoutMs = timeoutMs
|
|
79
|
+
this.fetchImpl = fetchImpl
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async send<T>(
|
|
83
|
+
path: string,
|
|
84
|
+
init: { method: string; headers?: Record<string, string>; body?: string },
|
|
85
|
+
): Promise<{ data: T; headers: Headers }> {
|
|
86
|
+
let res: Response
|
|
87
|
+
try {
|
|
88
|
+
res = await this.fetchImpl(`${this.base}${path}`, {
|
|
89
|
+
method: init.method,
|
|
90
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, ...init.headers },
|
|
91
|
+
body: init.body,
|
|
92
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
93
|
+
})
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// TimeoutError from AbortSignal.timeout, AbortError, or a network TypeError.
|
|
96
|
+
throw new DeepdockTransportError(`no answer from Deepdock: ${(err as Error).name}`)
|
|
97
|
+
}
|
|
98
|
+
let body: Envelope<T>
|
|
99
|
+
try {
|
|
100
|
+
body = (await res.json()) as Envelope<T>
|
|
101
|
+
} catch {
|
|
102
|
+
throw new DeepdockTransportError(`non-JSON answer with status ${res.status}`)
|
|
103
|
+
}
|
|
104
|
+
if (body !== null && typeof body === 'object' && 'error' in body) {
|
|
105
|
+
const { code, message, requestId, systems } = body.error
|
|
106
|
+
throw new DeepdockError(res.status, code, message, requestId, systems)
|
|
107
|
+
}
|
|
108
|
+
if (body === null || typeof body !== 'object' || !('data' in body)) {
|
|
109
|
+
throw new DeepdockTransportError(`unexpected body with status ${res.status}`)
|
|
110
|
+
}
|
|
111
|
+
return { data: body.data, headers: res.headers }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private actionPath(deviceId: string, system: string, action: string): string {
|
|
115
|
+
return `/v1/devices/${encodeURIComponent(deviceId)}/systems/${encodeURIComponent(system)}/actions/${encodeURIComponent(action)}`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Runs a read action. Pass `nextCursor` back as `cursor` for the next page. */
|
|
119
|
+
async read<Item>(
|
|
120
|
+
deviceId: string,
|
|
121
|
+
system: string,
|
|
122
|
+
action: string,
|
|
123
|
+
input: Record<string, string>,
|
|
124
|
+
page: { limit?: number; cursor?: string } = {},
|
|
125
|
+
): Promise<Page<Item>> {
|
|
126
|
+
const query = new URLSearchParams(input)
|
|
127
|
+
if (page.limit !== undefined) query.set('limit', String(page.limit))
|
|
128
|
+
if (page.cursor !== undefined) query.set('cursor', page.cursor)
|
|
129
|
+
const suffix = query.size > 0 ? `?${query}` : ''
|
|
130
|
+
return (await this.send<Page<Item>>(`${this.actionPath(deviceId, system, action)}${suffix}`, { method: 'GET' })).data
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Reads every page of a read action, in order. */
|
|
134
|
+
async *readAll<Item>(
|
|
135
|
+
deviceId: string,
|
|
136
|
+
system: string,
|
|
137
|
+
action: string,
|
|
138
|
+
input: Record<string, string>,
|
|
139
|
+
limit?: number,
|
|
140
|
+
): AsyncGenerator<Item> {
|
|
141
|
+
let cursor: string | undefined
|
|
142
|
+
do {
|
|
143
|
+
const page = await this.read<Item>(deviceId, system, action, input, { limit, cursor })
|
|
144
|
+
for (const item of page.items) yield item
|
|
145
|
+
cursor = page.nextCursor ?? undefined
|
|
146
|
+
} while (cursor !== undefined)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Runs a write action. `method` comes from the action index or the OpenAPI document; `input`
|
|
151
|
+
* values are all strings. `idempotencyKey` is new per intended write and the same on every retry.
|
|
152
|
+
*/
|
|
153
|
+
async write<Result>(
|
|
154
|
+
deviceId: string,
|
|
155
|
+
system: string,
|
|
156
|
+
action: string,
|
|
157
|
+
method: WriteMethod,
|
|
158
|
+
idempotencyKey: string,
|
|
159
|
+
input: Record<string, string>,
|
|
160
|
+
): Promise<WriteResult<Result>> {
|
|
161
|
+
const { data, headers } = await this.send<Result>(this.actionPath(deviceId, system, action), {
|
|
162
|
+
method,
|
|
163
|
+
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
|
|
164
|
+
body: JSON.stringify(input),
|
|
165
|
+
})
|
|
166
|
+
return { data, replayed: headers.get('Idempotent-Replayed') === 'true' }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The device's systems and each action's state. A live call to the device: can answer device_offline. */
|
|
170
|
+
async systems(deviceId: string): Promise<Record<string, DeviceSystem>> {
|
|
171
|
+
return (await this.send<Record<string, DeviceSystem>>(`/v1/devices/${encodeURIComponent(deviceId)}/systems`, { method: 'GET' })).data
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Claims a device by the pairing code it shows. Keep the returned `id`. */
|
|
175
|
+
async pair(code: string, label?: string): Promise<Device> {
|
|
176
|
+
return (
|
|
177
|
+
await this.send<Device>('/v1/devices', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify(label === undefined ? { code } : { code, label }),
|
|
181
|
+
})
|
|
182
|
+
).data
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async release(deviceId: string): Promise<void> {
|
|
186
|
+
await this.send<unknown>(`/v1/devices/${encodeURIComponent(deviceId)}`, { method: 'DELETE' })
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ------------------------------------------------------------------------------------------------
|
|
191
|
+
* Pending writes: one attempt against persisted state. Persist the row BEFORE the first send, so a
|
|
192
|
+
* crash between send and answer cannot produce a second record under a new key. A job retries rows
|
|
193
|
+
* left in `sent` with backoff (seconds, then minutes, for hours if the device stays offline).
|
|
194
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
195
|
+
|
|
196
|
+
export type PendingStatus = 'pending' | 'sent' | 'done' | 'unknown' | 'refused'
|
|
197
|
+
|
|
198
|
+
export type PendingWrite = {
|
|
199
|
+
/** 8 to 128 chars of A-Z a-z 0-9 _ -, stable per business event. */
|
|
200
|
+
key: string
|
|
201
|
+
deviceId: string
|
|
202
|
+
system: string
|
|
203
|
+
action: string
|
|
204
|
+
method: WriteMethod
|
|
205
|
+
input: Record<string, string>
|
|
206
|
+
status: PendingStatus
|
|
207
|
+
conflictRetries: number
|
|
208
|
+
result?: unknown
|
|
209
|
+
requestId?: string
|
|
210
|
+
refusedCode?: string
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A running write finishes within seconds; beyond this, treat a conflict as an unknown outcome. */
|
|
214
|
+
export const CONFLICT_RETRIES = 5
|
|
215
|
+
|
|
216
|
+
const RETRY_SAME_KEY = new Set([
|
|
217
|
+
'device_offline',
|
|
218
|
+
'target_busy',
|
|
219
|
+
'temporarily_unavailable',
|
|
220
|
+
'internal_error',
|
|
221
|
+
'action_paused',
|
|
222
|
+
'system_unavailable',
|
|
223
|
+
])
|
|
224
|
+
|
|
225
|
+
/** One attempt; returns the row with its new status. The caller persists it. */
|
|
226
|
+
export const attempt = async (dd: Deepdock, row: PendingWrite): Promise<PendingWrite> => {
|
|
227
|
+
try {
|
|
228
|
+
const { data } = await dd.write(row.deviceId, row.system, row.action, row.method, row.key, row.input)
|
|
229
|
+
return { ...row, status: 'done', result: data }
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (err instanceof DeepdockTransportError) return { ...row, status: 'sent' }
|
|
232
|
+
if (!(err instanceof DeepdockError)) throw err
|
|
233
|
+
if (RETRY_SAME_KEY.has(err.code)) return { ...row, status: 'sent', requestId: err.requestId }
|
|
234
|
+
if (err.code === 'idempotency_conflict') {
|
|
235
|
+
return row.conflictRetries < CONFLICT_RETRIES
|
|
236
|
+
? { ...row, status: 'sent', conflictRetries: row.conflictRetries + 1 }
|
|
237
|
+
: { ...row, status: 'unknown' }
|
|
238
|
+
}
|
|
239
|
+
if (err.code === 'outcome_unknown') return { ...row, status: 'unknown', requestId: err.requestId }
|
|
240
|
+
// invalid_input, target_rejected, action-specific 422s, not_found, check_failed, auth and
|
|
241
|
+
// resolution codes: nothing was written. Fix the cause; a changed input gets a new key.
|
|
242
|
+
return { ...row, status: 'refused', refusedCode: err.code, result: { message: err.message } }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Concepts
|
|
2
|
+
|
|
3
|
+
The words the API, the CLI and the dashboard use. Use them unchanged in code and copy.
|
|
4
|
+
|
|
5
|
+
## Organisation and partner
|
|
6
|
+
|
|
7
|
+
You, the partner, are an **organisation** in Deepdock. Everything you own (keys, devices,
|
|
8
|
+
incidents) is scoped to it. Members of your organisation see the dashboard; admins manage keys,
|
|
9
|
+
release devices.
|
|
10
|
+
|
|
11
|
+
## Keys
|
|
12
|
+
|
|
13
|
+
- **API key** (`ddk_live_` on production; a `ddk_test_` key belongs to a test deployment): your product's
|
|
14
|
+
credential on `/v1`. It runs actions, pairs and releases devices, and reads incidents. Created by an
|
|
15
|
+
admin in the dashboard (shown once) or from the CLI, which writes it straight into the app's env
|
|
16
|
+
file, keeps no copy and shows it to nobody; optionally expiring.
|
|
17
|
+
- **CLI sign-in**: one person's credential, used only by the `deepdock` CLI after `deepdock auth
|
|
18
|
+
login`. Valid 90 days. It manages (devices, keys, listing systems) and never runs an action.
|
|
19
|
+
|
|
20
|
+
Details: [auth-and-keys.md](auth-and-keys.md).
|
|
21
|
+
|
|
22
|
+
## Device
|
|
23
|
+
|
|
24
|
+
The Deepdock client installed on the customer's Windows PC next to their system. It connects
|
|
25
|
+
outwards to Deepdock; nothing in the customer's network is opened to the internet. A device is
|
|
26
|
+
identified by a UUID (`id`), carries a `label`, `hostname`, `os`, `clientVersion`, `status`
|
|
27
|
+
(`connected`, `offline` or `inactive`), `lastSeenAt`, `claimedAt` and the `systems` it reported.
|
|
28
|
+
|
|
29
|
+
- **Pair**: claim a device for your organisation with the 6-character code it shows while unpaired.
|
|
30
|
+
- **Release**: `DELETE /v1/devices/{deviceId}` gives the device back; it shows a new code.
|
|
31
|
+
- **Inactive**: a paired device that has not connected for 7 days. It stays yours and is active again
|
|
32
|
+
when it connects.
|
|
33
|
+
- **Update**: the client updates itself. A device on an older build than Deepdock ships reports
|
|
34
|
+
`updateAvailable`.
|
|
35
|
+
|
|
36
|
+
## System
|
|
37
|
+
|
|
38
|
+
A program on the device that Deepdock has made reachable, named by a slug such as `example-erp`. Each
|
|
39
|
+
has a display `label` (may be `null`), a `version` the device reported, a `status`, and actions.
|
|
40
|
+
`GET /v1/devices/{deviceId}/systems` is a live call to the device, so it can answer
|
|
41
|
+
`device_offline`.
|
|
42
|
+
|
|
43
|
+
### Every status value and what it means for you
|
|
44
|
+
|
|
45
|
+
| Field | Value | Meaning | What to do |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| device `status` | `connected` | the device is online and can run actions | normal |
|
|
48
|
+
| device `status` | `offline` | the device is not connected right now (PC off, no network) | calls answer `device_offline`; retry later, show "offline" |
|
|
49
|
+
| device `status` | `inactive` | not connected for 7 days; still paired | as offline; check with the site whether the PC still exists |
|
|
50
|
+
| system `status` | `running` | the customer's program is running on the device | normal |
|
|
51
|
+
| system `status` | `stopped` | the program is installed but not running | calls answer `system_unavailable`; the site starts the program |
|
|
52
|
+
| system `status` | `missing` | the program was not found where the device expects it | calls answer `system_unavailable`; contact the site and Deepdock |
|
|
53
|
+
| action state | `active` | the action can be called | normal |
|
|
54
|
+
| action state | `paused` | the action is paused with a `reason` and `pausedAt` | calls answer `action_paused`; show the feature as paused, poll `GET .../systems` at a low rate |
|
|
55
|
+
| pause `reason` | `structure_changed` | the system changed in a way the action depends on (for example a vendor update); Deepdock reviews and resumes | wait; nothing to do on your side |
|
|
56
|
+
| pause `reason` | `check_failed` | a write's result did not match and was undone | wait; tell the user the change was not saved |
|
|
57
|
+
| pause `reason` | `system_stopped`, `system_missing` | as the system statuses above; these need no resume and clear when the system is back | as above |
|
|
58
|
+
|
|
59
|
+
Switch on these values; any value you do not know is shown as it is.
|
|
60
|
+
|
|
61
|
+
## Action
|
|
62
|
+
|
|
63
|
+
One deterministic, validated operation on a system: for example search customers by surname prefix,
|
|
64
|
+
create a note, update a note's text. Deepdock builds and verifies each action before
|
|
65
|
+
offering it; it then runs the same way every time. Actions are
|
|
66
|
+
**assigned** to your organisation by Deepdock, so the set you can call is yours, not the whole
|
|
67
|
+
catalogue.
|
|
68
|
+
|
|
69
|
+
- A **read** is `GET` with query parameters and pages.
|
|
70
|
+
- A **write** is `POST` (creates a record, answers `201`) or `PATCH` (updates a record, answers
|
|
71
|
+
`200`), with a JSON body of string fields and an `Idempotency-Key` header.
|
|
72
|
+
- A write **either fully applies or not**: Deepdock checks the result before confirming, and undoes
|
|
73
|
+
the write and pauses the action if the check fails (`409 check_failed`).
|
|
74
|
+
- An action is **active** or **paused**. `GET /v1/devices/{deviceId}/systems` shows each action's
|
|
75
|
+
state with `reason` and `pausedAt`.
|
|
76
|
+
|
|
77
|
+
## Pause and incident
|
|
78
|
+
|
|
79
|
+
Deepdock pauses an action instead of guessing when the customer's system no longer looks the way
|
|
80
|
+
the action expects:
|
|
81
|
+
|
|
82
|
+
| Reason | What you get | Meaning |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `structure_changed` | `503 action_paused` | the system changed (for example a vendor update); Deepdock reviews and resumes |
|
|
85
|
+
| `check_failed` | that call `409 check_failed`, later calls `503 action_paused` | a write's result did not match and was undone |
|
|
86
|
+
| `system_stopped`, `system_missing` | `503 system_unavailable` | the customer's program is stopped or not found on the device |
|
|
87
|
+
|
|
88
|
+
Each pause opens an **incident** (`GET /v1/incidents`). Only Deepdock lifts a pause; staff at the
|
|
89
|
+
customer's site see that an action is paused but cannot resume it; there is no partner call to
|
|
90
|
+
resume. A stopped or missing system needs no resume: its incident closes once the system is back.
|
|
91
|
+
|
|
92
|
+
An incident has `id`, `system`, `version`, `action` (`null` for a system-level incident), `reason`,
|
|
93
|
+
`openedAt`, `closedAt`, and `devices` with `deviceId`, `pausedAt`, `resumedAt` and `inactiveAt`. It
|
|
94
|
+
closes once none of its devices is still paused. `GET /v1/incidents` lists incidents that are open
|
|
95
|
+
or closed within the last 30 days, newest first, at most 100.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Discovering systems and actions
|
|
2
|
+
|
|
3
|
+
Never hard-code an action's fields. Three sources, each authoritative for one thing:
|
|
4
|
+
|
|
5
|
+
| Question | Source | Key |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Which systems and actions does my organisation have? | `deepdock systems list --json` | CLI sign-in |
|
|
8
|
+
| What does this device run, and is each action active or paused? | `GET /v1/devices/{deviceId}/systems` | organisation |
|
|
9
|
+
| Which actions can I call on this device's system, with which schemas? | `GET /v1/devices/{deviceId}/systems/{system}/actions` | organisation |
|
|
10
|
+
| The exact paths, methods, fields, patterns, enums, lengths, responses | `deepdock systems openapi <system>` or `deepdock actions list <device> <system>` (or `GET /v1/systems/{system}/openapi.json`) | CLI sign-in or API key |
|
|
11
|
+
|
|
12
|
+
## The OpenAPI document
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
deepdock systems openapi example-erp --org <slug> > openapi/example-erp.json
|
|
16
|
+
# from your backend, with the API key:
|
|
17
|
+
curl "$BASE/v1/systems/example-erp/openapi.json" -H "Authorization: Bearer $DEEPDOCK_API_KEY"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
OpenAPI 3.1, one document per system, cacheable for 60 seconds. Discover through the CLI; check the
|
|
21
|
+
document into the repository and refresh it with the same command when you touch the integration.
|
|
22
|
+
|
|
23
|
+
What to read off it:
|
|
24
|
+
|
|
25
|
+
- **`paths`**: one operation per action at
|
|
26
|
+
`/v1/devices/{deviceId}/systems/{system}/actions/{action}`; the HTTP method is the action's
|
|
27
|
+
kind (`get` read, `post` create, `patch` update). Use the method the document gives; the other
|
|
28
|
+
write method answers `405 invalid_request` with an `Allow` header.
|
|
29
|
+
- **`summary` and `description`**: what the action does and what it changes in the system, what
|
|
30
|
+
an omitted optional input becomes, action-specific refusals (`code` and when), and what the
|
|
31
|
+
system only warns about and what the action does then. Read the description before mapping your
|
|
32
|
+
fields.
|
|
33
|
+
- **Read inputs** are query parameters, all `type: string`, each with `maxLength` and possibly
|
|
34
|
+
`minLength`, `pattern`, `enum` and a `description` such as "prefix match: case, punctuation and
|
|
35
|
+
umlaut spelling are ignored". Plus `limit` (integer, 1 to 250, default 25) and `cursor` (opaque).
|
|
36
|
+
An optional parameter is not in `required` and its description says what an omitted value
|
|
37
|
+
becomes.
|
|
38
|
+
- **Write inputs** are the `requestBody` object schema: `additionalProperties: false`, every
|
|
39
|
+
property `type: string`, `required` lists the mandatory ones. The `Idempotency-Key` header
|
|
40
|
+
parameter carries its own pattern (8 to 128 characters of `A-Z a-z 0-9 _ -`).
|
|
41
|
+
- **Responses**: a read's `200` is `{ data: { items: [...], nextCursor } }`; a create's `201` and a
|
|
42
|
+
replayed `200`; an update's `200`. Item and result fields are typed `string`, `integer` or `number`,
|
|
43
|
+
often nullable (`["integer", "null"]`). The `Error` schema's `code` enum is the full list of codes.
|
|
44
|
+
- **`servers`**: the base URL of the API.
|
|
45
|
+
|
|
46
|
+
The document describes **Deepdock's whole catalogue** for the system. Your organisation may be
|
|
47
|
+
assigned a subset. Write code only for actions that appear in both the document and `systems list`
|
|
48
|
+
(or the device's action index); an action in the document but not assigned to you answers
|
|
49
|
+
`404 action_not_found`.
|
|
50
|
+
|
|
51
|
+
## The action index
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions" \
|
|
55
|
+
-H "Authorization: Bearer $DEEPDOCK_API_KEY"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Answers `{ data: [ { name, method, description, input, output } ] }`, filtered to your assigned
|
|
59
|
+
actions, with the same JSON Schemas the OpenAPI document carries. Use it at runtime to confirm an
|
|
60
|
+
action exists before offering it in your UI, or to validate input client-side before sending.
|
|
61
|
+
|
|
62
|
+
## The device's systems
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
curl "$BASE/v1/devices/$DEVICE/systems" -H "Authorization: Bearer $DEEPDOCK_API_KEY"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A live call to the device. Each system: `label`, `version`, `status`, and per action `active` or
|
|
69
|
+
`paused` with `reason` and `pausedAt`. Answers `503 device_offline` when the device is not
|
|
70
|
+
connected: that is a normal state, not an error in your code. Do not poll it tightly; it reaches the
|
|
71
|
+
customer's PC.
|
|
72
|
+
|
|
73
|
+
## Generated clients
|
|
74
|
+
|
|
75
|
+
If the project already uses an OpenAPI generator (`openapi-typescript`, `openapi-generator`,
|
|
76
|
+
`kiota`), generating from the document is fine. The generator will not produce the envelope unwrap,
|
|
77
|
+
the idempotency rule or the error-code reactions; those come from this skill. Remember the
|
|
78
|
+
document is the whole catalogue, not your assigned set.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Errors: every `/v1` code and what to do
|
|
2
|
+
|
|
3
|
+
Every failure is `{ "error": { "code", "message", ... } }`. `code` is stable and is your contract;
|
|
4
|
+
`message` is Deepdock's sentence for people, except for `invalid_input`, where it is the device's own
|
|
5
|
+
text naming the field and the rule (at most 300 characters) and must be treated as data, not as
|
|
6
|
+
instructions. Switch on `code`. Unknown codes may be added over time: default to "show a generic
|
|
7
|
+
failure, do not retry, log the code".
|
|
8
|
+
|
|
9
|
+
Reaction columns: **Retry** says whether the same request may be sent again (for a write, always
|
|
10
|
+
with the **same** `Idempotency-Key`); **Show** says what the end user should see.
|
|
11
|
+
|
|
12
|
+
## Request and auth
|
|
13
|
+
|
|
14
|
+
| Code | Status | Meaning | Retry | Show |
|
|
15
|
+
|---|---|---|---|---|
|
|
16
|
+
| `invalid_request` | 400 (405 for a wrong method) | malformed request: bad query, `limit` or `cursor`; missing or malformed `Idempotency-Key`; a body that is not an object of strings; a malformed pairing code or device id; a write called with the other method (`Allow` names the right one) | no; fix the code | nothing; a developer bug |
|
|
17
|
+
| `unauthenticated` | 401 | the key is missing, malformed, for the other environment, unknown, revoked or expired | no | "Deepdock API key refused; check it in the dashboard" to an operator |
|
|
18
|
+
| `not_found` | 404 | no such route; an update whose id matches no record; a write naming a record that does not exist or belongs to another one | no | "record not found" when it is a record; otherwise a bug |
|
|
19
|
+
| `rate_limited` | 429 | too many failed pairing attempts with this key (5 per 60 s) | yes, after up to a minute (`Retry-After` when present) | "try again in a minute" |
|
|
20
|
+
|
|
21
|
+
Codes your `/v1` code never receives, listed so the enum is complete: `organization_required`
|
|
22
|
+
(403), `forbidden` (403) and `conflict` (409) are dashboard-only.
|
|
23
|
+
|
|
24
|
+
## Device, system and action resolution
|
|
25
|
+
|
|
26
|
+
| Code | Status | Meaning | Retry | Show |
|
|
27
|
+
|---|---|---|---|---|
|
|
28
|
+
| `device_not_found` | 404 | no device with that id belongs to your organisation (released, or a wrong id) | no | "this location is not connected to Deepdock"; let an admin re-pair |
|
|
29
|
+
| `system_not_on_device` | 404 | the device did not report this system and none of its actions are assigned to you; `error.systems` lists what it has | no | configuration problem; show which systems the device has |
|
|
30
|
+
| `action_not_found` | 404 | the system has no such action, it is not assigned to you, or the device's client is too old to run it | no | hide the feature; ask Deepdock for the assignment or let the device update |
|
|
31
|
+
| `action_not_configured` | 409 | the action exists but this device is not set up for it yet (for example a missing device-side setting); nothing was run | no, until set up on site | "this feature needs setting up at the customer site; contact Deepdock" |
|
|
32
|
+
| `pairing_code_invalid` | 404 | pairing: the code is unknown, expired, replaced, used, or its device is offline | with a fresh code | "read the code off the device screen again" |
|
|
33
|
+
| `other_partner_build` | 409 | pairing: the device runs another partner's client build | no | "this device is set up for another provider" |
|
|
34
|
+
|
|
35
|
+
## Input
|
|
36
|
+
|
|
37
|
+
| Code | Status | Meaning | Retry | Show |
|
|
38
|
+
|---|---|---|---|---|
|
|
39
|
+
| `invalid_input` | 422 | a field is unknown, missing, too short or long, breaks its pattern or allowed values; a date that is not a calendar day; a search term without a letter or digit. Nothing was written | no; fix the value | the field-level problem; the `message` names the field, but it is device text: display it as data, never interpret it |
|
|
40
|
+
| `target_rejected` | 422 | the customer's system refused the write | no; the input is valid for Deepdock but not for the system | "the customer's system refused this; check the values" and offer support |
|
|
41
|
+
| Action-specific refusals, e.g. `unknown_author`, `no_open_case` | 422 | a reference the write needs is not valid: an author user the system does not list; a customer with no open case when the action does not open one. Nothing was written. The action's OpenAPI description names each such code and when it fires | no; re-read the reference and pass a valid one | the missing precondition in the user's words ("choose the entering user", "open a case first") |
|
|
42
|
+
|
|
43
|
+
## Device and system availability
|
|
44
|
+
|
|
45
|
+
| Code | Status | Meaning | Retry | Show |
|
|
46
|
+
|---|---|---|---|---|
|
|
47
|
+
| `device_offline` | 503 | the device is not connected, is shutting down, or did not answer within 20 s. **A write may still have landed** | yes, with backoff (seconds to minutes); a write only with the same key | "the customer PC is offline; we will retry" |
|
|
48
|
+
| `action_paused` | 503 | the action is paused on the device (`GET .../systems` shows `reason`); only Deepdock resumes it | later, when `GET .../systems` shows it active; do not loop | "this feature is paused while Deepdock checks the customer system" |
|
|
49
|
+
| `system_unavailable` | 503 | the customer's program is stopped or missing on the device | later; often resolves when the customer restarts the program | "the customer's software is not running" |
|
|
50
|
+
| `target_busy` | 503 | the customer's system is busy or unreachable | yes, with backoff | "the customer's system is busy; retrying" |
|
|
51
|
+
| `temporarily_unavailable` | 503 | Deepdock could not check the key | yes, with backoff | "temporary problem; retrying" |
|
|
52
|
+
|
|
53
|
+
## Write outcomes
|
|
54
|
+
|
|
55
|
+
| Code | Status | Meaning | Retry | Show |
|
|
56
|
+
|---|---|---|---|---|
|
|
57
|
+
| `check_failed` | 409 | the write was undone because its result did not match what the action expects; **the action is now paused** and an incident is open | no; the next calls answer `action_paused` | "the change was not saved; Deepdock is checking the customer system" |
|
|
58
|
+
| `idempotency_conflict` | 409 | the key was used with a different input, its write is still running, or its outcome is unknown. One code, three causes; `message` must not be parsed to tell them apart | retry with the **same key and the same input** a bounded number of times with backoff (a running write finishes within seconds); if it still conflicts, handle it like `outcome_unknown`: read back, then decide | "a previous attempt is still in progress" |
|
|
59
|
+
| `outcome_unknown` | 500 | the system did not confirm the write; it may or may not have landed. The key stays blocked | **no.** Read the record back (a search) to see whether it landed before writing again under a new key; involve a human when the read cannot tell | "we could not confirm the change; please check in the customer system before trying again" |
|
|
60
|
+
| `internal_error` | 500 | something failed inside Deepdock; `error.requestId` is set | once, later; a write with the same key | "something went wrong"; log `requestId` for Deepdock support |
|
|
61
|
+
|
|
62
|
+
## A reaction table for code
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
type Reaction = 'fix-code' | 'fix-input' | 'retry-same-key' | 'retry-later' | 'wait-paused' | 'check-then-decide' | 'stop'
|
|
66
|
+
|
|
67
|
+
const REACTION: Record<string, Reaction> = {
|
|
68
|
+
invalid_request: 'fix-code',
|
|
69
|
+
unauthenticated: 'stop',
|
|
70
|
+
not_found: 'fix-input',
|
|
71
|
+
rate_limited: 'retry-later',
|
|
72
|
+
device_not_found: 'stop',
|
|
73
|
+
system_not_on_device: 'stop',
|
|
74
|
+
action_not_found: 'stop',
|
|
75
|
+
action_not_configured: 'stop',
|
|
76
|
+
pairing_code_invalid: 'fix-input',
|
|
77
|
+
other_partner_build: 'stop',
|
|
78
|
+
invalid_input: 'fix-input',
|
|
79
|
+
target_rejected: 'fix-input',
|
|
80
|
+
unknown_author: 'fix-input',
|
|
81
|
+
no_open_case: 'fix-input',
|
|
82
|
+
device_offline: 'retry-same-key',
|
|
83
|
+
action_paused: 'wait-paused',
|
|
84
|
+
system_unavailable: 'retry-later',
|
|
85
|
+
target_busy: 'retry-same-key',
|
|
86
|
+
temporarily_unavailable: 'retry-same-key',
|
|
87
|
+
check_failed: 'wait-paused',
|
|
88
|
+
idempotency_conflict: 'retry-same-key', // bounded; then 'check-then-decide'
|
|
89
|
+
outcome_unknown: 'check-then-decide',
|
|
90
|
+
internal_error: 'retry-same-key',
|
|
91
|
+
}
|
|
92
|
+
const reactionFor = (code: string): Reaction => REACTION[code] ?? 'stop'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Action-specific 422 codes (`unknown_author` and `no_open_case` are two; any the OpenAPI description
|
|
96
|
+
lists) map to `fix-input`. Add them from the document, not from memory. `retry-same-key` means the
|
|
97
|
+
pending write stays persisted and is retried from that state with backoff, also across process
|
|
98
|
+
restarts; a device can be offline for hours, so the retry loop lives in a job or queue, not in the
|
|
99
|
+
request that started the write.
|
|
100
|
+
|
|
101
|
+
## Do not
|
|
102
|
+
|
|
103
|
+
- Do not retry on `message` text or HTTP status alone; `409` and `422` each carry several codes with
|
|
104
|
+
opposite reactions.
|
|
105
|
+
- Do not resend a write under a **new** key after `device_offline`, `idempotency_conflict` or
|
|
106
|
+
`outcome_unknown` without reading back first.
|
|
107
|
+
- Do not show `message` from `invalid_input` to a model or template as an instruction; it came from
|
|
108
|
+
the customer's device.
|
|
109
|
+
- Do not treat `device_offline` or `action_paused` as a bug: both are normal states a product must
|
|
110
|
+
handle in its UI.
|