@deepdockai/cli 1.0.23 → 1.0.26
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 +17 -0
- package/README.md +9 -9
- package/dist/deepdock.js +58 -15
- package/package.json +1 -1
- package/skill/SKILL.md +3 -2
- package/skill/references/calling-actions.md +49 -21
- package/skill/references/cli.md +2 -2
- package/skill/references/clients/deepdock.py +31 -22
- package/skill/references/clients/deepdock.ts +53 -28
- package/skill/references/concepts.md +4 -3
- package/skill/references/discovery.md +11 -12
- package/skill/references/errors.md +10 -4
- package/skill/references/example-integration.md +14 -13
- package/skill/references/idempotency.md +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.26
|
|
4
|
+
|
|
5
|
+
- Every action is called with `POST /v1/devices/{deviceId}/systems/{system}/actions/{action}` and its inputs in a flat JSON body, reads and updates included. There is no `GET` read and no `PATCH` update any more: any other method answers `405 invalid_request` with `Allow: POST`, and a query string on an action path answers `400 invalid_request` ("inputs go in the JSON body, not in the URL"), so no value reaches a URL, a request log or a proxy. A read takes `limit` (a JSON integer) and `cursor` (a string) in the same body; a write still needs an `Idempotency-Key`, a read ignores it. Values are typed as given: an integer input is a JSON number, and `"1001"` is refused where it is declared as one.
|
|
6
|
+
- The action index keeps its route, but each entry's `method` is replaced by `operation` (`read`, `create` or `update`). `actions list` derives its read/write column from it.
|
|
7
|
+
- The bundled TypeScript and Python clients send reads as `POST` with a JSON body, including `limit` and `cursor`. `write` always sends `POST`, so its `method` parameter (and `PendingWrite.method`) is gone; input values are typed (`string | number | boolean`) instead of text only. Idempotency handling is unchanged.
|
|
8
|
+
- The skill's references describe the new contract.
|
|
9
|
+
|
|
10
|
+
## 1.0.25
|
|
11
|
+
|
|
12
|
+
- Three new error codes for writes over a customer system's own HTTP interface: `applied_unverified` (409, exit 2: the write landed but its read-back did not finish; `error.created` names the entry as `{ chartId }`, or null), `credentials_rejected` and `certificate_changed` (503, exit 3: a person at the practice renews the login or confirms the certificate in the Deepdock client). An earlier CLI already exits with the same codes through its status fallback.
|
|
13
|
+
- `system_version_unverified` can carry `error.reason: unreported` with `error.version: null`, and a system's `writes` can read `unverified` with reason `unreported`: such a write is verified for one exact release, and the system reported none.
|
|
14
|
+
- `error.uncertain` is documented on `outcome_unknown`. The skill's error reference describes all of it.
|
|
15
|
+
|
|
16
|
+
## 1.0.24
|
|
17
|
+
|
|
18
|
+
- `devices list` and `incidents list` read every page of the API's paged answer (`limit` and `cursor`, following `nextCursor`), so no device or incident past the first 500 or 100 stays invisible. Output is unchanged. They stop after 200 pages with a warning on stderr. This needs the API that pages these lists; an older answer is refused with exit 3.
|
|
19
|
+
|
|
3
20
|
## 1.0.23
|
|
4
21
|
|
|
5
22
|
- An incident's program version is named `programVersion` instead of `version`, in `GET /v1/incidents`, in `incidents list --json` and in the skill's concepts. Read the new name where you read the old one; the value is unchanged.
|
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ deepdock skill
|
|
|
101
101
|
| `devices pair <code> [--label <text>]` | claim a device by the code on its screen |
|
|
102
102
|
| `devices rename <id> <label>`, `devices release <id>` | rename a device; unpair it from the organisation (admin) |
|
|
103
103
|
| `devices systems <id>` | a device's live systems and action state, paused reasons included |
|
|
104
|
-
| `actions list <device> <system>` | every action a device offers, with its
|
|
104
|
+
| `actions list <device> <system>` | every action a device offers, with its operation and fields |
|
|
105
105
|
| `keys list`, `keys create <name>`, `keys revoke <id>` | the organisation's API keys (create and revoke: admin) |
|
|
106
106
|
| `incidents list` | open incidents, and those closed within the last 30 days |
|
|
107
107
|
| `skill [--agent claude\|codex\|cursor\|all]` | install the agent skill into the current directory |
|
|
@@ -154,14 +154,14 @@ your code will receive; `auth`, `org select` and `keys create` build it from nam
|
|
|
154
154
|
failure is one document on stderr, `{ "error": { "code", "message" }, "meta": { "hint" } }`,
|
|
155
155
|
with the API's error object unchanged. Switch on `code` or on the exit code, never on `message`.
|
|
156
156
|
|
|
157
|
-
| Exit | Meaning
|
|
158
|
-
| ---- |
|
|
159
|
-
| 0 | done
|
|
160
|
-
| 1 | something on the command line to fix (`invalid_usage`, `org_required`)
|
|
161
|
-
| 2 | the API refused the request; do not repeat it unchanged (`invalid_request`, `not_found`, `forbidden`, `rate_limited` and the other refusals)
|
|
162
|
-
| 3 | retry later (`device_offline`, `device_syncing`, `device_outdated`, `system_version_unverified`, `action_paused`, `access_outdated`, `access_missing`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `outcome_unknown`, `connection_lost`) |
|
|
163
|
-
| 4 | the CLI is too old for the API (`client_outdated`): run `npx @deepdockai/cli@latest`
|
|
164
|
-
| 5 | sign-in needed (`sign_in_needed`, `approval_denied`, `approval_expired`); `auth status` when nobody is signed in
|
|
157
|
+
| Exit | Meaning |
|
|
158
|
+
| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
159
|
+
| 0 | done |
|
|
160
|
+
| 1 | something on the command line to fix (`invalid_usage`, `org_required`) |
|
|
161
|
+
| 2 | the API refused the request; do not repeat it unchanged (`invalid_request`, `not_found`, `forbidden`, `rate_limited` and the other refusals) |
|
|
162
|
+
| 3 | retry later (`device_offline`, `device_syncing`, `device_outdated`, `system_version_unverified`, `action_paused`, `access_outdated`, `access_missing`, `credentials_rejected`, `certificate_changed`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `outcome_unknown`, `connection_lost`) |
|
|
163
|
+
| 4 | the CLI is too old for the API (`client_outdated`): run `npx @deepdockai/cli@latest` |
|
|
164
|
+
| 5 | sign-in needed (`sign_in_needed`, `approval_denied`, `approval_expired`); `auth status` when nobody is signed in |
|
|
165
165
|
|
|
166
166
|
The CLI never retries on its own: exit 3 says retry, you decide when. `deepdock commands --json`
|
|
167
167
|
prints the whole command tree.
|
package/dist/deepdock.js
CHANGED
|
@@ -10866,6 +10866,7 @@ var EXIT_CODES = {
|
|
|
10866
10866
|
rate_limited: 2,
|
|
10867
10867
|
target_rejected: 2,
|
|
10868
10868
|
readback_failed: 2,
|
|
10869
|
+
applied_unverified: 2,
|
|
10869
10870
|
idempotency_conflict: 2,
|
|
10870
10871
|
conflict: 2,
|
|
10871
10872
|
already_member: 2,
|
|
@@ -10880,6 +10881,8 @@ var EXIT_CODES = {
|
|
|
10880
10881
|
action_paused: 3,
|
|
10881
10882
|
access_outdated: 3,
|
|
10882
10883
|
access_missing: 3,
|
|
10884
|
+
credentials_rejected: 3,
|
|
10885
|
+
certificate_changed: 3,
|
|
10883
10886
|
system_unavailable: 3,
|
|
10884
10887
|
target_busy: 3,
|
|
10885
10888
|
temporarily_unavailable: 3,
|
|
@@ -11233,13 +11236,20 @@ var summary = (description) => {
|
|
|
11233
11236
|
return chars.length > 160 ? `${chars.slice(0, 159).join("").trimEnd()}…` : sentence;
|
|
11234
11237
|
};
|
|
11235
11238
|
var actionRow = (action) => {
|
|
11236
|
-
const
|
|
11239
|
+
const operation = typeof action.operation === "string" ? action.operation : undefined;
|
|
11237
11240
|
const fields = fieldNames(action.input);
|
|
11238
|
-
const {
|
|
11241
|
+
const {
|
|
11242
|
+
name,
|
|
11243
|
+
operation: _operation,
|
|
11244
|
+
input: _input,
|
|
11245
|
+
output: _output,
|
|
11246
|
+
description,
|
|
11247
|
+
...rest
|
|
11248
|
+
} = action;
|
|
11239
11249
|
const brief = summary(description);
|
|
11240
11250
|
return {
|
|
11241
11251
|
...name === undefined ? {} : { name },
|
|
11242
|
-
...
|
|
11252
|
+
...operation ? { kind: operation === "read" ? "read" : "write" } : {},
|
|
11243
11253
|
...fields ? { fields } : {},
|
|
11244
11254
|
...brief ? { description: brief } : {},
|
|
11245
11255
|
...rest
|
|
@@ -11429,7 +11439,7 @@ import { spawn } from "node:child_process";
|
|
|
11429
11439
|
import { homedir, hostname } from "node:os";
|
|
11430
11440
|
|
|
11431
11441
|
// src/version.ts
|
|
11432
|
-
var VERSION = "1.0.
|
|
11442
|
+
var VERSION = "1.0.26";
|
|
11433
11443
|
|
|
11434
11444
|
// src/runtime.ts
|
|
11435
11445
|
var DEFAULT_BASE_URL = "https://api.deepdock.ai";
|
|
@@ -12160,6 +12170,7 @@ var EXIT_CODES_HELP = `Exit codes
|
|
|
12160
12170
|
2 the API refused the request; do not repeat it unchanged
|
|
12161
12171
|
3 retry later (device_offline, device_syncing, device_outdated,
|
|
12162
12172
|
system_version_unverified, action_paused, access_outdated, access_missing,
|
|
12173
|
+
credentials_rejected, certificate_changed,
|
|
12163
12174
|
system_unavailable, target_busy, temporarily_unavailable, internal_error,
|
|
12164
12175
|
outcome_unknown, connection_lost)
|
|
12165
12176
|
4 the CLI is too old for the API (client_outdated)
|
|
@@ -12828,6 +12839,40 @@ var registerAuth = (program, run) => {
|
|
|
12828
12839
|
auth.command("logout").description("Revoke the sign-in and the API keys this CLI created").option("--keep-keys", "revoke only the sign-in and leave the API keys active").action((options, cmd) => run(cmd, (ctx) => logout(ctx, options)));
|
|
12829
12840
|
};
|
|
12830
12841
|
|
|
12842
|
+
// src/commands/pages.ts
|
|
12843
|
+
var PAGE_LIMIT = 500;
|
|
12844
|
+
var MAX_PAGES = 200;
|
|
12845
|
+
var unreadable = () => new CliError({
|
|
12846
|
+
code: "connection_lost",
|
|
12847
|
+
message: "The API answered a list in a shape this CLI does not read.",
|
|
12848
|
+
hint: "Update the CLI with `npm install -g @deepdockai/cli@latest`, then retry."
|
|
12849
|
+
});
|
|
12850
|
+
var readAllPages = async (ctx, path, org) => {
|
|
12851
|
+
const items = [];
|
|
12852
|
+
let cursor = null;
|
|
12853
|
+
let requestId;
|
|
12854
|
+
for (let page = 1;page <= MAX_PAGES; page++) {
|
|
12855
|
+
const query = new URLSearchParams({ limit: String(PAGE_LIMIT) });
|
|
12856
|
+
if (cursor !== null)
|
|
12857
|
+
query.set("cursor", cursor);
|
|
12858
|
+
const url = `${path}?${query.toString()}`;
|
|
12859
|
+
const response = org ? await ctx.call({ method: "GET", path: url, org }) : await ctx.callInOrg({ method: "GET", path: url });
|
|
12860
|
+
const data = response.data;
|
|
12861
|
+
if (!isRecord(data) || !Array.isArray(data.items))
|
|
12862
|
+
throw unreadable();
|
|
12863
|
+
const next = data.nextCursor;
|
|
12864
|
+
if (next !== null && (typeof next !== "string" || next === ""))
|
|
12865
|
+
throw unreadable();
|
|
12866
|
+
items.push(...data.items);
|
|
12867
|
+
requestId ??= response.requestId;
|
|
12868
|
+
if (next === null)
|
|
12869
|
+
return { items, requestId };
|
|
12870
|
+
cursor = next;
|
|
12871
|
+
}
|
|
12872
|
+
ctx.note(`Warning: stopped after ${MAX_PAGES} pages; the list is longer than the ${items.length} rows shown.`);
|
|
12873
|
+
return { items, requestId };
|
|
12874
|
+
};
|
|
12875
|
+
|
|
12831
12876
|
// src/commands/devices.ts
|
|
12832
12877
|
var meta = (requestId) => requestId ? { requestId } : {};
|
|
12833
12878
|
var updateNotes = (ctx, devices) => {
|
|
@@ -12840,19 +12885,18 @@ var updateNotes = (ctx, devices) => {
|
|
|
12840
12885
|
}
|
|
12841
12886
|
};
|
|
12842
12887
|
var list = async (ctx) => {
|
|
12843
|
-
const
|
|
12844
|
-
const devices = Array.isArray(response.data) ? response.data : [];
|
|
12888
|
+
const { items: devices, requestId } = await readAllPages(ctx, "/partner/devices");
|
|
12845
12889
|
if (devices.length === 0) {
|
|
12846
12890
|
return {
|
|
12847
|
-
data:
|
|
12848
|
-
meta: meta(
|
|
12891
|
+
data: devices,
|
|
12892
|
+
meta: meta(requestId),
|
|
12849
12893
|
text: "No devices are paired. Pair one with `deepdock devices pair <code>`."
|
|
12850
12894
|
};
|
|
12851
12895
|
}
|
|
12852
12896
|
updateNotes(ctx, devices);
|
|
12853
12897
|
return {
|
|
12854
|
-
data:
|
|
12855
|
-
meta: meta(
|
|
12898
|
+
data: devices,
|
|
12899
|
+
meta: meta(requestId),
|
|
12856
12900
|
text: deviceTable(devices, ctx.rt.now(), ctx.out)
|
|
12857
12901
|
};
|
|
12858
12902
|
};
|
|
@@ -12906,8 +12950,8 @@ var release = async (ctx, id) => {
|
|
|
12906
12950
|
if (!ctx.opts.yes) {
|
|
12907
12951
|
let label = "";
|
|
12908
12952
|
try {
|
|
12909
|
-
const
|
|
12910
|
-
const found =
|
|
12953
|
+
const { items } = await readAllPages(ctx, "/partner/devices", org.slug);
|
|
12954
|
+
const found = items.filter(isRecord).find((device) => field(device, "id") === deviceId);
|
|
12911
12955
|
label = found ? field(found, "label") : "";
|
|
12912
12956
|
} catch {}
|
|
12913
12957
|
const named = label ? ` (${stripControl(label)})` : "";
|
|
@@ -12966,10 +13010,9 @@ var registerDevices = (program, run) => {
|
|
|
12966
13010
|
var registerIncidents = (program, run) => {
|
|
12967
13011
|
const incidents = program.command("incidents").description("Incidents that paused an action on one of your devices");
|
|
12968
13012
|
incidents.command("list").description("List open incidents and those closed within 30 days").action((_options, cmd) => run(cmd, async (ctx) => {
|
|
12969
|
-
const
|
|
12970
|
-
const items = Array.isArray(response.data) ? response.data : [];
|
|
13013
|
+
const { items, requestId } = await readAllPages(ctx, "/partner/incidents");
|
|
12971
13014
|
const text = items.length === 0 ? "No incidents." : genericTable(items, ctx.out, INCIDENT_COLUMNS);
|
|
12972
|
-
return { data:
|
|
13015
|
+
return { data: items, meta: { requestId }, text };
|
|
12973
13016
|
}));
|
|
12974
13017
|
};
|
|
12975
13018
|
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -22,8 +22,9 @@ integration against a demo device. It is not for building or changing actions: D
|
|
|
22
22
|
screen, and from then on it belongs to your organisation until you release it.
|
|
23
23
|
3. A device reports the **systems** it runs; each system has **actions**, deterministic operations
|
|
24
24
|
that Deepdock builds and verifies before offering them.
|
|
25
|
-
4.
|
|
26
|
-
action
|
|
25
|
+
4. Every action is a `POST` with a flat JSON body, never a value in the URL. A **read** (the
|
|
26
|
+
action's `operation`) also takes `limit` and `cursor` in that body to page; a **write** (`create`
|
|
27
|
+
or `update`) needs an `Idempotency-Key` as well.
|
|
27
28
|
5. Every write needs an `Idempotency-Key`: a new key per intended write, the same key on every retry,
|
|
28
29
|
and a repeat answers the stored result with `Idempotent-Replayed: true`, writing nothing twice.
|
|
29
30
|
6. A write either fully applies or not: Deepdock checks its result before confirming, and a write
|
|
@@ -3,16 +3,35 @@
|
|
|
3
3
|
All paths are under `/v1/devices/{deviceId}/systems/{system}/actions/{action}`. `deviceId` is the
|
|
4
4
|
UUID from pairing; `system` and `action` are the slugs from the action index.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
composed and a decomposed spelling are the same value.
|
|
6
|
+
## Calling an action: POST
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
Every action, whether it reads, creates or updates, is called with `POST` and a flat JSON object as
|
|
9
|
+
the body. What the action does is its `operation` (`read`, `create` or `update`) in the action
|
|
10
|
+
index and the OpenAPI document; the HTTP method never says. Any other method answers
|
|
11
|
+
`405 invalid_request` with `Allow: POST`.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Inputs are typed as the action's schema declares them and are never sent as text when they are not:
|
|
14
|
+
an integer is a JSON number (`"patientId": 4711`, not `"4711"`; the text form is refused with
|
|
15
|
+
`400 invalid_request`). Text is normalised to Unicode NFC before anything else happens; a composed
|
|
16
|
+
and a decomposed spelling are the same value.
|
|
17
|
+
|
|
18
|
+
**Never put an input in the URL.** A value in a URL reaches request logs, proxies and browser
|
|
19
|
+
history, which is where a patient's name or id must not end up. Any query string on an action
|
|
20
|
+
path, whatever it holds, answers `400 invalid_request` ("inputs go in the JSON body, not in the
|
|
21
|
+
URL") without repeating a name or a value. Because the body is JSON, a `+` or a non-ASCII letter
|
|
22
|
+
needs no escaping.
|
|
23
|
+
|
|
24
|
+
### Reads
|
|
25
|
+
|
|
26
|
+
A read takes its inputs, and `limit` (a JSON integer from 1 to 250, default 25) and `cursor` (a
|
|
27
|
+
string) for paging, in the same body. No `Idempotency-Key`: a repeated read changes nothing, and the
|
|
28
|
+
header is ignored.
|
|
12
29
|
|
|
13
30
|
```bash
|
|
14
|
-
curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchCustomers
|
|
15
|
-
-H "Authorization: Bearer $DEEPDOCK_API_KEY"
|
|
31
|
+
curl -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchCustomers" \
|
|
32
|
+
-H "Authorization: Bearer $DEEPDOCK_API_KEY" \
|
|
33
|
+
-H "Content-Type: application/json" \
|
|
34
|
+
-d '{"lastName":"Müller","limit":50}'
|
|
16
35
|
```
|
|
17
36
|
|
|
18
37
|
```json
|
|
@@ -30,11 +49,10 @@ curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchCustomers?lastN
|
|
|
30
49
|
punctuation, umlaut spelling). Design your search UI around that rule rather than filtering
|
|
31
50
|
client-side.
|
|
32
51
|
|
|
33
|
-
|
|
52
|
+
### Writes (create and update)
|
|
34
53
|
|
|
35
|
-
Body: a JSON object
|
|
36
|
-
|
|
37
|
-
`Idempotency-Key`, required.
|
|
54
|
+
Body: a flat JSON object with only the fields the schema lists (`additionalProperties: false`) and
|
|
55
|
+
all of its `required` ones, each typed as the schema declares. Header `Idempotency-Key`, required.
|
|
38
56
|
|
|
39
57
|
```bash
|
|
40
58
|
curl -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/createNote" \
|
|
@@ -46,11 +64,11 @@ curl -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/createNote" \
|
|
|
46
64
|
|
|
47
65
|
- A create answers `201` with `{ data: { <returned fields> } }`, typically the new record's id. A
|
|
48
66
|
replay of the same key answers `200` with the same data and `Idempotent-Replayed: true`.
|
|
49
|
-
- An update answers `200` whether it ran now or is a replay; the header tells
|
|
67
|
+
- An update is a `POST` too and answers `200` whether it ran now or is a replay; the header tells
|
|
68
|
+
you which.
|
|
50
69
|
- The returned id is the system's own id (for example `noteId`). Store it as the system gave it;
|
|
51
|
-
do not predict or re-key it. Result field types come from each action's own output schema:
|
|
52
|
-
|
|
53
|
-
string. Do not reuse one action's result type for another.
|
|
70
|
+
do not predict or re-key it. Result field types come from each action's own output schema: do not
|
|
71
|
+
reuse one action's result type for another.
|
|
54
72
|
- Each update under a new key writes again and may add an entry to the system's own change log,
|
|
55
73
|
even when values are unchanged. Do not "update" to no-op.
|
|
56
74
|
|
|
@@ -63,7 +81,7 @@ of a customer, the note id of an update. The pattern is always the same:
|
|
|
63
81
|
|
|
64
82
|
1. Read the reference with the matching read action (for example a users read, a case read, a
|
|
65
83
|
customer search).
|
|
66
|
-
2. Pass its id back
|
|
84
|
+
2. Pass its id back in the type the write's schema declares, usually the integer the read returned.
|
|
67
85
|
3. Handle the action-specific refusal the description names: for example `422 unknown_author` when
|
|
68
86
|
the user is not one the system lists, or `422 no_open_case` when the customer has no open case and
|
|
69
87
|
the action does not open one. Both mean nothing was written; fix the reference, do not retry
|
|
@@ -73,13 +91,23 @@ Never invent an id, copy one from another device, or hard-code one from a test r
|
|
|
73
91
|
|
|
74
92
|
## Easy to get wrong
|
|
75
93
|
|
|
76
|
-
- **
|
|
77
|
-
|
|
78
|
-
`customerId: 1001
|
|
79
|
-
|
|
94
|
+
- **Send each input as the type its schema declares.** An id the schema declares as an integer goes
|
|
95
|
+
in as a JSON number (`"customerId": 1001`), one it declares as text goes in as text
|
|
96
|
+
(`"customerId": "1001"`: digits only, no leading zero, at most 10 characters). A value of the
|
|
97
|
+
wrong type is refused with `400 invalid_request`, never converted. Result field types also come
|
|
98
|
+
from the action's own output schema.
|
|
80
99
|
- **Mark every test text `TEST-DEEPDOCK-<id>`** on the demo device only, and list what you created
|
|
81
100
|
([testing.md](testing.md)).
|
|
82
|
-
- A
|
|
101
|
+
- A call with a query string, or with any method but `POST`, is refused (`400` and `405`), so a
|
|
102
|
+
client library that puts parameters in the URL for you needs its body mode.
|
|
103
|
+
|
|
104
|
+
## Refusals specific to the call shape
|
|
105
|
+
|
|
106
|
+
| Answer | Cause | Fix |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `405 invalid_request`, `Allow: POST` | any method but `POST` on an action path | call it with `POST` |
|
|
109
|
+
| `400 invalid_request` | a query string on an action path | move every value into the JSON body |
|
|
110
|
+
| `400 invalid_request` | an input of the wrong type, such as `"4711"` where an integer is declared | send the type the schema declares |
|
|
83
111
|
|
|
84
112
|
## Timeouts and latency
|
|
85
113
|
|
|
@@ -110,7 +138,7 @@ attempts per key per rolling 60 seconds, whose `429` carries neither the header
|
|
|
110
138
|
| Header | When | Meaning |
|
|
111
139
|
|---|---|---|
|
|
112
140
|
| `Idempotent-Replayed: true` | a write's `200` | the stored result of an earlier run; nothing written again |
|
|
113
|
-
| `Allow` | `405 invalid_request` |
|
|
141
|
+
| `Allow` | `405 invalid_request` | always `POST`: every action is called with it |
|
|
114
142
|
| `WWW-Authenticate: Bearer` | `401` | the key was refused |
|
|
115
143
|
| `Retry-After` | `429` | seconds to wait, when present |
|
|
116
144
|
|
package/skill/references/cli.md
CHANGED
|
@@ -24,7 +24,7 @@ the API key the CLI wrote to the app's env file.
|
|
|
24
24
|
| `devices rename <id> <label>` | rename a device | member |
|
|
25
25
|
| `devices release <id>` | release a device (asks on a terminal; `--yes` otherwise) | admin |
|
|
26
26
|
| `devices systems <id>` | the device's live systems and action state, paused reasons included; `device_offline` is a normal answer | member |
|
|
27
|
-
| `actions list <device> <system>` | the action index: every action with its
|
|
27
|
+
| `actions list <device> <system>` | the action index: every action with its operation and field schemas | member |
|
|
28
28
|
| `keys list` | the organisation's API keys: name, prefix, created, last used, expires, status | member |
|
|
29
29
|
| `keys create <name> --expires <7d\|30d\|90d> --write-env <file>` | create an API key: opens a link, a human approves "Create an API key in <Org>?", the CLI receives the key once and writes `DEEPDOCK_API_KEY=<key>` into the file. It keeps no copy and shows the value to nobody. Answers id, name, prefix, expiry and where it was written | admin |
|
|
30
30
|
| `keys revoke <id>` | revoke a key (asks on a terminal; `--yes` otherwise); removes the env entry if this CLI wrote it | admin |
|
|
@@ -84,7 +84,7 @@ A failure is one document on stderr: the API's error object unchanged plus what
|
|
|
84
84
|
| 0 | done | |
|
|
85
85
|
| 1 | something on the command line to fix | `invalid_usage` (a bad flag, an unknown slug, a question with no terminal and no `--yes`, an env file git does not ignore), `org_required` |
|
|
86
86
|
| 2 | the API refused the request; do not repeat it unchanged | `invalid_request`, `not_found`, `device_not_found`, `pairing_code_invalid`, `other_partner_build`, `rate_limited`, `forbidden`, `conflict` and the other refusals (not `access_outdated` and `access_missing`, see exit 3) |
|
|
87
|
-
| 3 | retry later (`access_outdated` and `access_missing`: after the customer repeats "Erneut einrichten" in the Deepdock client) | `device_offline`, `device_syncing`, `device_outdated`, `system_version_unverified`, `action_paused`, `access_outdated`, `access_missing`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `outcome_unknown`, `connection_lost` |
|
|
87
|
+
| 3 | retry later (`access_outdated` and `access_missing`: after the customer repeats "Erneut einrichten" in the Deepdock client) | `device_offline`, `device_syncing`, `device_outdated`, `system_version_unverified`, `action_paused`, `access_outdated`, `access_missing`, `credentials_rejected`, `certificate_changed`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `outcome_unknown`, `connection_lost` |
|
|
88
88
|
| 4 | the CLI is too old for the API: run `npx @deepdockai/cli@latest` | `client_outdated` |
|
|
89
89
|
| 5 | sign-in needed: run `deepdock auth login` | `sign_in_needed`, `approval_denied`, `approval_expired` |
|
|
90
90
|
|
|
@@ -2,32 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
It does the four things the OpenAPI document cannot tell you: the bearer header, the envelope
|
|
4
4
|
unwrap, the Idempotency-Key header with replay detection, and a typed error carrying the code.
|
|
5
|
-
Everything action-specific (
|
|
5
|
+
Everything action-specific (fields, types, patterns) comes from the live OpenAPI document.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
11
|
from dataclasses import dataclass, field, replace
|
|
12
|
-
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
|
12
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
|
13
13
|
from urllib.parse import quote
|
|
14
14
|
|
|
15
15
|
import httpx
|
|
16
16
|
|
|
17
|
+
# A flat JSON value as the action's schema declares it: a number is a number, never its text.
|
|
18
|
+
InputValue = Union[str, int, float, bool]
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class DeepdockError(Exception):
|
|
19
22
|
"""Deepdock answered `{ "error": { "code", ... } }`. Switch on `code`, never on `message`."""
|
|
20
23
|
|
|
21
24
|
def __init__(self, status: int, code: str, message: str, request_id: Optional[str] = None,
|
|
22
25
|
systems: Optional[List[str]] = None, version: Optional[str] = None,
|
|
23
|
-
reason: Optional[str] = None
|
|
26
|
+
reason: Optional[str] = None, created: Optional[Dict[str, Any]] = None,
|
|
27
|
+
uncertain: Optional[bool] = None):
|
|
24
28
|
super().__init__(message)
|
|
25
29
|
self.status = status
|
|
26
30
|
self.code = code
|
|
27
31
|
self.request_id = request_id
|
|
28
32
|
self.systems = systems
|
|
29
|
-
self.version = version # system_version_unverified: the version the system last reported
|
|
30
|
-
self.reason = reason # system_version_unverified: unverified_version or
|
|
33
|
+
self.version = version # system_version_unverified: the version the system last reported, None when it reported none
|
|
34
|
+
self.reason = reason # system_version_unverified: unverified_version, unparseable or unreported
|
|
35
|
+
self.created = created # applied_unverified, outcome_unknown with no rollback: {"chartId"} or None
|
|
36
|
+
self.uncertain = uncertain # outcome_unknown: True when the write may or may not have landed
|
|
31
37
|
|
|
32
38
|
|
|
33
39
|
class DeepdockTransportError(Exception):
|
|
@@ -67,7 +73,7 @@ class Deepdock:
|
|
|
67
73
|
if isinstance(body, dict) and "error" in body:
|
|
68
74
|
e = body["error"]
|
|
69
75
|
raise DeepdockError(r.status_code, e["code"], e["message"], e.get("requestId"), e.get("systems"),
|
|
70
|
-
e.get("version"), e.get("reason"))
|
|
76
|
+
e.get("version"), e.get("reason"), e.get("created"), e.get("uncertain"))
|
|
71
77
|
if not isinstance(body, dict) or "data" not in body:
|
|
72
78
|
raise DeepdockTransportError(f"unexpected body with status {r.status_code}")
|
|
73
79
|
return body["data"], r.headers
|
|
@@ -76,18 +82,19 @@ class Deepdock:
|
|
|
76
82
|
def _action_path(device_id: str, system: str, action: str) -> str:
|
|
77
83
|
return f"/v1/devices/{quote(device_id, safe='')}/systems/{quote(system, safe='')}/actions/{quote(action, safe='')}"
|
|
78
84
|
|
|
79
|
-
def read(self, device_id: str, system: str, action: str, params: Dict[str,
|
|
85
|
+
def read(self, device_id: str, system: str, action: str, params: Dict[str, InputValue],
|
|
80
86
|
limit: Optional[int] = None, cursor: Optional[str] = None) -> Page:
|
|
81
|
-
"""Runs a read action
|
|
82
|
-
|
|
87
|
+
"""Runs a read action: a POST whose JSON body holds the inputs and the paging fields, no
|
|
88
|
+
Idempotency-Key. Pass `next_cursor` back as `cursor` for the next page."""
|
|
89
|
+
body: Dict[str, InputValue] = dict(params)
|
|
83
90
|
if limit is not None:
|
|
84
|
-
|
|
91
|
+
body["limit"] = limit
|
|
85
92
|
if cursor is not None:
|
|
86
|
-
|
|
87
|
-
data, _ = self._send("
|
|
93
|
+
body["cursor"] = cursor
|
|
94
|
+
data, _ = self._send("POST", self._action_path(device_id, system, action), json=body)
|
|
88
95
|
return Page(items=data["items"], next_cursor=data.get("nextCursor"))
|
|
89
96
|
|
|
90
|
-
def read_all(self, device_id: str, system: str, action: str, params: Dict[str,
|
|
97
|
+
def read_all(self, device_id: str, system: str, action: str, params: Dict[str, InputValue],
|
|
91
98
|
limit: Optional[int] = None) -> Iterator[Dict[str, Any]]:
|
|
92
99
|
"""Reads every page of a read action, in order."""
|
|
93
100
|
cursor: Optional[str] = None
|
|
@@ -98,10 +105,10 @@ class Deepdock:
|
|
|
98
105
|
return
|
|
99
106
|
cursor = page.next_cursor
|
|
100
107
|
|
|
101
|
-
def write(self, device_id: str, system: str, action: str,
|
|
102
|
-
body: Dict[str,
|
|
103
|
-
"""Runs a write action
|
|
104
|
-
data, headers = self._send(
|
|
108
|
+
def write(self, device_id: str, system: str, action: str, idempotency_key: str,
|
|
109
|
+
body: Dict[str, InputValue]) -> WriteResult:
|
|
110
|
+
"""Runs a write action (a create or an update) with POST; `body` values are typed as the action's schema declares."""
|
|
111
|
+
data, headers = self._send("POST", self._action_path(device_id, system, action), json=body,
|
|
105
112
|
headers={"Idempotency-Key": idempotency_key})
|
|
106
113
|
return WriteResult(data=data, replayed=headers.get("Idempotent-Replayed") == "true")
|
|
107
114
|
|
|
@@ -143,8 +150,7 @@ class PendingWrite:
|
|
|
143
150
|
device_id: str
|
|
144
151
|
system: str
|
|
145
152
|
action: str
|
|
146
|
-
|
|
147
|
-
input: Dict[str, str]
|
|
153
|
+
input: Dict[str, InputValue]
|
|
148
154
|
status: str = "pending" # pending | sent | done | unknown | refused
|
|
149
155
|
conflict_retries: int = 0
|
|
150
156
|
result: Optional[Dict[str, Any]] = field(default=None)
|
|
@@ -155,7 +161,7 @@ class PendingWrite:
|
|
|
155
161
|
def attempt(dd: Deepdock, row: PendingWrite) -> PendingWrite:
|
|
156
162
|
"""One attempt; returns the row with its new status. The caller persists it."""
|
|
157
163
|
try:
|
|
158
|
-
res = dd.write(row.device_id, row.system, row.action, row.
|
|
164
|
+
res = dd.write(row.device_id, row.system, row.action, row.key, row.input)
|
|
159
165
|
return replace(row, status="done", result=res.data)
|
|
160
166
|
except DeepdockTransportError:
|
|
161
167
|
return replace(row, status="sent")
|
|
@@ -166,8 +172,11 @@ def attempt(dd: Deepdock, row: PendingWrite) -> PendingWrite:
|
|
|
166
172
|
if row.conflict_retries < CONFLICT_RETRIES:
|
|
167
173
|
return replace(row, status="sent", conflict_retries=row.conflict_retries + 1)
|
|
168
174
|
return replace(row, status="unknown")
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
# applied_unverified: the entry exists but was not verified; never write it again under a new
|
|
176
|
+
# key. result["created"] names it for the check.
|
|
177
|
+
if e.code in ("outcome_unknown", "applied_unverified"):
|
|
178
|
+
return replace(row, status="unknown", request_id=e.request_id,
|
|
179
|
+
result={"created": e.created, "uncertain": e.uncertain})
|
|
171
180
|
# invalid_input, target_rejected, action-specific 422s, not_found, readback_failed, auth and
|
|
172
181
|
# resolution codes: nothing was written. Fix the cause; a changed input gets a new key.
|
|
173
182
|
return replace(row, status="refused", refused_code=e.code, result={"message": str(e)})
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* It does the four things the OpenAPI document cannot tell you: the bearer header, the envelope
|
|
5
5
|
* unwrap, the Idempotency-Key header with replay detection, and a typed error carrying the code.
|
|
6
|
-
* Everything action-specific (
|
|
6
|
+
* Everything action-specific (fields, types, patterns) comes from the live OpenAPI document.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// Plain fields, no constructor parameter properties: Node runs this file with its default type
|
|
@@ -13,15 +13,21 @@ export class DeepdockError extends Error {
|
|
|
13
13
|
readonly code: string
|
|
14
14
|
readonly requestId?: string
|
|
15
15
|
readonly systems?: string[]
|
|
16
|
-
/** system_version_unverified: the version the system last reported. */
|
|
17
|
-
readonly version?: string
|
|
18
|
-
/** system_version_unverified: `unverified_version` or `
|
|
16
|
+
/** system_version_unverified: the version the system last reported, `null` when it reported none. */
|
|
17
|
+
readonly version?: string | null
|
|
18
|
+
/** system_version_unverified: `unverified_version`, `unparseable` or `unreported`. */
|
|
19
19
|
readonly reason?: string
|
|
20
|
+
/** applied_unverified, outcome_unknown on a system with no rollback: the entry, or `null` when not known. */
|
|
21
|
+
readonly created?: { chartId?: string } | null
|
|
22
|
+
/** outcome_unknown: `true` when the write may or may not have landed. */
|
|
23
|
+
readonly uncertain?: boolean
|
|
20
24
|
|
|
21
25
|
constructor(status: number, code: string, message: string, requestId?: string,
|
|
22
26
|
systems?: string[],
|
|
23
|
-
version?: string,
|
|
27
|
+
version?: string | null,
|
|
24
28
|
reason?: string,
|
|
29
|
+
created?: { chartId?: string } | null,
|
|
30
|
+
uncertain?: boolean,
|
|
25
31
|
) {
|
|
26
32
|
super(message)
|
|
27
33
|
this.name = 'DeepdockError'
|
|
@@ -31,6 +37,8 @@ export class DeepdockError extends Error {
|
|
|
31
37
|
this.systems = systems
|
|
32
38
|
this.version = version
|
|
33
39
|
this.reason = reason
|
|
40
|
+
this.created = created
|
|
41
|
+
this.uncertain = uncertain
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
44
|
|
|
@@ -42,12 +50,14 @@ export class DeepdockTransportError extends Error {
|
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
type ErrorBody = { code: string; message: string; requestId?: string; systems?: string[]; version?: string; reason?: string }
|
|
53
|
+
type ErrorBody = { code: string; message: string; requestId?: string; systems?: string[]; version?: string | null; reason?: string; created?: { chartId?: string } | null; uncertain?: boolean }
|
|
46
54
|
type Envelope<T> = { data: T } | { error: ErrorBody }
|
|
47
55
|
|
|
48
56
|
export type Page<Item> = { items: Item[]; nextCursor: string | null }
|
|
49
57
|
export type WriteResult<T> = { data: T; replayed: boolean }
|
|
50
|
-
|
|
58
|
+
/** A flat JSON value as the action's schema declares it: a number is a number, never its text. */
|
|
59
|
+
export type InputValue = string | number | boolean
|
|
60
|
+
export type ActionInput = Record<string, InputValue>
|
|
51
61
|
|
|
52
62
|
export type SystemStatus = 'running' | 'stopped' | 'missing' | 'unknown'
|
|
53
63
|
/** The database access the device last reported; `null` while none is known. */
|
|
@@ -56,11 +66,12 @@ export type SystemAccess = 'ok' | 'outdated' | 'missing' | 'password_missing' |
|
|
|
56
66
|
/**
|
|
57
67
|
* Whether writes on the system run. `unverified` makes writes answer system_version_unverified:
|
|
58
68
|
* `unverified_version` clears once Deepdock verifies the version; `unparseable` does not clear by
|
|
59
|
-
* waiting (contact Deepdock)
|
|
69
|
+
* waiting (contact Deepdock); `unreported` clears once the device reports the system's version (a
|
|
70
|
+
* write verified for one exact release needs it).
|
|
60
71
|
*/
|
|
61
72
|
export type SystemWrites =
|
|
62
73
|
| { state: 'verified' | 'unknown' | 'partial' }
|
|
63
|
-
| { state: 'unverified'; reason: 'unverified_version' | 'unparseable' }
|
|
74
|
+
| { state: 'unverified'; reason: 'unverified_version' | 'unparseable' | 'unreported' }
|
|
64
75
|
|
|
65
76
|
export type Device = {
|
|
66
77
|
id: string
|
|
@@ -165,8 +176,8 @@ export class Deepdock {
|
|
|
165
176
|
throw new DeepdockTransportError(`non-JSON answer with status ${res.status}`)
|
|
166
177
|
}
|
|
167
178
|
if (body !== null && typeof body === 'object' && 'error' in body) {
|
|
168
|
-
const { code, message, requestId, systems, version, reason } = body.error
|
|
169
|
-
throw new DeepdockError(res.status, code, message, requestId, systems, version, reason)
|
|
179
|
+
const { code, message, requestId, systems, version, reason, created, uncertain } = body.error
|
|
180
|
+
throw new DeepdockError(res.status, code, message, requestId, systems, version, reason, created, uncertain)
|
|
170
181
|
}
|
|
171
182
|
if (body === null || typeof body !== 'object' || !('data' in body)) {
|
|
172
183
|
throw new DeepdockTransportError(`unexpected body with status ${res.status}`)
|
|
@@ -178,19 +189,27 @@ export class Deepdock {
|
|
|
178
189
|
return `/v1/devices/${encodeURIComponent(deviceId)}/systems/${encodeURIComponent(system)}/actions/${encodeURIComponent(action)}`
|
|
179
190
|
}
|
|
180
191
|
|
|
181
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* Runs a read action: a POST whose JSON body holds the inputs and the paging fields, no
|
|
194
|
+
* Idempotency-Key. Pass `nextCursor` back as `cursor` for the next page.
|
|
195
|
+
*/
|
|
182
196
|
async read<Item>(
|
|
183
197
|
deviceId: string,
|
|
184
198
|
system: string,
|
|
185
199
|
action: string,
|
|
186
|
-
input:
|
|
200
|
+
input: ActionInput,
|
|
187
201
|
page: { limit?: number; cursor?: string } = {},
|
|
188
202
|
): Promise<Page<Item>> {
|
|
189
|
-
const
|
|
190
|
-
if (page.limit !== undefined)
|
|
191
|
-
if (page.cursor !== undefined)
|
|
192
|
-
|
|
193
|
-
|
|
203
|
+
const body: ActionInput = { ...input }
|
|
204
|
+
if (page.limit !== undefined) body.limit = page.limit
|
|
205
|
+
if (page.cursor !== undefined) body.cursor = page.cursor
|
|
206
|
+
return (
|
|
207
|
+
await this.send<Page<Item>>(this.actionPath(deviceId, system, action), {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify(body),
|
|
211
|
+
})
|
|
212
|
+
).data
|
|
194
213
|
}
|
|
195
214
|
|
|
196
215
|
/** Reads every page of a read action, in order. */
|
|
@@ -198,7 +217,7 @@ export class Deepdock {
|
|
|
198
217
|
deviceId: string,
|
|
199
218
|
system: string,
|
|
200
219
|
action: string,
|
|
201
|
-
input:
|
|
220
|
+
input: ActionInput,
|
|
202
221
|
limit?: number,
|
|
203
222
|
): AsyncGenerator<Item> {
|
|
204
223
|
let cursor: string | undefined
|
|
@@ -210,19 +229,18 @@ export class Deepdock {
|
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
/**
|
|
213
|
-
* Runs a write action. `
|
|
214
|
-
*
|
|
232
|
+
* Runs a write action (a create or an update) with POST. `input` values are typed as the action's
|
|
233
|
+
* schema declares. `idempotencyKey` is new per intended write and the same on every retry.
|
|
215
234
|
*/
|
|
216
235
|
async write<Result>(
|
|
217
236
|
deviceId: string,
|
|
218
237
|
system: string,
|
|
219
238
|
action: string,
|
|
220
|
-
method: WriteMethod,
|
|
221
239
|
idempotencyKey: string,
|
|
222
|
-
input:
|
|
240
|
+
input: ActionInput,
|
|
223
241
|
): Promise<WriteResult<Result>> {
|
|
224
242
|
const { data, headers } = await this.send<Result>(this.actionPath(deviceId, system, action), {
|
|
225
|
-
method,
|
|
243
|
+
method: 'POST',
|
|
226
244
|
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
|
|
227
245
|
body: JSON.stringify(input),
|
|
228
246
|
})
|
|
@@ -267,8 +285,7 @@ export type PendingWrite = {
|
|
|
267
285
|
deviceId: string
|
|
268
286
|
system: string
|
|
269
287
|
action: string
|
|
270
|
-
|
|
271
|
-
input: Record<string, string>
|
|
288
|
+
input: ActionInput
|
|
272
289
|
status: PendingStatus
|
|
273
290
|
conflictRetries: number
|
|
274
291
|
result?: unknown
|
|
@@ -294,7 +311,7 @@ const RETRY_SAME_KEY = new Set([
|
|
|
294
311
|
/** One attempt; returns the row with its new status. The caller persists it. */
|
|
295
312
|
export const attempt = async (dd: Deepdock, row: PendingWrite): Promise<PendingWrite> => {
|
|
296
313
|
try {
|
|
297
|
-
const { data } = await dd.write(row.deviceId, row.system, row.action, row.
|
|
314
|
+
const { data } = await dd.write(row.deviceId, row.system, row.action, row.key, row.input)
|
|
298
315
|
return { ...row, status: 'done', result: data }
|
|
299
316
|
} catch (err) {
|
|
300
317
|
if (err instanceof DeepdockTransportError) return { ...row, status: 'sent' }
|
|
@@ -305,7 +322,15 @@ export const attempt = async (dd: Deepdock, row: PendingWrite): Promise<PendingW
|
|
|
305
322
|
? { ...row, status: 'sent', conflictRetries: row.conflictRetries + 1 }
|
|
306
323
|
: { ...row, status: 'unknown' }
|
|
307
324
|
}
|
|
308
|
-
|
|
325
|
+
// applied_unverified: the entry exists but was not verified; never write it again under a new
|
|
326
|
+
// key. `result.created` names it for the check.
|
|
327
|
+
if (err.code === 'outcome_unknown' || err.code === 'applied_unverified')
|
|
328
|
+
return {
|
|
329
|
+
...row,
|
|
330
|
+
status: 'unknown',
|
|
331
|
+
requestId: err.requestId,
|
|
332
|
+
result: { created: err.created, uncertain: err.uncertain },
|
|
333
|
+
}
|
|
309
334
|
// invalid_input, target_rejected, action-specific 422s, not_found, readback_failed, auth and
|
|
310
335
|
// resolution codes: nothing was written. Fix the cause; a changed input gets a new key.
|
|
311
336
|
return { ...row, status: 'refused', refusedCode: err.code, result: { message: err.message } }
|
|
@@ -72,9 +72,10 @@ offering it; it then runs the same way every time. Actions are
|
|
|
72
72
|
**assigned** to your organisation by Deepdock, so the set you can call is yours, not the whole
|
|
73
73
|
catalogue.
|
|
74
74
|
|
|
75
|
-
-
|
|
76
|
-
- A **
|
|
77
|
-
|
|
75
|
+
- Every action is called with `POST` and a flat JSON body; its `operation` says what it does.
|
|
76
|
+
- A **read** also pages with `limit` and `cursor` in the body.
|
|
77
|
+
- A **write** is a `create` (answers `201`) or an `update` (answers `200`), and needs an
|
|
78
|
+
`Idempotency-Key` header.
|
|
78
79
|
- A write **either fully applies or not**: Deepdock checks the result before confirming, and if the check fails it saves nothing
|
|
79
80
|
and pauses the action (`409 readback_failed`).
|
|
80
81
|
- An action is **active** or **paused**. `GET /v1/devices/{deviceId}/systems` shows each action's
|
|
@@ -7,7 +7,7 @@ Never hard-code an action's fields. Three sources, each authoritative for one th
|
|
|
7
7
|
| Which systems and actions does my organisation have? | `deepdock systems list --json` | CLI sign-in |
|
|
8
8
|
| What does this device run, and is each action active or paused? | `GET /v1/devices/{deviceId}/systems` | organisation |
|
|
9
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,
|
|
10
|
+
| The exact paths, operations, fields, patterns, enums, lengths, responses | `deepdock systems openapi <system>` (CLI sign-in) or `GET /v1/systems/{system}/openapi.json` (API key); per device `deepdock actions list <device> <system>` (CLI sign-in) | CLI sign-in or API key |
|
|
11
11
|
|
|
12
12
|
## The OpenAPI document
|
|
13
13
|
|
|
@@ -25,20 +25,19 @@ document into the repository and refresh it with the same command when you touch
|
|
|
25
25
|
What to read off it:
|
|
26
26
|
|
|
27
27
|
- **`paths`**: one operation per action at
|
|
28
|
-
`/v1/devices/{deviceId}/systems/{system}/actions/{action}`;
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
`/v1/devices/{deviceId}/systems/{system}/actions/{action}`; every one is a `post`;
|
|
29
|
+
its `operation` says whether it reads, creates or updates. Any other method answers
|
|
30
|
+
`405 invalid_request` with `Allow: POST`.
|
|
31
31
|
- **`summary` and `description`**: what the action does and what it changes in the system, what
|
|
32
32
|
an omitted optional input becomes, action-specific refusals (`code` and when), and what the
|
|
33
33
|
system only warns about and what the action does then. Read the description before mapping your
|
|
34
34
|
fields.
|
|
35
|
-
- **
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
property `type: string`, `required` lists the mandatory ones. The `Idempotency-Key` header
|
|
35
|
+
- **Inputs** are the `requestBody` object schema of every action, read or write:
|
|
36
|
+
`additionalProperties: false`, each property with its declared `type`, and for text `maxLength`
|
|
37
|
+
and possibly `minLength`, `pattern`, `enum` and a `description` such as "prefix match: case, punctuation and
|
|
38
|
+
umlaut spelling are ignored". A read's body also takes `limit` (integer, 1 to 250, default 25) and
|
|
39
|
+
`cursor` (opaque). An optional input is not in `required` and its description says what an
|
|
40
|
+
omitted value becomes. A write's `Idempotency-Key` header
|
|
42
41
|
parameter carries its own pattern (8 to 128 characters of `A-Z a-z 0-9 _ -`).
|
|
43
42
|
- **Responses**: a read's `200` is `{ data: { items: [...], nextCursor } }`; a create's `201` and a
|
|
44
43
|
replayed `200`; an update's `200`. Item and result fields are typed `string`, `integer` or `number`,
|
|
@@ -57,7 +56,7 @@ curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions" \
|
|
|
57
56
|
-H "Authorization: Bearer $DEEPDOCK_API_KEY"
|
|
58
57
|
```
|
|
59
58
|
|
|
60
|
-
Answers `{ data: [ { name,
|
|
59
|
+
Answers `{ data: [ { name, operation, description, input, output } ] }`, filtered to your assigned
|
|
61
60
|
actions, with the same JSON Schemas the OpenAPI document carries. Use it at runtime to confirm an
|
|
62
61
|
action exists before offering it in your UI, or to validate input client-side before sending.
|
|
63
62
|
|
|
@@ -17,7 +17,7 @@ with the **same** `Idempotency-Key`); **Show** says what the end user should see
|
|
|
17
17
|
|
|
18
18
|
| Code | Status | Meaning | Retry | Show |
|
|
19
19
|
|---|---|---|---|---|
|
|
20
|
-
| `invalid_request` | 400 (405 for a
|
|
20
|
+
| `invalid_request` | 400 (405 for a method but `POST`) | malformed request: bad `limit` or `cursor` in a read's body; missing or malformed `Idempotency-Key`; a body that is not an object; an action input that is unknown, missing, too short or long, out of bounds or not an allowed value (`fields` names each); a malformed pairing code or device id; a query string on an action path (inputs go in the JSON body), an input of the wrong type, such as `"4711"` where an integer is declared, an action called with any method but `POST` (`405`, `Allow: POST`) | no; fix the code | nothing; a developer bug |
|
|
21
21
|
| `payload_too_large` | 413 | the request body is over 1 MiB | no; send less | nothing; a developer bug, or shorten the text |
|
|
22
22
|
| `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 |
|
|
23
23
|
| `partner_not_approved` | 403 | your organisation is waiting for approval by Deepdock; no key of it works yet. Keys made before the approval start working once it is given | no, until Deepdock approves; then yes | "our Deepdock account is waiting for approval; contact Deepdock" |
|
|
@@ -62,10 +62,12 @@ dashboard-only.
|
|
|
62
62
|
| `device_offline` | 503 | the device is not connected, is shutting down, did not answer within 20 s, or the call was cut off inside Deepdock by a restart or deploy. **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" |
|
|
63
63
|
| `device_syncing` | 503 | the device has not reported its Deepdock client yet (just paired, or its report did not come through); Deepdock asks it again. Nothing was written | yes, after `Retry-After` (5 s); a write with the same key | "the customer PC is connecting; retrying" |
|
|
64
64
|
| `device_outdated` | 409 | the device's Deepdock client is too old for this call. It updates itself when a newer version is released for your organisation, once nobody is signed in on the PC. Nothing was written | after the update (`outdated` on `GET /v1/devices/{deviceId}` turns `false`); retrying before it gets the same answer. A write keeps its key | "the Deepdock client on the customer PC needs an update" |
|
|
65
|
-
| `system_version_unverified` | 409 | a write on a system whose vendor version Deepdock has not verified yet. Nothing was sent to the device; reads still work. `error.version` is the version the system last reported and `error.reason` is `unverified_version`,
|
|
65
|
+
| `system_version_unverified` | 409 | a write on a system whose vendor version Deepdock has not verified yet. Nothing was sent to the device; reads still work. `error.version` is the version the system last reported (null when it reported none) and `error.reason` is `unverified_version`, `unparseable` when that version is not dotted digits, or `unreported` when the system reported no version: a write over the system's own HTTP interface is verified for one exact release and needs it | `unverified_version`: after Deepdock has verified that version (`writes` on the system in `GET /v1/devices/{deviceId}` turns `verified`); retrying before it gets the same answer. `unparseable`: retrying does not help, contact Deepdock; only a different reported version changes it. `unreported`: later, once the device reports the system's version. A write keeps its key | "the customer's system was updated to a version Deepdock has not yet verified; changes will work once it has" |
|
|
66
66
|
| `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" |
|
|
67
67
|
| `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" |
|
|
68
68
|
| `target_busy` | 503 | the customer's system is busy or unreachable | yes, with backoff | "the customer's system is busy; retrying" |
|
|
69
|
+
| `credentials_rejected` | 503 | the customer's system refused the device's login. Nothing was sent | later, after a person at the customer has renewed the access in the Deepdock client; do not loop. A write keeps its key | "the connection to the customer's software needs to be renewed at the practice" |
|
|
70
|
+
| `certificate_changed` | 503 | the customer's system shows a certificate other than the one the device trusts. Nothing was sent | later, after a person at the customer has confirmed the new certificate in the Deepdock client; do not loop. A write keeps its key | "the connection to the customer's software needs to be confirmed at the practice" |
|
|
69
71
|
| `temporarily_unavailable` | 503 | Deepdock could not check the key or reach its own database (sent with `Retry-After: 5`) | yes, with backoff | "temporary problem; retrying" |
|
|
70
72
|
|
|
71
73
|
## Write outcomes
|
|
@@ -73,8 +75,9 @@ dashboard-only.
|
|
|
73
75
|
| Code | Status | Meaning | Retry | Show |
|
|
74
76
|
|---|---|---|---|---|
|
|
75
77
|
| `readback_failed` | 409 | the write's result did not match what the action expects, so nothing was saved; **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" |
|
|
78
|
+
| `applied_unverified` | 409 | a write on a system with no rollback was applied, but its read-back did not finish. The entry exists: `error.created` names it as `{ chartId }`, or is null when the entry is not known | **no.** The entry is there; writing again creates a second one. Check it in the customer system | "the change was saved but could not be confirmed; please check it in the customer system" |
|
|
76
79
|
| `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" |
|
|
77
|
-
| `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" |
|
|
80
|
+
| `outcome_unknown` | 500 | the system did not confirm the write; it may or may not have landed. The key stays blocked. On a system with no rollback, `error.uncertain` is true and `error.created` names the entry when one is known | **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" |
|
|
78
81
|
| `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 |
|
|
79
82
|
|
|
80
83
|
## A reaction table for code
|
|
@@ -104,6 +107,8 @@ const REACTION: Record<string, Reaction> = {
|
|
|
104
107
|
payload_too_large: 'fix-code',
|
|
105
108
|
access_outdated: 'wait-setup',
|
|
106
109
|
access_missing: 'wait-setup',
|
|
110
|
+
credentials_rejected: 'wait-setup',
|
|
111
|
+
certificate_changed: 'wait-setup',
|
|
107
112
|
device_offline: 'retry-same-key',
|
|
108
113
|
device_syncing: 'retry-same-key',
|
|
109
114
|
device_outdated: 'retry-later',
|
|
@@ -113,6 +118,7 @@ const REACTION: Record<string, Reaction> = {
|
|
|
113
118
|
target_busy: 'retry-same-key',
|
|
114
119
|
temporarily_unavailable: 'retry-same-key',
|
|
115
120
|
readback_failed: 'wait-paused',
|
|
121
|
+
applied_unverified: 'check-then-decide', // the entry exists; reading it back shows it
|
|
116
122
|
idempotency_conflict: 'retry-same-key', // bounded; then 'check-then-decide'
|
|
117
123
|
outcome_unknown: 'check-then-decide',
|
|
118
124
|
internal_error: 'retry-same-key',
|
|
@@ -120,7 +126,7 @@ const REACTION: Record<string, Reaction> = {
|
|
|
120
126
|
const reactionFor = (code: string): Reaction => REACTION[code] ?? 'stop'
|
|
121
127
|
```
|
|
122
128
|
|
|
123
|
-
Action-specific 422 codes beyond the ones above (any the OpenAPI description lists) map to `fix-input`. Add them from the document, not from memory. `wait-setup` means a person at the customer must repeat the database access setup ("Erneut einrichten")
|
|
129
|
+
Action-specific 422 codes beyond the ones above (any the OpenAPI description lists) map to `fix-input`. Add them from the document, not from memory. `wait-setup` means a person at the customer must act in the Deepdock client first: repeat the database access setup ("Erneut einrichten") for `access_*`, renew the login for `credentials_rejected`, confirm the new certificate for `certificate_changed`; there is no `Retry-After`, so do not loop. Check `access` on the system in `GET .../systems` (`ok` again), then retry; a write keeps its key. `retry-same-key` means the
|
|
124
130
|
pending write stays persisted and is retried from that state with backoff, also across process
|
|
125
131
|
restarts; a device can be offline for hours, so the retry loop lives in a job or queue, not in the
|
|
126
132
|
request that started the write.
|
|
@@ -23,7 +23,7 @@ import { Deepdock, DeepdockError, DeepdockTransportError, attempt, type PendingW
|
|
|
23
23
|
const dd = new Deepdock(process.env.DEEPDOCK_API_KEY!) // base from DEEPDOCK_BASE_URL, else production; 30 s timeout
|
|
24
24
|
const page = await dd.read<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' }, { limit: 50 })
|
|
25
25
|
for await (const p of dd.readAll<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' })) { /* every page */ }
|
|
26
|
-
const { data, replayed } = await dd.write<{ noteId: number }>(deviceId, 'example-erp', 'createNote',
|
|
26
|
+
const { data, replayed } = await dd.write<{ noteId: number }>(deviceId, 'example-erp', 'createNote', key, input)
|
|
27
27
|
const systems = await dd.systems(deviceId) // live; can throw device_offline
|
|
28
28
|
const device = await dd.pair('K7Q-4MX', 'Demo') // keep device.id
|
|
29
29
|
```
|
|
@@ -33,7 +33,7 @@ from deepdock import Deepdock, DeepdockError, DeepdockTransportError, PendingWri
|
|
|
33
33
|
|
|
34
34
|
dd = Deepdock(os.environ["DEEPDOCK_API_KEY"])
|
|
35
35
|
page = dd.read(device_id, "example-erp", "searchNotes", {"text": "TEST-DEEPDOCK-"}, limit=50)
|
|
36
|
-
res = dd.write(device_id, "example-erp", "createNote",
|
|
36
|
+
res = dd.write(device_id, "example-erp", "createNote", key, body) # res.data, res.replayed
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Errors: `DeepdockError` has `status`, `code`, `requestId`, `systems`; switch on `code`
|
|
@@ -42,7 +42,7 @@ Errors: `DeepdockError` has `status`, `code`, `requestId`, `systems`; switch on
|
|
|
42
42
|
|
|
43
43
|
## Sending a write: persist first, then one attempt at a time
|
|
44
44
|
|
|
45
|
-
Persist the pending write (key, device, action,
|
|
45
|
+
Persist the pending write (key, device, action, input, status) **before** the first send,
|
|
46
46
|
so a crash between send and answer cannot produce a second record under a new key. `attempt()`
|
|
47
47
|
moves the row to `done`, `sent` (retry later, same key), `unknown` (read back before anything
|
|
48
48
|
else) or `refused` (nothing was written; a changed input gets a new key). A job retries `sent`
|
|
@@ -51,7 +51,7 @@ rows with backoff: seconds, then minutes, for hours if the device stays offline.
|
|
|
51
51
|
```ts
|
|
52
52
|
const row: PendingWrite = {
|
|
53
53
|
key: `order-${order.id}-create-note`, // 8 to 128 chars of A-Z a-z 0-9 _ -, stable per business event
|
|
54
|
-
deviceId, system: 'example-erp', action: 'createNote',
|
|
54
|
+
deviceId, system: 'example-erp', action: 'createNote',
|
|
55
55
|
input, status: 'pending', conflictRetries: 0,
|
|
56
56
|
}
|
|
57
57
|
await store.save(row) // before the first send
|
|
@@ -74,7 +74,7 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
|
74
74
|
export const run = async (store: { save: (row: PendingWrite) => Promise<void> }) => {
|
|
75
75
|
const marker = `TEST-DEEPDOCK-${randomUUID().replaceAll('-', '').slice(0, 6).toUpperCase()}`
|
|
76
76
|
let row: PendingWrite = {
|
|
77
|
-
key: `demo-${marker}-create`, deviceId, system, action: 'createNote',
|
|
77
|
+
key: `demo-${marker}-create`, deviceId, system, action: 'createNote',
|
|
78
78
|
input: { text: marker, orderNumber: '1001' },
|
|
79
79
|
status: 'pending', conflictRetries: 0,
|
|
80
80
|
}
|
|
@@ -122,8 +122,9 @@ curl -sS "$BASE/v1/devices/$DEVICE/systems" -H "$AUTH"
|
|
|
122
122
|
# The action index (your assigned actions, with schemas)
|
|
123
123
|
curl -sS "$BASE/v1/devices/$DEVICE/systems/example-erp/actions" -H "$AUTH"
|
|
124
124
|
|
|
125
|
-
# A read, paged: pass data.nextCursor back as cursor
|
|
126
|
-
curl -sS "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchNotes
|
|
125
|
+
# A read is a POST too, with its inputs and limit/cursor in the body; paged: pass data.nextCursor back as cursor
|
|
126
|
+
curl -sS -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchNotes" \
|
|
127
|
+
-H "$AUTH" -H 'Content-Type: application/json' -d '{"text":"TEST-DEEPDOCK-","limit":50}'
|
|
127
128
|
|
|
128
129
|
# A create: new Idempotency-Key per intended write, the same on every retry; 201, or 200 with
|
|
129
130
|
# Idempotent-Replayed: true on a replay (-i shows the headers)
|
|
@@ -131,10 +132,10 @@ curl -sS -i -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/create
|
|
|
131
132
|
-H "$AUTH" -H "Idempotency-Key: demo-$(date +%s)-create" -H 'Content-Type: application/json' \
|
|
132
133
|
-d '{"text":"TEST-DEEPDOCK-K3M9QA","orderNumber":"1001"}'
|
|
133
134
|
|
|
134
|
-
# An update:
|
|
135
|
-
curl -sS -i -X
|
|
135
|
+
# An update: also POST, same rules
|
|
136
|
+
curl -sS -i -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/updateNote" \
|
|
136
137
|
-H "$AUTH" -H "Idempotency-Key: demo-$(date +%s)-update" -H 'Content-Type: application/json' \
|
|
137
|
-
-d '{"noteId":
|
|
138
|
+
-d '{"noteId":1001,"text":"TEST-DEEPDOCK-K3M9QA edited"}'
|
|
138
139
|
|
|
139
140
|
# Release a device
|
|
140
141
|
curl -sS -X DELETE "$BASE/v1/devices/$DEVICE" -H "$AUTH"
|
|
@@ -142,7 +143,7 @@ curl -sS -X DELETE "$BASE/v1/devices/$DEVICE" -H "$AUTH"
|
|
|
142
143
|
|
|
143
144
|
A port in another language keeps the clients' shape: one `send` that sets the bearer header,
|
|
144
145
|
parses the envelope, throws a typed error with `code` on `{ error }` and a transport error on no
|
|
145
|
-
answer or non-JSON; `read` with `limit` and `cursor
|
|
146
|
+
answer or non-JSON; `read` as a POST with `limit` and `cursor` in the body; `write` with `Idempotency-Key` and the
|
|
146
147
|
`Idempotent-Replayed` header; `attempt` over a persisted pending write.
|
|
147
148
|
|
|
148
149
|
## Mapping the user's intent to a call (checklist)
|
|
@@ -150,9 +151,9 @@ answer or non-JSON; `read` with `limit` and `cursor`; `write` with `Idempotency-
|
|
|
150
151
|
1. Which device? From your product's configuration for that customer site; never from the
|
|
151
152
|
user's free text.
|
|
152
153
|
2. Is the action active? `systems()` once per session; show paused features as paused.
|
|
153
|
-
3. Which action and
|
|
154
|
+
3. Which action and operation? From the stored index for that system.
|
|
154
155
|
4. Does the write need a reference (author user, open case, record id)? Read it first.
|
|
155
|
-
5. Build the input
|
|
156
|
+
5. Build the input in the types the schema declares; validate against the schema (`pattern`, `minLength`, `maxLength`,
|
|
156
157
|
`enum`) before sending so the user sees the rule where they typed.
|
|
157
158
|
6. Choose the idempotency key from your business event and persist it with the pending write.
|
|
158
159
|
7. Send; map the code with the reaction table; show the user the right sentence.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Idempotency for writes
|
|
2
2
|
|
|
3
|
-
Every `
|
|
4
|
-
refused with `400 invalid_request`.
|
|
3
|
+
Every write (a `create` or `update` action, called with `POST`) carries an `Idempotency-Key` header. Without it the request is
|
|
4
|
+
refused with `400 invalid_request`. A read ignores the header.
|
|
5
5
|
|
|
6
6
|
## The key
|
|
7
7
|
|
|
@@ -74,6 +74,7 @@ pending ──send──▶ sent ──2xx (fresh or replayed)──────
|
|
|
74
74
|
│ ──idempotency_conflict, retries left ───▶ sent (retry, same key, same input)
|
|
75
75
|
│ ──idempotency_conflict, retries spent ──▶ unknown (read back before anything else)
|
|
76
76
|
│ ──outcome_unknown ──────────────────────▶ unknown (read back before anything else)
|
|
77
|
+
│ ──applied_unverified ───────────────────▶ unknown (the entry exists: check it by result.created, never write it again)
|
|
77
78
|
│ ──invalid_input, target_rejected, action-specific 422, not_found, readback_failed ▶ refused (key is free; new input gets a new key)
|
|
78
79
|
│ ──action_paused, system_unavailable ────▶ sent (retry once the action is active again)
|
|
79
80
|
```
|