@deepdockai/cli 1.0.25 → 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 CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## 1.0.25
4
11
 
5
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.
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 method and fields |
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 |
package/dist/deepdock.js CHANGED
@@ -11236,13 +11236,20 @@ var summary = (description) => {
11236
11236
  return chars.length > 160 ? `${chars.slice(0, 159).join("").trimEnd()}…` : sentence;
11237
11237
  };
11238
11238
  var actionRow = (action) => {
11239
- const method = typeof action.method === "string" ? action.method : undefined;
11239
+ const operation = typeof action.operation === "string" ? action.operation : undefined;
11240
11240
  const fields = fieldNames(action.input);
11241
- const { name, method: _method, input: _input, output: _output, description, ...rest } = action;
11241
+ const {
11242
+ name,
11243
+ operation: _operation,
11244
+ input: _input,
11245
+ output: _output,
11246
+ description,
11247
+ ...rest
11248
+ } = action;
11242
11249
  const brief = summary(description);
11243
11250
  return {
11244
11251
  ...name === undefined ? {} : { name },
11245
- ...method ? { kind: method.toUpperCase() === "GET" ? "read" : "write" } : {},
11252
+ ...operation ? { kind: operation === "read" ? "read" : "write" } : {},
11246
11253
  ...fields ? { fields } : {},
11247
11254
  ...brief ? { description: brief } : {},
11248
11255
  ...rest
@@ -11432,7 +11439,7 @@ import { spawn } from "node:child_process";
11432
11439
  import { homedir, hostname } from "node:os";
11433
11440
 
11434
11441
  // src/version.ts
11435
- var VERSION = "1.0.25";
11442
+ var VERSION = "1.0.26";
11436
11443
 
11437
11444
  // src/runtime.ts
11438
11445
  var DEFAULT_BASE_URL = "https://api.deepdock.ai";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepdockai/cli",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "Deepdock partner CLI: pair devices, see systems and actions, create the API key your code runs them with.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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. A **read** action is a `GET` with query parameters and pages with `limit` and `cursor`; a **write**
26
- action is a `POST` (create) or `PATCH` (update) with a JSON body of string fields.
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
- Every input value is text and is normalised to Unicode NFC before anything else happens; a
7
- composed and a decomposed spelling are the same value.
6
+ ## Calling an action: POST
8
7
 
9
- ## Reads: `GET`
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
- Input as query parameters; `limit` (1 to 250, default 25) and `cursor` for paging.
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?lastName=M%C3%BCl&limit=50" \
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
- ## Writes: `POST` (create) or `PATCH` (update)
52
+ ### Writes (create and update)
34
53
 
35
- Body: a JSON object whose values are all strings (numbers and ids too, as strings), with only the
36
- fields the schema lists (`additionalProperties: false`) and all of its `required` ones. Header
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 you which.
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: a
52
- create typically returns the new id as an integer, while an update echoes the id you sent as a
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 as a string field in the write.
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
- - **Ids come back as integers and go in as strings.** A read and a create answer ids as integers
77
- (a create's new id too); only an update echoes the key you sent, as the string you sent.
78
- `customerId: 1001` in a search result goes back as `customerId=1001` in a read's query string and
79
- as `"customerId": "1001"` in a write body: digits only, no leading zero, at most 10 characters.
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 read action's query string treats `+` as a space: send a literal `+` as `%2B`.
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` | the method the action has |
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
 
@@ -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 method and field schemas | member |
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 |
@@ -2,18 +2,21 @@
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 (paths, fields, methods) comes from the live OpenAPI document.
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`."""
@@ -79,18 +82,19 @@ class Deepdock:
79
82
  def _action_path(device_id: str, system: str, action: str) -> str:
80
83
  return f"/v1/devices/{quote(device_id, safe='')}/systems/{quote(system, safe='')}/actions/{quote(action, safe='')}"
81
84
 
82
- def read(self, device_id: str, system: str, action: str, params: Dict[str, str],
85
+ def read(self, device_id: str, system: str, action: str, params: Dict[str, InputValue],
83
86
  limit: Optional[int] = None, cursor: Optional[str] = None) -> Page:
84
- """Runs a read action. Pass `next_cursor` back as `cursor` for the next page."""
85
- query: Dict[str, str] = dict(params)
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)
86
90
  if limit is not None:
87
- query["limit"] = str(limit)
91
+ body["limit"] = limit
88
92
  if cursor is not None:
89
- query["cursor"] = cursor
90
- data, _ = self._send("GET", self._action_path(device_id, system, action), params=query)
93
+ body["cursor"] = cursor
94
+ data, _ = self._send("POST", self._action_path(device_id, system, action), json=body)
91
95
  return Page(items=data["items"], next_cursor=data.get("nextCursor"))
92
96
 
93
- def read_all(self, device_id: str, system: str, action: str, params: Dict[str, str],
97
+ def read_all(self, device_id: str, system: str, action: str, params: Dict[str, InputValue],
94
98
  limit: Optional[int] = None) -> Iterator[Dict[str, Any]]:
95
99
  """Reads every page of a read action, in order."""
96
100
  cursor: Optional[str] = None
@@ -101,10 +105,10 @@ class Deepdock:
101
105
  return
102
106
  cursor = page.next_cursor
103
107
 
104
- def write(self, device_id: str, system: str, action: str, method: str, idempotency_key: str,
105
- body: Dict[str, str]) -> WriteResult:
106
- """Runs a write action. `method` is POST or PATCH from the action index; `body` values are all strings."""
107
- data, headers = self._send(method, self._action_path(device_id, system, action), json=body,
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,
108
112
  headers={"Idempotency-Key": idempotency_key})
109
113
  return WriteResult(data=data, replayed=headers.get("Idempotent-Replayed") == "true")
110
114
 
@@ -146,8 +150,7 @@ class PendingWrite:
146
150
  device_id: str
147
151
  system: str
148
152
  action: str
149
- method: str
150
- input: Dict[str, str]
153
+ input: Dict[str, InputValue]
151
154
  status: str = "pending" # pending | sent | done | unknown | refused
152
155
  conflict_retries: int = 0
153
156
  result: Optional[Dict[str, Any]] = field(default=None)
@@ -158,7 +161,7 @@ class PendingWrite:
158
161
  def attempt(dd: Deepdock, row: PendingWrite) -> PendingWrite:
159
162
  """One attempt; returns the row with its new status. The caller persists it."""
160
163
  try:
161
- res = dd.write(row.device_id, row.system, row.action, row.method, row.key, row.input)
164
+ res = dd.write(row.device_id, row.system, row.action, row.key, row.input)
162
165
  return replace(row, status="done", result=res.data)
163
166
  except DeepdockTransportError:
164
167
  return replace(row, status="sent")
@@ -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 (paths, fields, methods) comes from the live OpenAPI document.
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
@@ -55,7 +55,9 @@ type Envelope<T> = { data: T } | { error: ErrorBody }
55
55
 
56
56
  export type Page<Item> = { items: Item[]; nextCursor: string | null }
57
57
  export type WriteResult<T> = { data: T; replayed: boolean }
58
- export type WriteMethod = 'POST' | 'PATCH'
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>
59
61
 
60
62
  export type SystemStatus = 'running' | 'stopped' | 'missing' | 'unknown'
61
63
  /** The database access the device last reported; `null` while none is known. */
@@ -187,19 +189,27 @@ export class Deepdock {
187
189
  return `/v1/devices/${encodeURIComponent(deviceId)}/systems/${encodeURIComponent(system)}/actions/${encodeURIComponent(action)}`
188
190
  }
189
191
 
190
- /** Runs a read action. Pass `nextCursor` back as `cursor` for the next page. */
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
+ */
191
196
  async read<Item>(
192
197
  deviceId: string,
193
198
  system: string,
194
199
  action: string,
195
- input: Record<string, string>,
200
+ input: ActionInput,
196
201
  page: { limit?: number; cursor?: string } = {},
197
202
  ): Promise<Page<Item>> {
198
- const query = new URLSearchParams(input)
199
- if (page.limit !== undefined) query.set('limit', String(page.limit))
200
- if (page.cursor !== undefined) query.set('cursor', page.cursor)
201
- const suffix = query.size > 0 ? `?${query}` : ''
202
- return (await this.send<Page<Item>>(`${this.actionPath(deviceId, system, action)}${suffix}`, { method: 'GET' })).data
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
203
213
  }
204
214
 
205
215
  /** Reads every page of a read action, in order. */
@@ -207,7 +217,7 @@ export class Deepdock {
207
217
  deviceId: string,
208
218
  system: string,
209
219
  action: string,
210
- input: Record<string, string>,
220
+ input: ActionInput,
211
221
  limit?: number,
212
222
  ): AsyncGenerator<Item> {
213
223
  let cursor: string | undefined
@@ -219,19 +229,18 @@ export class Deepdock {
219
229
  }
220
230
 
221
231
  /**
222
- * Runs a write action. `method` comes from the action index or the OpenAPI document; `input`
223
- * values are all strings. `idempotencyKey` is new per intended write and the same on every retry.
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.
224
234
  */
225
235
  async write<Result>(
226
236
  deviceId: string,
227
237
  system: string,
228
238
  action: string,
229
- method: WriteMethod,
230
239
  idempotencyKey: string,
231
- input: Record<string, string>,
240
+ input: ActionInput,
232
241
  ): Promise<WriteResult<Result>> {
233
242
  const { data, headers } = await this.send<Result>(this.actionPath(deviceId, system, action), {
234
- method,
243
+ method: 'POST',
235
244
  headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
236
245
  body: JSON.stringify(input),
237
246
  })
@@ -276,8 +285,7 @@ export type PendingWrite = {
276
285
  deviceId: string
277
286
  system: string
278
287
  action: string
279
- method: WriteMethod
280
- input: Record<string, string>
288
+ input: ActionInput
281
289
  status: PendingStatus
282
290
  conflictRetries: number
283
291
  result?: unknown
@@ -303,7 +311,7 @@ const RETRY_SAME_KEY = new Set([
303
311
  /** One attempt; returns the row with its new status. The caller persists it. */
304
312
  export const attempt = async (dd: Deepdock, row: PendingWrite): Promise<PendingWrite> => {
305
313
  try {
306
- const { data } = await dd.write(row.deviceId, row.system, row.action, row.method, row.key, row.input)
314
+ const { data } = await dd.write(row.deviceId, row.system, row.action, row.key, row.input)
307
315
  return { ...row, status: 'done', result: data }
308
316
  } catch (err) {
309
317
  if (err instanceof DeepdockTransportError) return { ...row, status: 'sent' }
@@ -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
- - A **read** is `GET` with query parameters and pages.
76
- - A **write** is `POST` (creates a record, answers `201`) or `PATCH` (updates a record, answers
77
- `200`), with a JSON body of string fields and an `Idempotency-Key` header.
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, methods, 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 |
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}`; the HTTP method is the action's
29
- kind (`get` read, `post` create, `patch` update). Use the method the document gives; the other
30
- write method answers `405 invalid_request` with an `Allow` header.
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
- - **Read inputs** are query parameters, all `type: string`, each with `maxLength` and possibly
36
- `minLength`, `pattern`, `enum` and a `description` such as "prefix match: case, punctuation and
37
- umlaut spelling are ignored". Plus `limit` (integer, 1 to 250, default 25) and `cursor` (opaque).
38
- An optional parameter is not in `required` and its description says what an omitted value
39
- becomes.
40
- - **Write inputs** are the `requestBody` object schema: `additionalProperties: false`, every
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, method, description, input, output } ] }`, filtered to your assigned
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 wrong method) | malformed request: bad query, `limit` or `cursor`; missing or malformed `Idempotency-Key`; a body that is not an object; an action input that is unknown, missing, of the wrong type, too short or long, out of bounds or not an allowed value (`fields` names each); 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 |
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" |
@@ -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', 'POST', key, input)
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", "POST", key, body) # res.data, res.replayed
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, method, input, status) **before** the first send,
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', method: 'POST',
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', method: 'POST',
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?text=TEST-DEEPDOCK-&limit=50" -H "$AUTH"
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: PATCH, same rules
135
- curl -sS -i -X PATCH "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/updateNote" \
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":"1001","text":"TEST-DEEPDOCK-K3M9QA edited"}'
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`; `write` with `Idempotency-Key` and the
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 method? From the stored index for that system.
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 as strings; validate against the schema (`pattern`, `minLength`, `maxLength`,
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 `POST` or `PATCH` to an action carries an `Idempotency-Key` header. Without it the request is
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