@com55/node-red-ai-access 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +319 -0
- package/LICENSE +22 -0
- package/README.md +269 -2
- package/ai-access.cjs +8 -0
- package/ai-access.html +34 -0
- package/dist/plugin.cjs +5471 -0
- package/dist/plugin.d.cts +693 -0
- package/docs/README.md +26 -0
- package/docs/api/openapi.yaml +2579 -0
- package/docs/errors.md +83 -0
- package/docs/mcp-clients.md +78 -0
- package/docs/mcp-tools.md +148 -0
- package/package.json +135 -4
|
@@ -0,0 +1,2579 @@
|
|
|
1
|
+
openapi: 3.1.0
|
|
2
|
+
|
|
3
|
+
info:
|
|
4
|
+
title: Node-RED AI Access Plugin API
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
summary: Temporary, owner-approved Node-RED access for AI clients
|
|
7
|
+
description: |
|
|
8
|
+
REST surface exposed by the `@com55/node-red-ai-access` Node-RED plugin
|
|
9
|
+
under the editor admin root (respects `httpAdminRoot`).
|
|
10
|
+
|
|
11
|
+
**Git mental model:** a proposal is like a pull request. Human approve =
|
|
12
|
+
merge; reject = close; withdraw = the author closes their own PR. If `rev` /
|
|
13
|
+
`expectedRev` does not match, treat it as a conflict: re-read the flow,
|
|
14
|
+
rebase the edit, and resubmit. Every access link can see all proposals
|
|
15
|
+
(`mine: true|false`).
|
|
16
|
+
|
|
17
|
+
## Base path
|
|
18
|
+
|
|
19
|
+
All routes are under `{httpAdminRoot}/mcp/api/v1`
|
|
20
|
+
(for example `/mcp/api/v1` when `httpAdminRoot` is `/`).
|
|
21
|
+
|
|
22
|
+
## Quick start for access-link clients
|
|
23
|
+
|
|
24
|
+
### Self-contained GET link (no headers)
|
|
25
|
+
|
|
26
|
+
Prefer this when the AI can only issue HTTP GET (no `Authorization` header,
|
|
27
|
+
no MCP). An admin creates a **get_link** token via `POST /links` (this
|
|
28
|
+
OpenAPI document); the create response returns `url` like
|
|
29
|
+
`http(s)://{host}{httpAdminRoot}/mcp/a/{token}` (default TTL **4h**).
|
|
30
|
+
|
|
31
|
+
**GET `/mcp/a/<token>/…` never mints tokens.** There is no link-create
|
|
32
|
+
operation under the get-link surface (no `POST …/links`). Attempted mint
|
|
33
|
+
paths return **405** `link_create_not_supported`. Only admins with `*`
|
|
34
|
+
may create tokens on `POST /mcp/api/v1/links`.
|
|
35
|
+
|
|
36
|
+
1. `GET {url}` — markdown docs (workflow, exact paths, errors, limits).
|
|
37
|
+
2. `GET {url}/status`, `/flows`, …
|
|
38
|
+
3. Layered read (prefer before loading whole tabs):
|
|
39
|
+
`GET {url}/flows/{id}?summary=1` → `GET {url}/search?q=&flow=` →
|
|
40
|
+
`GET {url}/nodes/{id}` / `…/nodes/{id}/field/{name}?lines=A-B`.
|
|
41
|
+
4. Small string edits (preferred): `GET {url}/propose?flow=&rev=&node=&field=&find=&replace=`
|
|
42
|
+
(`occurrence` 1-based if find is not unique). Same as patch
|
|
43
|
+
`{op:'replace_text',...}` / REST·MCP `payload.nodes`.
|
|
44
|
+
5. Whole property: `GET {url}/propose?flow=&rev=&node=&set.<prop>=&summary=`
|
|
45
|
+
6. Multi-node / add / remove / replace_text: `GET {url}/propose?flow=&rev=&patch=<base64url JSON>`
|
|
46
|
+
where the JSON is `nodes:[{op:'set'|'add'|'remove'|'replace_text',...}]` or a
|
|
47
|
+
full `replace_flow` document (URL soft-cap ~8KB → use chunked drafts;
|
|
48
|
+
prefer ~4–5 KB raw chunks; base64url adds ~33%).
|
|
49
|
+
7. Chunked drafts: `…/draft/new` → `…/draft/{id}/append?seq=&data=` →
|
|
50
|
+
`…/draft/{id}/submit?sha256=` (30 min expiry, 1.5MB **decoded** bytes;
|
|
51
|
+
field mode validates node/field at `draft/new`; `mode=json` may add nodes).
|
|
52
|
+
Last resort if still too large: placeholder propose → human deploy → user pastes
|
|
53
|
+
full content in the editor (not reviewed via diff).
|
|
54
|
+
8. Human approves on `/mcp`. JSON uses `Cache-Control: no-store`.
|
|
55
|
+
Unknown paths under `/mcp/a/*` return JSON `not_found` (never echo the token).
|
|
56
|
+
|
|
57
|
+
Recommended edit loop: summary → search → read a line range →
|
|
58
|
+
`replace_text` → propose.
|
|
59
|
+
|
|
60
|
+
### Bearer REST (clients that can send headers)
|
|
61
|
+
|
|
62
|
+
Plain HTTP AI clients (no MCP required) can also use an access-link token
|
|
63
|
+
and this REST API:
|
|
64
|
+
|
|
65
|
+
1. Ask the user for an access link from the `/mcp` admin page (**Access
|
|
66
|
+
links** → Create). Prefer the copy-link output (full GET `url` + Bearer
|
|
67
|
+
token + docs hint).
|
|
68
|
+
2. Discover endpoints (no auth): `GET {httpAdminRoot}/mcp/api/v1`.
|
|
69
|
+
3. Send `Authorization: Bearer <token>` on every authenticated call.
|
|
70
|
+
4. Read flows lightly: `GET /flows`, then `GET /flows/{id}?summary=1`,
|
|
71
|
+
`GET /search?q=`, `GET /nodes/{id}`, `GET /nodes/{id}/field/{name}?lines=A-B`
|
|
72
|
+
(full `GET /flows/{id}` still available; response always includes
|
|
73
|
+
`flow.type`, defaulting to `tab`).
|
|
74
|
+
5. Submit a proposal. Prefer targeted `replace_text` for small string edits:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"type": "replace_flow",
|
|
79
|
+
"expectedRev": "<rev from GET /flows or /flows/{id}>",
|
|
80
|
+
"payload": {
|
|
81
|
+
"flowId": "tab1",
|
|
82
|
+
"nodes": [
|
|
83
|
+
{
|
|
84
|
+
"op": "replace_text",
|
|
85
|
+
"node": "n1",
|
|
86
|
+
"field": "template",
|
|
87
|
+
"find": "Hello",
|
|
88
|
+
"replace": "Hi"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or a full tab via `payload.flow` (from `GET /flows/{id}`).
|
|
96
|
+
|
|
97
|
+
Missing tokens return **401** `access_link_missing`; invalid/expired/revoked
|
|
98
|
+
tokens return **401** `access_link_invalid`. Both tell you to ask the user
|
|
99
|
+
for a new link. OpenAPI: `GET /openapi.yaml` (or `GET /openapi.json`). MCP (optional):
|
|
100
|
+
`POST {httpAdminRoot}/mcp/rpc` with the same Bearer token.
|
|
101
|
+
|
|
102
|
+
## MCP JSON-RPC
|
|
103
|
+
|
|
104
|
+
AI clients may also use MCP Streamable HTTP at `{httpAdminRoot}/mcp/rpc`
|
|
105
|
+
with the same access-link Bearer token. Tools mirror the AI-facing REST
|
|
106
|
+
operations (`get_status`, `list_flows`, `get_flow` with optional `summary`,
|
|
107
|
+
`get_node`, `get_node_field`, `search_flows`, `list_node_types`,
|
|
108
|
+
`get_node_type`, `search_palette`, `submit_proposal`, `list_proposals`
|
|
109
|
+
(alias `list_my_proposals`),
|
|
110
|
+
`get_proposal`, `get_proposal_diff`). Approve/reject/link management stay on
|
|
111
|
+
this REST API and the `/mcp` admin page. This OpenAPI document covers the
|
|
112
|
+
REST surface only.
|
|
113
|
+
|
|
114
|
+
## Authentication
|
|
115
|
+
|
|
116
|
+
- **Admin session** — Node-RED `adminAuth` cookie/session. Users with
|
|
117
|
+
`read` may view status, flows, proposals, and logs. Users with `*` may
|
|
118
|
+
also manage access links and approve/reject proposals.
|
|
119
|
+
- **Access-link Bearer token** — temporary token minted by an admin with
|
|
120
|
+
`*` via `POST /links` (never under `/mcp/a`). Each token has a
|
|
121
|
+
`kind` (`get_link` | `rest` | `mcp`) and only works on that surface.
|
|
122
|
+
AI clients send `Authorization: Bearer <token>` for **rest** (this API)
|
|
123
|
+
or **mcp** (`/mcp/rpc`). Tokens are in-memory only (lost on restart).
|
|
124
|
+
|
|
125
|
+
When an access-link token is **missing**, AI-facing routes return **401**
|
|
126
|
+
with `code: access_link_missing`. When the token is **invalid**, expired,
|
|
127
|
+
revoked, or cleared by a restart, they return **401** with
|
|
128
|
+
`code: access_link_invalid`. Both include `WWW-Authenticate: Bearer` and a
|
|
129
|
+
message that tells the AI to ask the user for a new access link.
|
|
130
|
+
When the token kind does not match the surface, routes return **403**
|
|
131
|
+
with `code: wrong_token_kind` (`expectedKind` / `actualKind`).
|
|
132
|
+
|
|
133
|
+
## Admin page (`GET /mcp`)
|
|
134
|
+
|
|
135
|
+
The HTML shell is served **without** adminAuth so the browser can read the
|
|
136
|
+
Node-RED editor token from `localStorage` (editors do not use cookies) and
|
|
137
|
+
call APIs with `Authorization: Bearer`. All data and mutating actions stay
|
|
138
|
+
behind `needsPermission` / access-link checks.
|
|
139
|
+
|
|
140
|
+
## Size limits
|
|
141
|
+
|
|
142
|
+
Proposal JSON is capped at **1_500_000 bytes** (`MAX_PROPOSAL_JSON_BYTES` /
|
|
143
|
+
~1.5MB) on **every** channel: GET propose/drafts, REST `POST /proposals`,
|
|
144
|
+
and MCP `submit_proposal`. Oversized proposal JSON or audit append payloads
|
|
145
|
+
return **413** with `code: payload_too_large` (never silently truncate).
|
|
146
|
+
Malformed JSON bodies return **400** JSON (`code: invalid_json` on REST;
|
|
147
|
+
JSON-RPC `-32700` Parse error on `/mcp/rpc`) — never HTML stack traces.
|
|
148
|
+
Unauthenticated admin API calls return JSON `{ code: auth_required }`
|
|
149
|
+
(not plain-text `Unauthorized` from `needsPermission`).
|
|
150
|
+
The MCP endpoint shares the same limits, rate limiting, and audit logging
|
|
151
|
+
as REST.
|
|
152
|
+
|
|
153
|
+
## Proposal visibility and conflicts
|
|
154
|
+
|
|
155
|
+
Every access-link token can **list and read all proposals**. Each item
|
|
156
|
+
includes `mine: true|false` for the caller. Filter with `?mine=true`,
|
|
157
|
+
`?status=`, and/or `?flowId=` (REST/GET), or MCP `list_proposals` with the
|
|
158
|
+
same fields. Paginate with `limit`/`offset`; every channel returns the
|
|
159
|
+
same pagination fields: `total` (matches before paging), `offset`, `limit`
|
|
160
|
+
(when given), `hasMore`, and `nextOffset` (only when `hasMore`). Pagination
|
|
161
|
+
is offset-based (no cursor). MCP keeps `list_my_proposals` as an alias.
|
|
162
|
+
|
|
163
|
+
Invalid filters are rejected (never clamped or ignored) with
|
|
164
|
+
`400 invalid_filter`, naming `parameter` and `allowed`: `status` must be a
|
|
165
|
+
proposal status, `limit` an integer 1–200, `offset` an integer ≥ 0, and
|
|
166
|
+
`mine` one of true/false/1/0. MCP argument errors use the same JSON body
|
|
167
|
+
(`invalid_filter` for list filters, `invalid_arguments` otherwise).
|
|
168
|
+
|
|
169
|
+
On submit and on **get** of a **pending** proposal, responses may include
|
|
170
|
+
`conflictingProposals` (same-`expectedRev` peers or true node/field overlaps)
|
|
171
|
+
with optional `overlappingNodeIds` / `overlappingFields`. List endpoints
|
|
172
|
+
omit `conflictingProposals`. Submit, get, and list responses include
|
|
173
|
+
`autoClosedCount` when that request auto-closed pending proposals as
|
|
174
|
+
`expired` (omitted when zero).
|
|
175
|
+
|
|
176
|
+
Validation: a single error returns its own code (e.g. `404 node_not_found`);
|
|
177
|
+
several errors return `400 validation_failed` with message
|
|
178
|
+
`N validation errors` and every entry in `errors[]`. Duplicate node ids in a
|
|
179
|
+
full tab or batch return `duplicate_node_id`.
|
|
180
|
+
|
|
181
|
+
Pending proposals whose `expectedRev` no longer matches the live flow rev
|
|
182
|
+
are auto-closed as `expired`. When a proposal is approved, other pending
|
|
183
|
+
proposals on the same flow become `superseded`. Closed proposals are never
|
|
184
|
+
conflicts.
|
|
185
|
+
|
|
186
|
+
## Withdraw
|
|
187
|
+
|
|
188
|
+
The creating access link may withdraw its own pending proposal
|
|
189
|
+
(`POST /proposals/{id}/withdraw`, GET `…/proposals/{id}/withdraw?confirm=1`,
|
|
190
|
+
or MCP `withdraw_proposal`). GET withdraw requires `confirm=1` so prefetchers
|
|
191
|
+
cannot trigger it. Status becomes `withdrawn` (separate from admin
|
|
192
|
+
`rejected`).
|
|
193
|
+
|
|
194
|
+
## Known issues
|
|
195
|
+
|
|
196
|
+
- **Rate limits are per token (and per IP).** Each access-link token has its
|
|
197
|
+
own token bucket. Several tokens can each use the full per-token limit at
|
|
198
|
+
the same time; the shared IP bucket still applies across them.
|
|
199
|
+
- **`RateLimit-*` headers describe the per-token bucket** on requests that
|
|
200
|
+
carry a Bearer or path token (`RateLimit-Policy: 300;w=60` by default):
|
|
201
|
+
that is the bucket being consumed and normally the stricter one. Requests
|
|
202
|
+
without a token report the per-IP bucket (`600;w=60`). Both buckets are
|
|
203
|
+
enforced; either can return `429 rate_limited`.
|
|
204
|
+
|
|
205
|
+
contact:
|
|
206
|
+
name: com55
|
|
207
|
+
url: https://github.com/com55/node-red-ai-access
|
|
208
|
+
license:
|
|
209
|
+
name: MIT
|
|
210
|
+
url: https://opensource.org/licenses/MIT
|
|
211
|
+
|
|
212
|
+
servers:
|
|
213
|
+
- url: /mcp/api/v1
|
|
214
|
+
description: Relative to Node-RED httpAdminRoot (default `/`)
|
|
215
|
+
|
|
216
|
+
tags:
|
|
217
|
+
- name: Status
|
|
218
|
+
- name: Access links
|
|
219
|
+
- name: Flows
|
|
220
|
+
- name: Node types
|
|
221
|
+
- name: Proposals
|
|
222
|
+
- name: Audit log
|
|
223
|
+
- name: Spec
|
|
224
|
+
|
|
225
|
+
paths:
|
|
226
|
+
/:
|
|
227
|
+
get:
|
|
228
|
+
tags: [Spec]
|
|
229
|
+
summary: Discovery index (no auth)
|
|
230
|
+
description: |
|
|
231
|
+
Unauthenticated JSON index listing auth scheme, OpenAPI URL, MCP
|
|
232
|
+
endpoint, and available REST paths. Intended for plain HTTP AI clients.
|
|
233
|
+
Advertises that token minting is admin `POST /links` only — not under
|
|
234
|
+
GET `/mcp/a/<token>`.
|
|
235
|
+
operationId: getDiscoveryIndex
|
|
236
|
+
security: []
|
|
237
|
+
responses:
|
|
238
|
+
'200':
|
|
239
|
+
description: Discovery payload
|
|
240
|
+
content:
|
|
241
|
+
application/json:
|
|
242
|
+
schema:
|
|
243
|
+
type: object
|
|
244
|
+
required: [ok, auth, openapi, mcp, endpoints]
|
|
245
|
+
properties:
|
|
246
|
+
ok:
|
|
247
|
+
type: boolean
|
|
248
|
+
name:
|
|
249
|
+
type: string
|
|
250
|
+
version:
|
|
251
|
+
type: string
|
|
252
|
+
auth:
|
|
253
|
+
type: object
|
|
254
|
+
required: [scheme, header]
|
|
255
|
+
properties:
|
|
256
|
+
scheme:
|
|
257
|
+
type: string
|
|
258
|
+
const: Bearer
|
|
259
|
+
header:
|
|
260
|
+
type: string
|
|
261
|
+
example: 'Authorization: Bearer <token>'
|
|
262
|
+
description:
|
|
263
|
+
type: string
|
|
264
|
+
tokenKinds:
|
|
265
|
+
type: array
|
|
266
|
+
items:
|
|
267
|
+
$ref: '#/components/schemas/AccessLinkKind'
|
|
268
|
+
mint:
|
|
269
|
+
type: object
|
|
270
|
+
description: Where admins create tokens (not under get-link)
|
|
271
|
+
properties:
|
|
272
|
+
path:
|
|
273
|
+
type: string
|
|
274
|
+
const: /links
|
|
275
|
+
method:
|
|
276
|
+
type: string
|
|
277
|
+
const: POST
|
|
278
|
+
auth:
|
|
279
|
+
type: string
|
|
280
|
+
const: admin
|
|
281
|
+
note:
|
|
282
|
+
type: string
|
|
283
|
+
openapi:
|
|
284
|
+
type: string
|
|
285
|
+
openapiJson:
|
|
286
|
+
type: string
|
|
287
|
+
mcp:
|
|
288
|
+
type: string
|
|
289
|
+
apiBase:
|
|
290
|
+
type: string
|
|
291
|
+
getLink:
|
|
292
|
+
type: object
|
|
293
|
+
description: |
|
|
294
|
+
Self-contained GET surface metadata. `canMintTokens` is
|
|
295
|
+
always false — no link-create under `/mcp/a`.
|
|
296
|
+
properties:
|
|
297
|
+
base:
|
|
298
|
+
type: string
|
|
299
|
+
canMintTokens:
|
|
300
|
+
type: boolean
|
|
301
|
+
const: false
|
|
302
|
+
note:
|
|
303
|
+
type: string
|
|
304
|
+
endpoints:
|
|
305
|
+
type: array
|
|
306
|
+
items:
|
|
307
|
+
type: object
|
|
308
|
+
required: [method, path, auth, summary]
|
|
309
|
+
properties:
|
|
310
|
+
method:
|
|
311
|
+
type: string
|
|
312
|
+
path:
|
|
313
|
+
type: string
|
|
314
|
+
auth:
|
|
315
|
+
type: string
|
|
316
|
+
summary:
|
|
317
|
+
type: string
|
|
318
|
+
|
|
319
|
+
/openapi.yaml:
|
|
320
|
+
get:
|
|
321
|
+
tags: [Spec]
|
|
322
|
+
summary: OpenAPI document (YAML)
|
|
323
|
+
operationId: getOpenApiYaml
|
|
324
|
+
security: []
|
|
325
|
+
responses:
|
|
326
|
+
'200':
|
|
327
|
+
description: OpenAPI 3.1 YAML for this API
|
|
328
|
+
content:
|
|
329
|
+
application/yaml:
|
|
330
|
+
schema:
|
|
331
|
+
type: string
|
|
332
|
+
|
|
333
|
+
/openapi.json:
|
|
334
|
+
get:
|
|
335
|
+
tags: [Spec]
|
|
336
|
+
summary: OpenAPI document (JSON)
|
|
337
|
+
description: The same specification as `/openapi.yaml`, converted to JSON.
|
|
338
|
+
operationId: getOpenApiJson
|
|
339
|
+
security: []
|
|
340
|
+
responses:
|
|
341
|
+
'200':
|
|
342
|
+
description: OpenAPI 3.1 JSON for this API
|
|
343
|
+
content:
|
|
344
|
+
application/json:
|
|
345
|
+
schema:
|
|
346
|
+
type: object
|
|
347
|
+
|
|
348
|
+
/status:
|
|
349
|
+
get:
|
|
350
|
+
tags: [Status]
|
|
351
|
+
summary: Plugin status
|
|
352
|
+
description: |
|
|
353
|
+
Accepts an **admin session** or a valid **access-link Bearer**.
|
|
354
|
+
Access-link callers receive `permission: null` and a `caller` object
|
|
355
|
+
with link label, expiry, permissions, and base URLs.
|
|
356
|
+
operationId: getStatus
|
|
357
|
+
security:
|
|
358
|
+
- AdminSession: []
|
|
359
|
+
- AccessLinkBearer: []
|
|
360
|
+
responses:
|
|
361
|
+
'200':
|
|
362
|
+
description: Status payload
|
|
363
|
+
content:
|
|
364
|
+
application/json:
|
|
365
|
+
schema:
|
|
366
|
+
$ref: '#/components/schemas/Status'
|
|
367
|
+
'401':
|
|
368
|
+
$ref: '#/components/responses/Unauthorized'
|
|
369
|
+
|
|
370
|
+
/settings:
|
|
371
|
+
get:
|
|
372
|
+
tags: [Status]
|
|
373
|
+
summary: Get effective AI Access settings
|
|
374
|
+
description: |
|
|
375
|
+
Returns effective settings with precedence metadata
|
|
376
|
+
(`disk` → `settings.js` → built-in defaults). Admin `read` or `*`.
|
|
377
|
+
`read` users get `canEdit: false`; `*` can PUT.
|
|
378
|
+
operationId: getAiAccessSettings
|
|
379
|
+
security:
|
|
380
|
+
- AdminSession: []
|
|
381
|
+
responses:
|
|
382
|
+
'200':
|
|
383
|
+
description: Effective settings
|
|
384
|
+
content:
|
|
385
|
+
application/json:
|
|
386
|
+
schema:
|
|
387
|
+
$ref: '#/components/schemas/AiAccessSettingsResponse'
|
|
388
|
+
'401':
|
|
389
|
+
$ref: '#/components/responses/Unauthorized'
|
|
390
|
+
put:
|
|
391
|
+
tags: [Status]
|
|
392
|
+
summary: Update AI Access settings (persist to disk)
|
|
393
|
+
description: |
|
|
394
|
+
Requires `*`. Writes `<userDir>/.ai-access/settings.json` and applies
|
|
395
|
+
immediately (no restart). Partial body merges into existing disk
|
|
396
|
+
overrides.
|
|
397
|
+
operationId: putAiAccessSettings
|
|
398
|
+
security:
|
|
399
|
+
- AdminSession: []
|
|
400
|
+
requestBody:
|
|
401
|
+
required: true
|
|
402
|
+
content:
|
|
403
|
+
application/json:
|
|
404
|
+
schema:
|
|
405
|
+
$ref: '#/components/schemas/AiAccessSettingsPatch'
|
|
406
|
+
responses:
|
|
407
|
+
'200':
|
|
408
|
+
description: Updated effective settings
|
|
409
|
+
content:
|
|
410
|
+
application/json:
|
|
411
|
+
schema:
|
|
412
|
+
$ref: '#/components/schemas/AiAccessSettingsResponse'
|
|
413
|
+
'400':
|
|
414
|
+
$ref: '#/components/responses/BadRequest'
|
|
415
|
+
'401':
|
|
416
|
+
$ref: '#/components/responses/Unauthorized'
|
|
417
|
+
'403':
|
|
418
|
+
$ref: '#/components/responses/Forbidden'
|
|
419
|
+
'413':
|
|
420
|
+
$ref: '#/components/responses/PayloadTooLarge'
|
|
421
|
+
|
|
422
|
+
/links:
|
|
423
|
+
get:
|
|
424
|
+
tags: [Access links]
|
|
425
|
+
summary: List active access links
|
|
426
|
+
description: Requires full admin permission (`*`).
|
|
427
|
+
operationId: listLinks
|
|
428
|
+
security:
|
|
429
|
+
- AdminSession: []
|
|
430
|
+
responses:
|
|
431
|
+
'200':
|
|
432
|
+
description: Active links (no raw tokens)
|
|
433
|
+
content:
|
|
434
|
+
application/json:
|
|
435
|
+
schema:
|
|
436
|
+
type: object
|
|
437
|
+
required: [links]
|
|
438
|
+
properties:
|
|
439
|
+
links:
|
|
440
|
+
type: array
|
|
441
|
+
items:
|
|
442
|
+
$ref: '#/components/schemas/AccessLinkPublic'
|
|
443
|
+
'401':
|
|
444
|
+
$ref: '#/components/responses/Unauthorized'
|
|
445
|
+
'403':
|
|
446
|
+
$ref: '#/components/responses/Forbidden'
|
|
447
|
+
post:
|
|
448
|
+
tags: [Access links]
|
|
449
|
+
summary: Create an access link
|
|
450
|
+
description: |
|
|
451
|
+
Requires full admin permission (`*`). This is the **only** link-create
|
|
452
|
+
operation — GET `/mcp/a/<token>/…` has no mint/create-link route.
|
|
453
|
+
The raw `token` is returned **once** in this response. For
|
|
454
|
+
`kind=get_link`, a self-contained `url` is included for GET-only
|
|
455
|
+
clients at `/mcp/a/<token>`. For `rest` / `mcp`, `url` is omitted
|
|
456
|
+
(use the Bearer token on the matching surface). Default TTL is
|
|
457
|
+
14400 seconds (4h). Caps: `get_link` ≤ 24h; `rest` / `mcp` ≤ 30d
|
|
458
|
+
(over-cap → **400**). Max active links defaults to 10
|
|
459
|
+
(`aiAccess.maxActiveLinks`).
|
|
460
|
+
operationId: createLink
|
|
461
|
+
security:
|
|
462
|
+
- AdminSession: []
|
|
463
|
+
requestBody:
|
|
464
|
+
required: false
|
|
465
|
+
content:
|
|
466
|
+
application/json:
|
|
467
|
+
schema:
|
|
468
|
+
$ref: '#/components/schemas/CreateLinkRequest'
|
|
469
|
+
responses:
|
|
470
|
+
'201':
|
|
471
|
+
description: Link created (includes token)
|
|
472
|
+
content:
|
|
473
|
+
application/json:
|
|
474
|
+
schema:
|
|
475
|
+
$ref: '#/components/schemas/AccessLinkCreated'
|
|
476
|
+
'400':
|
|
477
|
+
description: |
|
|
478
|
+
Invalid body — including unknown `kind`
|
|
479
|
+
(`code: invalid_link_kind`).
|
|
480
|
+
content:
|
|
481
|
+
application/json:
|
|
482
|
+
schema:
|
|
483
|
+
oneOf:
|
|
484
|
+
- $ref: '#/components/schemas/InvalidLinkKindError'
|
|
485
|
+
- $ref: '#/components/schemas/Error'
|
|
486
|
+
'401':
|
|
487
|
+
$ref: '#/components/responses/Unauthorized'
|
|
488
|
+
'403':
|
|
489
|
+
$ref: '#/components/responses/Forbidden'
|
|
490
|
+
'409':
|
|
491
|
+
description: Active link limit reached
|
|
492
|
+
content:
|
|
493
|
+
application/json:
|
|
494
|
+
schema:
|
|
495
|
+
$ref: '#/components/schemas/Error'
|
|
496
|
+
|
|
497
|
+
/links/{id}:
|
|
498
|
+
delete:
|
|
499
|
+
tags: [Access links]
|
|
500
|
+
summary: Revoke an access link
|
|
501
|
+
operationId: revokeLink
|
|
502
|
+
security:
|
|
503
|
+
- AdminSession: []
|
|
504
|
+
parameters:
|
|
505
|
+
- $ref: '#/components/parameters/LinkId'
|
|
506
|
+
responses:
|
|
507
|
+
'200':
|
|
508
|
+
description: Link revoked
|
|
509
|
+
content:
|
|
510
|
+
application/json:
|
|
511
|
+
schema:
|
|
512
|
+
type: object
|
|
513
|
+
required: [revoked, link]
|
|
514
|
+
properties:
|
|
515
|
+
revoked:
|
|
516
|
+
type: boolean
|
|
517
|
+
const: true
|
|
518
|
+
link:
|
|
519
|
+
$ref: '#/components/schemas/AccessLinkPublic'
|
|
520
|
+
'401':
|
|
521
|
+
$ref: '#/components/responses/Unauthorized'
|
|
522
|
+
'403':
|
|
523
|
+
$ref: '#/components/responses/Forbidden'
|
|
524
|
+
'404':
|
|
525
|
+
$ref: '#/components/responses/NotFound'
|
|
526
|
+
|
|
527
|
+
/flows:
|
|
528
|
+
get:
|
|
529
|
+
tags: [Flows]
|
|
530
|
+
summary: List flows with revision
|
|
531
|
+
description: |
|
|
532
|
+
AI clients and admins with at least `read` may call this.
|
|
533
|
+
Use `rev` as `expectedRev` when submitting a `replace_flow` proposal.
|
|
534
|
+
operationId: listFlows
|
|
535
|
+
security:
|
|
536
|
+
- AccessLinkBearer: []
|
|
537
|
+
- AdminSession: []
|
|
538
|
+
responses:
|
|
539
|
+
'200':
|
|
540
|
+
description: Flat flows array plus global revision
|
|
541
|
+
content:
|
|
542
|
+
application/json:
|
|
543
|
+
schema:
|
|
544
|
+
$ref: '#/components/schemas/FlowsSnapshot'
|
|
545
|
+
'401':
|
|
546
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
547
|
+
'403':
|
|
548
|
+
$ref: '#/components/responses/Forbidden'
|
|
549
|
+
'502':
|
|
550
|
+
$ref: '#/components/responses/BadGateway'
|
|
551
|
+
|
|
552
|
+
/flows/{id}:
|
|
553
|
+
get:
|
|
554
|
+
tags: [Flows]
|
|
555
|
+
summary: Get one flow (tab) with revision
|
|
556
|
+
description: |
|
|
557
|
+
Returns the nested tab shape used as the basis for a `replace_flow`
|
|
558
|
+
proposal payload. Pass `summary=1` for a lightweight node list
|
|
559
|
+
(`id`/`type`/`name`/`wires` + per-node `bytes`, no field contents).
|
|
560
|
+
Always re-read before proposing so `expectedRev` matches the current
|
|
561
|
+
revision.
|
|
562
|
+
operationId: getFlow
|
|
563
|
+
security:
|
|
564
|
+
- AccessLinkBearer: []
|
|
565
|
+
- AdminSession: []
|
|
566
|
+
parameters:
|
|
567
|
+
- $ref: '#/components/parameters/FlowId'
|
|
568
|
+
- name: summary
|
|
569
|
+
in: query
|
|
570
|
+
required: false
|
|
571
|
+
description: When `1`/`true`/`yes`, return a node summary without field bodies
|
|
572
|
+
schema:
|
|
573
|
+
type: string
|
|
574
|
+
enum: ['1', 'true', 'yes']
|
|
575
|
+
responses:
|
|
576
|
+
'200':
|
|
577
|
+
description: Nested flow tab plus revision, or summary shape when `summary=1`
|
|
578
|
+
content:
|
|
579
|
+
application/json:
|
|
580
|
+
schema:
|
|
581
|
+
oneOf:
|
|
582
|
+
- $ref: '#/components/schemas/FlowSnapshot'
|
|
583
|
+
- $ref: '#/components/schemas/FlowSummary'
|
|
584
|
+
'400':
|
|
585
|
+
$ref: '#/components/responses/BadRequest'
|
|
586
|
+
'401':
|
|
587
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
588
|
+
'403':
|
|
589
|
+
$ref: '#/components/responses/Forbidden'
|
|
590
|
+
'404':
|
|
591
|
+
$ref: '#/components/responses/NotFound'
|
|
592
|
+
'502':
|
|
593
|
+
$ref: '#/components/responses/BadGateway'
|
|
594
|
+
|
|
595
|
+
/nodes/{id}:
|
|
596
|
+
get:
|
|
597
|
+
tags: [Flows]
|
|
598
|
+
summary: Get one full node
|
|
599
|
+
description: |
|
|
600
|
+
Returns one node plus owning `flowId` when known. Optional `flow`
|
|
601
|
+
scopes the lookup to a tab.
|
|
602
|
+
operationId: getNode
|
|
603
|
+
security:
|
|
604
|
+
- AccessLinkBearer: []
|
|
605
|
+
- AdminSession: []
|
|
606
|
+
parameters:
|
|
607
|
+
- name: id
|
|
608
|
+
in: path
|
|
609
|
+
required: true
|
|
610
|
+
schema:
|
|
611
|
+
type: string
|
|
612
|
+
- name: flow
|
|
613
|
+
in: query
|
|
614
|
+
required: false
|
|
615
|
+
description: Optional tab id to scope the lookup
|
|
616
|
+
schema:
|
|
617
|
+
type: string
|
|
618
|
+
responses:
|
|
619
|
+
'200':
|
|
620
|
+
description: One node with revision
|
|
621
|
+
content:
|
|
622
|
+
application/json:
|
|
623
|
+
schema:
|
|
624
|
+
$ref: '#/components/schemas/NodeSnapshot'
|
|
625
|
+
'400':
|
|
626
|
+
$ref: '#/components/responses/BadRequest'
|
|
627
|
+
'401':
|
|
628
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
629
|
+
'403':
|
|
630
|
+
$ref: '#/components/responses/Forbidden'
|
|
631
|
+
'502':
|
|
632
|
+
$ref: '#/components/responses/BadGateway'
|
|
633
|
+
|
|
634
|
+
/nodes/{id}/field/{name}:
|
|
635
|
+
get:
|
|
636
|
+
tags: [Flows]
|
|
637
|
+
summary: Get one node string field
|
|
638
|
+
description: |
|
|
639
|
+
Returns one string field. Optional `lines=A-B` (1-based inclusive)
|
|
640
|
+
returns a slice; response always includes `totalLines` and `bytes`.
|
|
641
|
+
operationId: getNodeField
|
|
642
|
+
security:
|
|
643
|
+
- AccessLinkBearer: []
|
|
644
|
+
- AdminSession: []
|
|
645
|
+
parameters:
|
|
646
|
+
- name: id
|
|
647
|
+
in: path
|
|
648
|
+
required: true
|
|
649
|
+
schema:
|
|
650
|
+
type: string
|
|
651
|
+
- name: name
|
|
652
|
+
in: path
|
|
653
|
+
required: true
|
|
654
|
+
schema:
|
|
655
|
+
type: string
|
|
656
|
+
- name: lines
|
|
657
|
+
in: query
|
|
658
|
+
required: false
|
|
659
|
+
description: Optional 1-based line range `A` or `A-B`
|
|
660
|
+
schema:
|
|
661
|
+
type: string
|
|
662
|
+
pattern: '^[0-9]+(-[0-9]+)?$'
|
|
663
|
+
- name: flow
|
|
664
|
+
in: query
|
|
665
|
+
required: false
|
|
666
|
+
schema:
|
|
667
|
+
type: string
|
|
668
|
+
responses:
|
|
669
|
+
'200':
|
|
670
|
+
description: Field value (possibly sliced) with metadata
|
|
671
|
+
content:
|
|
672
|
+
application/json:
|
|
673
|
+
schema:
|
|
674
|
+
$ref: '#/components/schemas/NodeFieldSnapshot'
|
|
675
|
+
'400':
|
|
676
|
+
$ref: '#/components/responses/BadRequest'
|
|
677
|
+
'401':
|
|
678
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
679
|
+
'403':
|
|
680
|
+
$ref: '#/components/responses/Forbidden'
|
|
681
|
+
'502':
|
|
682
|
+
$ref: '#/components/responses/BadGateway'
|
|
683
|
+
|
|
684
|
+
/search:
|
|
685
|
+
get:
|
|
686
|
+
tags: [Flows]
|
|
687
|
+
summary: Text search across string fields
|
|
688
|
+
description: |
|
|
689
|
+
Case-sensitive substring search. Hits include node id, field, line
|
|
690
|
+
numbers, and a short snippet. Results are capped (default 50);
|
|
691
|
+
`truncated:true` when the cap is hit. Optional `flow` scopes to one tab.
|
|
692
|
+
operationId: searchFlows
|
|
693
|
+
security:
|
|
694
|
+
- AccessLinkBearer: []
|
|
695
|
+
- AdminSession: []
|
|
696
|
+
parameters:
|
|
697
|
+
- name: q
|
|
698
|
+
in: query
|
|
699
|
+
required: true
|
|
700
|
+
schema:
|
|
701
|
+
type: string
|
|
702
|
+
minLength: 1
|
|
703
|
+
- name: flow
|
|
704
|
+
in: query
|
|
705
|
+
required: false
|
|
706
|
+
schema:
|
|
707
|
+
type: string
|
|
708
|
+
responses:
|
|
709
|
+
'200':
|
|
710
|
+
description: Search hits
|
|
711
|
+
content:
|
|
712
|
+
application/json:
|
|
713
|
+
schema:
|
|
714
|
+
$ref: '#/components/schemas/SearchResult'
|
|
715
|
+
'400':
|
|
716
|
+
$ref: '#/components/responses/BadRequest'
|
|
717
|
+
'401':
|
|
718
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
719
|
+
'403':
|
|
720
|
+
$ref: '#/components/responses/Forbidden'
|
|
721
|
+
'502':
|
|
722
|
+
$ref: '#/components/responses/BadGateway'
|
|
723
|
+
|
|
724
|
+
/node-types:
|
|
725
|
+
get:
|
|
726
|
+
tags: [Node types]
|
|
727
|
+
summary: List installed palette node types
|
|
728
|
+
description: |
|
|
729
|
+
Returns installed Node-RED palette types (`type`, `module`, optional
|
|
730
|
+
`category`, `description`, `enabled`, `version`, `setId`). Optional
|
|
731
|
+
`q` filters type/module/category/description (case-insensitive).
|
|
732
|
+
Results are capped; response includes `total` and `truncated`.
|
|
733
|
+
operationId: listNodeTypes
|
|
734
|
+
security:
|
|
735
|
+
- AccessLinkBearer: []
|
|
736
|
+
- AdminSession: []
|
|
737
|
+
parameters:
|
|
738
|
+
- name: q
|
|
739
|
+
in: query
|
|
740
|
+
required: false
|
|
741
|
+
schema:
|
|
742
|
+
type: string
|
|
743
|
+
responses:
|
|
744
|
+
'200':
|
|
745
|
+
description: Installed node types
|
|
746
|
+
content:
|
|
747
|
+
application/json:
|
|
748
|
+
schema:
|
|
749
|
+
$ref: '#/components/schemas/NodeTypeListResult'
|
|
750
|
+
'401':
|
|
751
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
752
|
+
'403':
|
|
753
|
+
$ref: '#/components/responses/Forbidden'
|
|
754
|
+
'502':
|
|
755
|
+
$ref: '#/components/responses/BadGateway'
|
|
756
|
+
|
|
757
|
+
/node-types/{type}:
|
|
758
|
+
get:
|
|
759
|
+
tags: [Node types]
|
|
760
|
+
summary: Get one installed node type
|
|
761
|
+
description: |
|
|
762
|
+
Detail for one type: `properties`/`defaults`, `inputs`/`outputs`,
|
|
763
|
+
`requiredConfigNodeTypes`, short `help`, plus `category`/`module`/`color`
|
|
764
|
+
when available. **404** `node_type_not_found` when unknown (may include
|
|
765
|
+
`nearest` suggestions).
|
|
766
|
+
operationId: getNodeType
|
|
767
|
+
security:
|
|
768
|
+
- AccessLinkBearer: []
|
|
769
|
+
- AdminSession: []
|
|
770
|
+
parameters:
|
|
771
|
+
- name: type
|
|
772
|
+
in: path
|
|
773
|
+
required: true
|
|
774
|
+
schema:
|
|
775
|
+
type: string
|
|
776
|
+
responses:
|
|
777
|
+
'200':
|
|
778
|
+
description: Node type detail
|
|
779
|
+
content:
|
|
780
|
+
application/json:
|
|
781
|
+
schema:
|
|
782
|
+
$ref: '#/components/schemas/NodeTypeDetail'
|
|
783
|
+
'401':
|
|
784
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
785
|
+
'403':
|
|
786
|
+
$ref: '#/components/responses/Forbidden'
|
|
787
|
+
'404':
|
|
788
|
+
$ref: '#/components/responses/NotFound'
|
|
789
|
+
'502':
|
|
790
|
+
$ref: '#/components/responses/BadGateway'
|
|
791
|
+
|
|
792
|
+
/palette/search:
|
|
793
|
+
get:
|
|
794
|
+
tags: [Node types]
|
|
795
|
+
summary: Search Node-RED catalogue (recommend only)
|
|
796
|
+
description: |
|
|
797
|
+
Searches `catalogue.nodered.org` (cached ~1h). Returns modules with
|
|
798
|
+
`installed` boolean. **Recommend only — never installs.** Ask the user
|
|
799
|
+
to install via Manage palette, then recheck `/node-types`. Response
|
|
800
|
+
always includes a `hint` stating this.
|
|
801
|
+
operationId: searchPalette
|
|
802
|
+
security:
|
|
803
|
+
- AccessLinkBearer: []
|
|
804
|
+
- AdminSession: []
|
|
805
|
+
parameters:
|
|
806
|
+
- name: q
|
|
807
|
+
in: query
|
|
808
|
+
required: true
|
|
809
|
+
schema:
|
|
810
|
+
type: string
|
|
811
|
+
minLength: 1
|
|
812
|
+
responses:
|
|
813
|
+
'200':
|
|
814
|
+
description: Catalogue search hits
|
|
815
|
+
content:
|
|
816
|
+
application/json:
|
|
817
|
+
schema:
|
|
818
|
+
$ref: '#/components/schemas/PaletteSearchResult'
|
|
819
|
+
'400':
|
|
820
|
+
$ref: '#/components/responses/BadRequest'
|
|
821
|
+
'401':
|
|
822
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
823
|
+
'403':
|
|
824
|
+
$ref: '#/components/responses/Forbidden'
|
|
825
|
+
'502':
|
|
826
|
+
$ref: '#/components/responses/BadGateway'
|
|
827
|
+
|
|
828
|
+
/proposals:
|
|
829
|
+
get:
|
|
830
|
+
tags: [Proposals]
|
|
831
|
+
summary: List proposals
|
|
832
|
+
description: |
|
|
833
|
+
Admins see all proposals. Access-link clients also see **all** proposals,
|
|
834
|
+
each marked `mine: true|false`. Filter with `?mine=true` (own only) or
|
|
835
|
+
`?mine=false` (only other links' proposals), `?status=` (one or more
|
|
836
|
+
statuses, comma-separated `?status=pending,superseded` or repeated
|
|
837
|
+
`?status=pending&status=expired`; any unknown value is `invalid_filter`),
|
|
838
|
+
and/or `?flowId=`. Other filter parameters may appear at most once. Paginate with `?limit=` (1–200) and `?offset=`; responses
|
|
839
|
+
always include `total` (count after filters, before pagination). Pending
|
|
840
|
+
items may include `conflictingProposals` on get/submit (not on list).
|
|
841
|
+
operationId: listProposals
|
|
842
|
+
security:
|
|
843
|
+
- AccessLinkBearer: []
|
|
844
|
+
- AdminSession: []
|
|
845
|
+
parameters:
|
|
846
|
+
- name: mine
|
|
847
|
+
in: query
|
|
848
|
+
required: false
|
|
849
|
+
schema:
|
|
850
|
+
type: boolean
|
|
851
|
+
description: >-
|
|
852
|
+
Access-link only. true (or 1) returns only this token's proposals;
|
|
853
|
+
false (or 0) returns only proposals created by other links.
|
|
854
|
+
- name: status
|
|
855
|
+
in: query
|
|
856
|
+
required: false
|
|
857
|
+
style: form
|
|
858
|
+
explode: true
|
|
859
|
+
schema:
|
|
860
|
+
type: array
|
|
861
|
+
items:
|
|
862
|
+
$ref: '#/components/schemas/ProposalStatus'
|
|
863
|
+
description: >-
|
|
864
|
+
One or more statuses. Repeat the parameter or pass a comma-separated
|
|
865
|
+
list. Any invalid value returns 400 invalid_filter.
|
|
866
|
+
- name: flowId
|
|
867
|
+
in: query
|
|
868
|
+
required: false
|
|
869
|
+
schema:
|
|
870
|
+
type: string
|
|
871
|
+
description: Filter by payload.flowId
|
|
872
|
+
- name: limit
|
|
873
|
+
in: query
|
|
874
|
+
required: false
|
|
875
|
+
schema:
|
|
876
|
+
type: integer
|
|
877
|
+
minimum: 1
|
|
878
|
+
maximum: 200
|
|
879
|
+
description: Page size. Omit to return all matching proposals.
|
|
880
|
+
- name: offset
|
|
881
|
+
in: query
|
|
882
|
+
required: false
|
|
883
|
+
schema:
|
|
884
|
+
type: integer
|
|
885
|
+
minimum: 0
|
|
886
|
+
default: 0
|
|
887
|
+
description: Skip this many filtered results
|
|
888
|
+
- $ref: '#/components/parameters/ProposalInclude'
|
|
889
|
+
- $ref: '#/components/parameters/ProposalIncludePayload'
|
|
890
|
+
responses:
|
|
891
|
+
'200':
|
|
892
|
+
description: Proposal list
|
|
893
|
+
content:
|
|
894
|
+
application/json:
|
|
895
|
+
schema:
|
|
896
|
+
type: object
|
|
897
|
+
required: [proposals, total]
|
|
898
|
+
properties:
|
|
899
|
+
proposals:
|
|
900
|
+
type: array
|
|
901
|
+
items:
|
|
902
|
+
$ref: '#/components/schemas/Proposal'
|
|
903
|
+
total:
|
|
904
|
+
type: integer
|
|
905
|
+
description: Count after filters, before limit/offset
|
|
906
|
+
limit:
|
|
907
|
+
type: integer
|
|
908
|
+
description: Echo of requested limit when pagination is used
|
|
909
|
+
offset:
|
|
910
|
+
type: integer
|
|
911
|
+
description: Echo of offset when pagination is used
|
|
912
|
+
'401':
|
|
913
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
914
|
+
'403':
|
|
915
|
+
$ref: '#/components/responses/Forbidden'
|
|
916
|
+
post:
|
|
917
|
+
tags: [Proposals]
|
|
918
|
+
summary: Create a replace_flow proposal
|
|
919
|
+
description: |
|
|
920
|
+
**Access-link Bearer only.** Submits a proposed replacement for one
|
|
921
|
+
flow tab. Does not mutate Node-RED until an admin with `*` approves.
|
|
922
|
+
|
|
923
|
+
Include `expectedRev` (preferred; `expected_rev` accepted as alias)
|
|
924
|
+
from the latest `GET /flows` or `GET /flows/{id}` response. The live
|
|
925
|
+
flow revision is checked **at submit time** (**409**
|
|
926
|
+
`version_mismatch` if stale). On approve, a stale revision marks the
|
|
927
|
+
proposal `rejected` with a resubmit message (no deploy).
|
|
928
|
+
|
|
929
|
+
Optional `title` (≤80 chars) is stored as a human-friendly label; when
|
|
930
|
+
omitted it is auto-generated from the change set. Responses always
|
|
931
|
+
include `title`, `displayName` (`title ·` server-local submitted time
|
|
932
|
+
with offset), absolute `reviewUrl`, and relative `reviewPath`. AI clients
|
|
933
|
+
should cite `displayName` + `reviewUrl` to users (not the raw id).
|
|
934
|
+
|
|
935
|
+
`payload.flow` must be a tab object (`id`, `type`, `label?`, `nodes[]`,
|
|
936
|
+
optional `configs[]`). Payloads over 1_500_000 bytes return **413**.
|
|
937
|
+
Unknown palette `node.type` values return **400** `unknown_node_type`
|
|
938
|
+
with `nearest` suggestions — call `/node-types` or `/palette/search`
|
|
939
|
+
and ask the user to install via Manage palette (this API never installs).
|
|
940
|
+
|
|
941
|
+
Responses are **lean** by default (no `payload` / `baseFlow`). Pass
|
|
942
|
+
`?include=payload,baseFlow` or `?include_payload=true` to include them
|
|
943
|
+
(same as MCP `include_payload`).
|
|
944
|
+
operationId: createProposal
|
|
945
|
+
security:
|
|
946
|
+
- AccessLinkBearer: []
|
|
947
|
+
parameters:
|
|
948
|
+
- $ref: '#/components/parameters/ProposalInclude'
|
|
949
|
+
- $ref: '#/components/parameters/ProposalIncludePayload'
|
|
950
|
+
requestBody:
|
|
951
|
+
required: true
|
|
952
|
+
content:
|
|
953
|
+
application/json:
|
|
954
|
+
schema:
|
|
955
|
+
$ref: '#/components/schemas/CreateProposalRequest'
|
|
956
|
+
responses:
|
|
957
|
+
'201':
|
|
958
|
+
description: Proposal created (`pending`)
|
|
959
|
+
content:
|
|
960
|
+
application/json:
|
|
961
|
+
schema:
|
|
962
|
+
$ref: '#/components/schemas/Proposal'
|
|
963
|
+
'400':
|
|
964
|
+
$ref: '#/components/responses/BadRequest'
|
|
965
|
+
'401':
|
|
966
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
967
|
+
'403':
|
|
968
|
+
$ref: '#/components/responses/Forbidden'
|
|
969
|
+
'409':
|
|
970
|
+
$ref: '#/components/responses/VersionConflict'
|
|
971
|
+
'413':
|
|
972
|
+
$ref: '#/components/responses/PayloadTooLarge'
|
|
973
|
+
'415':
|
|
974
|
+
$ref: '#/components/responses/UnsupportedMediaType'
|
|
975
|
+
|
|
976
|
+
/proposals/{id}:
|
|
977
|
+
get:
|
|
978
|
+
tags: [Proposals]
|
|
979
|
+
summary: Get one proposal
|
|
980
|
+
operationId: getProposal
|
|
981
|
+
security:
|
|
982
|
+
- AccessLinkBearer: []
|
|
983
|
+
- AdminSession: []
|
|
984
|
+
parameters:
|
|
985
|
+
- $ref: '#/components/parameters/ProposalId'
|
|
986
|
+
- $ref: '#/components/parameters/ProposalInclude'
|
|
987
|
+
- $ref: '#/components/parameters/ProposalIncludePayload'
|
|
988
|
+
responses:
|
|
989
|
+
'200':
|
|
990
|
+
description: Proposal
|
|
991
|
+
content:
|
|
992
|
+
application/json:
|
|
993
|
+
schema:
|
|
994
|
+
$ref: '#/components/schemas/Proposal'
|
|
995
|
+
'400':
|
|
996
|
+
$ref: '#/components/responses/BadRequest'
|
|
997
|
+
'401':
|
|
998
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
999
|
+
'403':
|
|
1000
|
+
$ref: '#/components/responses/Forbidden'
|
|
1001
|
+
'404':
|
|
1002
|
+
$ref: '#/components/responses/NotFound'
|
|
1003
|
+
|
|
1004
|
+
/proposals/{id}/diff:
|
|
1005
|
+
get:
|
|
1006
|
+
tags: [Proposals]
|
|
1007
|
+
summary: Diff a proposal against the current flow
|
|
1008
|
+
description: |
|
|
1009
|
+
Compares the proposal payload to the **base flow snapshot** stored at
|
|
1010
|
+
submit (`baseFlow` at `expectedRev`) — what the AI actually changed —
|
|
1011
|
+
not the live editor flow. Returns counts of added / removed / modified
|
|
1012
|
+
nodes. Added/removed nodes are single presence blocks with properties
|
|
1013
|
+
(long text fields listed for expandable UI). Modified nodes include
|
|
1014
|
+
line-level diffs for text fields such as `func` and `template`.
|
|
1015
|
+
|
|
1016
|
+
`baseRevStale` is true only for **pending** proposals when
|
|
1017
|
+
`expectedRev` does not match the live revision (approve would fail).
|
|
1018
|
+
Approved/rejected/failed proposals never set `baseRevStale`. When
|
|
1019
|
+
pending and stale, `vsCurrent` optionally includes proposal-vs-live
|
|
1020
|
+
sections for review.
|
|
1021
|
+
|
|
1022
|
+
Admins with `read` can diff any proposal. Access-link clients can only
|
|
1023
|
+
diff proposals they created.
|
|
1024
|
+
operationId: getProposalDiff
|
|
1025
|
+
security:
|
|
1026
|
+
- AccessLinkBearer: []
|
|
1027
|
+
- AdminSession: []
|
|
1028
|
+
parameters:
|
|
1029
|
+
- $ref: '#/components/parameters/ProposalId'
|
|
1030
|
+
responses:
|
|
1031
|
+
'200':
|
|
1032
|
+
description: Proposal diff
|
|
1033
|
+
content:
|
|
1034
|
+
application/json:
|
|
1035
|
+
schema:
|
|
1036
|
+
$ref: '#/components/schemas/ProposalDiff'
|
|
1037
|
+
'400':
|
|
1038
|
+
$ref: '#/components/responses/BadRequest'
|
|
1039
|
+
'401':
|
|
1040
|
+
$ref: '#/components/responses/AccessLinkUnauthorized'
|
|
1041
|
+
'403':
|
|
1042
|
+
$ref: '#/components/responses/Forbidden'
|
|
1043
|
+
'404':
|
|
1044
|
+
$ref: '#/components/responses/NotFound'
|
|
1045
|
+
|
|
1046
|
+
/proposals/{id}/approve:
|
|
1047
|
+
post:
|
|
1048
|
+
tags: [Proposals]
|
|
1049
|
+
summary: Approve and deploy a proposal
|
|
1050
|
+
description: |
|
|
1051
|
+
Requires full admin permission (`*`). Deploys via the FlowAdapter.
|
|
1052
|
+
Optional body `deploymentType`: `nodes` (default from settings),
|
|
1053
|
+
`flows`, or `full`.
|
|
1054
|
+
|
|
1055
|
+
Revision conflict: proposal is marked `rejected` with a resubmit
|
|
1056
|
+
message (HTTP 200 with `status: rejected`). Other deploy errors mark
|
|
1057
|
+
it `failed`. Concurrent approve of the same id returns 409.
|
|
1058
|
+
operationId: approveProposal
|
|
1059
|
+
security:
|
|
1060
|
+
- AdminSession: []
|
|
1061
|
+
parameters:
|
|
1062
|
+
- $ref: '#/components/parameters/ProposalId'
|
|
1063
|
+
requestBody:
|
|
1064
|
+
required: false
|
|
1065
|
+
content:
|
|
1066
|
+
application/json:
|
|
1067
|
+
schema:
|
|
1068
|
+
$ref: '#/components/schemas/ApproveProposalRequest'
|
|
1069
|
+
responses:
|
|
1070
|
+
'200':
|
|
1071
|
+
description: Updated proposal (`approved`, `rejected`, or `failed`)
|
|
1072
|
+
content:
|
|
1073
|
+
application/json:
|
|
1074
|
+
schema:
|
|
1075
|
+
$ref: '#/components/schemas/Proposal'
|
|
1076
|
+
'400':
|
|
1077
|
+
$ref: '#/components/responses/BadRequest'
|
|
1078
|
+
'401':
|
|
1079
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1080
|
+
'403':
|
|
1081
|
+
$ref: '#/components/responses/Forbidden'
|
|
1082
|
+
'404':
|
|
1083
|
+
$ref: '#/components/responses/NotFound'
|
|
1084
|
+
'409':
|
|
1085
|
+
description: Not pending, or approve already in progress
|
|
1086
|
+
content:
|
|
1087
|
+
application/json:
|
|
1088
|
+
schema:
|
|
1089
|
+
$ref: '#/components/schemas/Error'
|
|
1090
|
+
'413':
|
|
1091
|
+
$ref: '#/components/responses/PayloadTooLarge'
|
|
1092
|
+
'415':
|
|
1093
|
+
$ref: '#/components/responses/UnsupportedMediaType'
|
|
1094
|
+
|
|
1095
|
+
/proposals/{id}/reject:
|
|
1096
|
+
post:
|
|
1097
|
+
tags: [Proposals]
|
|
1098
|
+
summary: Reject a proposal
|
|
1099
|
+
description: Requires full admin permission (`*`). Never mutates flows.
|
|
1100
|
+
operationId: rejectProposal
|
|
1101
|
+
security:
|
|
1102
|
+
- AdminSession: []
|
|
1103
|
+
parameters:
|
|
1104
|
+
- $ref: '#/components/parameters/ProposalId'
|
|
1105
|
+
requestBody:
|
|
1106
|
+
required: false
|
|
1107
|
+
content:
|
|
1108
|
+
application/json:
|
|
1109
|
+
schema:
|
|
1110
|
+
$ref: '#/components/schemas/RejectProposalRequest'
|
|
1111
|
+
responses:
|
|
1112
|
+
'200':
|
|
1113
|
+
description: Proposal rejected
|
|
1114
|
+
content:
|
|
1115
|
+
application/json:
|
|
1116
|
+
schema:
|
|
1117
|
+
$ref: '#/components/schemas/Proposal'
|
|
1118
|
+
'400':
|
|
1119
|
+
$ref: '#/components/responses/BadRequest'
|
|
1120
|
+
'401':
|
|
1121
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1122
|
+
'403':
|
|
1123
|
+
$ref: '#/components/responses/Forbidden'
|
|
1124
|
+
'404':
|
|
1125
|
+
$ref: '#/components/responses/NotFound'
|
|
1126
|
+
'409':
|
|
1127
|
+
description: Proposal is not pending
|
|
1128
|
+
content:
|
|
1129
|
+
application/json:
|
|
1130
|
+
schema:
|
|
1131
|
+
$ref: '#/components/schemas/Error'
|
|
1132
|
+
'413':
|
|
1133
|
+
$ref: '#/components/responses/PayloadTooLarge'
|
|
1134
|
+
'415':
|
|
1135
|
+
$ref: '#/components/responses/UnsupportedMediaType'
|
|
1136
|
+
|
|
1137
|
+
/proposals/{id}/withdraw:
|
|
1138
|
+
post:
|
|
1139
|
+
tags: [Proposals]
|
|
1140
|
+
summary: Withdraw own pending proposal
|
|
1141
|
+
description: |
|
|
1142
|
+
Access-link Bearer only. Withdraws a **pending** proposal created by
|
|
1143
|
+
this token. Status becomes `withdrawn` (not `rejected`). Optional
|
|
1144
|
+
JSON body `{ "reason": "…" }`.
|
|
1145
|
+
operationId: withdrawProposal
|
|
1146
|
+
security:
|
|
1147
|
+
- AccessLinkBearer: []
|
|
1148
|
+
parameters:
|
|
1149
|
+
- $ref: '#/components/parameters/ProposalId'
|
|
1150
|
+
requestBody:
|
|
1151
|
+
required: false
|
|
1152
|
+
content:
|
|
1153
|
+
application/json:
|
|
1154
|
+
schema:
|
|
1155
|
+
type: object
|
|
1156
|
+
properties:
|
|
1157
|
+
reason:
|
|
1158
|
+
type: string
|
|
1159
|
+
responses:
|
|
1160
|
+
'200':
|
|
1161
|
+
description: Proposal withdrawn
|
|
1162
|
+
content:
|
|
1163
|
+
application/json:
|
|
1164
|
+
schema:
|
|
1165
|
+
$ref: '#/components/schemas/Proposal'
|
|
1166
|
+
'401':
|
|
1167
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1168
|
+
'403':
|
|
1169
|
+
description: Not owned by this access link
|
|
1170
|
+
content:
|
|
1171
|
+
application/json:
|
|
1172
|
+
schema:
|
|
1173
|
+
$ref: '#/components/schemas/Error'
|
|
1174
|
+
'404':
|
|
1175
|
+
$ref: '#/components/responses/NotFound'
|
|
1176
|
+
'409':
|
|
1177
|
+
description: Proposal is not pending
|
|
1178
|
+
content:
|
|
1179
|
+
application/json:
|
|
1180
|
+
schema:
|
|
1181
|
+
$ref: '#/components/schemas/Error'
|
|
1182
|
+
'415':
|
|
1183
|
+
$ref: '#/components/responses/UnsupportedMediaType'
|
|
1184
|
+
|
|
1185
|
+
/logs:
|
|
1186
|
+
get:
|
|
1187
|
+
tags: [Audit log]
|
|
1188
|
+
summary: List recent audit log entries
|
|
1189
|
+
description: Admin session with at least `read`. Newest entries first.
|
|
1190
|
+
operationId: listLogs
|
|
1191
|
+
security:
|
|
1192
|
+
- AdminSession: []
|
|
1193
|
+
parameters:
|
|
1194
|
+
- name: limit
|
|
1195
|
+
in: query
|
|
1196
|
+
schema:
|
|
1197
|
+
type: integer
|
|
1198
|
+
minimum: 1
|
|
1199
|
+
maximum: 500
|
|
1200
|
+
default: 100
|
|
1201
|
+
responses:
|
|
1202
|
+
'200':
|
|
1203
|
+
description: Audit entries
|
|
1204
|
+
content:
|
|
1205
|
+
application/json:
|
|
1206
|
+
schema:
|
|
1207
|
+
type: object
|
|
1208
|
+
required: [logs]
|
|
1209
|
+
properties:
|
|
1210
|
+
logs:
|
|
1211
|
+
type: array
|
|
1212
|
+
items:
|
|
1213
|
+
$ref: '#/components/schemas/AuditEntry'
|
|
1214
|
+
'401':
|
|
1215
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1216
|
+
'403':
|
|
1217
|
+
$ref: '#/components/responses/Forbidden'
|
|
1218
|
+
|
|
1219
|
+
components:
|
|
1220
|
+
securitySchemes:
|
|
1221
|
+
AdminSession:
|
|
1222
|
+
type: apiKey
|
|
1223
|
+
in: cookie
|
|
1224
|
+
name: node-red-session
|
|
1225
|
+
description: |
|
|
1226
|
+
Node-RED editor admin session established via `adminAuth`.
|
|
1227
|
+
Equivalent browser cookie/session auth used by the `/mcp` admin UI.
|
|
1228
|
+
AccessLinkBearer:
|
|
1229
|
+
type: http
|
|
1230
|
+
scheme: bearer
|
|
1231
|
+
bearerFormat: access-link
|
|
1232
|
+
description: |
|
|
1233
|
+
Temporary access-link token from `POST /links` (`token` field).
|
|
1234
|
+
On 401, ask the user for a new access link.
|
|
1235
|
+
|
|
1236
|
+
parameters:
|
|
1237
|
+
LinkId:
|
|
1238
|
+
name: id
|
|
1239
|
+
in: path
|
|
1240
|
+
required: true
|
|
1241
|
+
schema:
|
|
1242
|
+
type: string
|
|
1243
|
+
FlowId:
|
|
1244
|
+
name: id
|
|
1245
|
+
in: path
|
|
1246
|
+
required: true
|
|
1247
|
+
schema:
|
|
1248
|
+
type: string
|
|
1249
|
+
ProposalId:
|
|
1250
|
+
name: id
|
|
1251
|
+
in: path
|
|
1252
|
+
required: true
|
|
1253
|
+
schema:
|
|
1254
|
+
type: string
|
|
1255
|
+
pattern: '^prop-'
|
|
1256
|
+
ProposalInclude:
|
|
1257
|
+
name: include
|
|
1258
|
+
in: query
|
|
1259
|
+
required: false
|
|
1260
|
+
schema:
|
|
1261
|
+
type: string
|
|
1262
|
+
description: |
|
|
1263
|
+
Comma-separated fields to include on lean proposal responses.
|
|
1264
|
+
Supported keys: `payload`, `baseFlow`. Default is lean (omit both).
|
|
1265
|
+
Example: `include=payload,baseFlow`.
|
|
1266
|
+
ProposalIncludePayload:
|
|
1267
|
+
name: include_payload
|
|
1268
|
+
in: query
|
|
1269
|
+
required: false
|
|
1270
|
+
schema:
|
|
1271
|
+
type: boolean
|
|
1272
|
+
description: |
|
|
1273
|
+
Alias for MCP clients: when `true` / `1` / `yes`, includes both
|
|
1274
|
+
`payload` and `baseFlow` (same as `include=payload,baseFlow`).
|
|
1275
|
+
|
|
1276
|
+
responses:
|
|
1277
|
+
BadRequest:
|
|
1278
|
+
description: Invalid request
|
|
1279
|
+
content:
|
|
1280
|
+
application/json:
|
|
1281
|
+
schema:
|
|
1282
|
+
$ref: '#/components/schemas/Error'
|
|
1283
|
+
Unauthorized:
|
|
1284
|
+
description: Admin authentication required or insufficient permissions
|
|
1285
|
+
content:
|
|
1286
|
+
application/json:
|
|
1287
|
+
schema:
|
|
1288
|
+
$ref: '#/components/schemas/Error'
|
|
1289
|
+
AccessLinkUnauthorized:
|
|
1290
|
+
description: |
|
|
1291
|
+
Missing (`code: access_link_missing`), wrong scheme such as Basic
|
|
1292
|
+
(`code: bearer_scheme_required`), known-expired
|
|
1293
|
+
(`code: token_expired`), or invalid/revoked/pre-restart
|
|
1294
|
+
(`code: access_link_invalid`) access link. Includes
|
|
1295
|
+
`WWW-Authenticate: Bearer` (on every channel, including GET links).
|
|
1296
|
+
`token_expired` also includes `expiredAt` and `accessLinksUrl` (the
|
|
1297
|
+
`/mcp` page's Access links section, where the user creates a new link). Message instructs the AI to ask the user
|
|
1298
|
+
for a new access link (or to use Bearer, not Basic).
|
|
1299
|
+
headers:
|
|
1300
|
+
WWW-Authenticate:
|
|
1301
|
+
schema:
|
|
1302
|
+
type: string
|
|
1303
|
+
description: 'Bearer realm="node-red-ai-access", charset="UTF-8"'
|
|
1304
|
+
content:
|
|
1305
|
+
application/json:
|
|
1306
|
+
schema:
|
|
1307
|
+
$ref: '#/components/schemas/Error'
|
|
1308
|
+
examples:
|
|
1309
|
+
missing:
|
|
1310
|
+
value:
|
|
1311
|
+
error: 'Access link Bearer token is missing. Ask the user for a new access link and send Authorization: Bearer <token>.'
|
|
1312
|
+
code: access_link_missing
|
|
1313
|
+
basicScheme:
|
|
1314
|
+
value:
|
|
1315
|
+
error: 'Authorization must use the Bearer scheme (Authorization: Bearer <access-link-token>), not Basic. Ask the user for a new access link.'
|
|
1316
|
+
code: bearer_scheme_required
|
|
1317
|
+
expired:
|
|
1318
|
+
value:
|
|
1319
|
+
error: 'Access link token has expired. Ask the user for a new access link.'
|
|
1320
|
+
code: token_expired
|
|
1321
|
+
expiredAt: '2026-01-01T12:00:00.000Z'
|
|
1322
|
+
accessLinksUrl: 'https://node-red.example/mcp#links'
|
|
1323
|
+
hint: 'Ask the user to create a new access link in Node-RED on the /mcp page → Access links (https://node-red.example/mcp#links).'
|
|
1324
|
+
askForNewLink:
|
|
1325
|
+
value:
|
|
1326
|
+
error: 'Access link is invalid, revoked, or no longer available after a restart. Ask the user for a new access link.'
|
|
1327
|
+
code: access_link_invalid
|
|
1328
|
+
VersionConflict:
|
|
1329
|
+
description: |
|
|
1330
|
+
`expectedRev` does not match the current flow revision
|
|
1331
|
+
(`code: version_mismatch`). Re-read flows and resubmit.
|
|
1332
|
+
content:
|
|
1333
|
+
application/json:
|
|
1334
|
+
schema:
|
|
1335
|
+
$ref: '#/components/schemas/VersionConflictError'
|
|
1336
|
+
Forbidden:
|
|
1337
|
+
description: |
|
|
1338
|
+
AI features disabled, full (`*`) permission required, or access-link
|
|
1339
|
+
token kind mismatch (`code: wrong_token_kind` with `expectedKind` /
|
|
1340
|
+
`actualKind`).
|
|
1341
|
+
content:
|
|
1342
|
+
application/json:
|
|
1343
|
+
schema:
|
|
1344
|
+
oneOf:
|
|
1345
|
+
- $ref: '#/components/schemas/WrongTokenKindError'
|
|
1346
|
+
- $ref: '#/components/schemas/Error'
|
|
1347
|
+
examples:
|
|
1348
|
+
wrongTokenKind:
|
|
1349
|
+
value:
|
|
1350
|
+
error: 'Access link kind mismatch: expected rest, got get_link'
|
|
1351
|
+
code: wrong_token_kind
|
|
1352
|
+
expectedKind: rest
|
|
1353
|
+
actualKind: get_link
|
|
1354
|
+
hint: 'This REST endpoint requires a rest token. This token is kind=get_link — ask the user for a REST Bearer token (kind=rest).'
|
|
1355
|
+
NotFound:
|
|
1356
|
+
description: Resource not found
|
|
1357
|
+
content:
|
|
1358
|
+
application/json:
|
|
1359
|
+
schema:
|
|
1360
|
+
$ref: '#/components/schemas/Error'
|
|
1361
|
+
LinkCreateNotSupported:
|
|
1362
|
+
description: |
|
|
1363
|
+
Attempted to create/list/revoke access links under the GET
|
|
1364
|
+
`/mcp/a/<token>` surface (`code: link_create_not_supported`).
|
|
1365
|
+
GET links cannot mint tokens — use admin `POST /links` instead.
|
|
1366
|
+
content:
|
|
1367
|
+
application/json:
|
|
1368
|
+
schema:
|
|
1369
|
+
$ref: '#/components/schemas/LinkCreateNotSupportedError'
|
|
1370
|
+
PayloadTooLarge:
|
|
1371
|
+
description: Proposal JSON or audit append exceeds size cap
|
|
1372
|
+
content:
|
|
1373
|
+
application/json:
|
|
1374
|
+
schema:
|
|
1375
|
+
$ref: '#/components/schemas/PayloadTooLargeError'
|
|
1376
|
+
UnsupportedMediaType:
|
|
1377
|
+
description: |
|
|
1378
|
+
Request body Content-Type is not `application/json`
|
|
1379
|
+
(`code: unsupported_media_type`). Send
|
|
1380
|
+
`Content-Type: application/json` with a JSON body.
|
|
1381
|
+
content:
|
|
1382
|
+
application/json:
|
|
1383
|
+
schema:
|
|
1384
|
+
$ref: '#/components/schemas/Error'
|
|
1385
|
+
examples:
|
|
1386
|
+
notJson:
|
|
1387
|
+
value:
|
|
1388
|
+
error: 'Content-Type must be application/json'
|
|
1389
|
+
code: unsupported_media_type
|
|
1390
|
+
hint: 'Send Content-Type: application/json with a JSON body.'
|
|
1391
|
+
BadGateway:
|
|
1392
|
+
description: Upstream Node-RED runtime/Admin API failure
|
|
1393
|
+
content:
|
|
1394
|
+
application/json:
|
|
1395
|
+
schema:
|
|
1396
|
+
$ref: '#/components/schemas/Error'
|
|
1397
|
+
|
|
1398
|
+
schemas:
|
|
1399
|
+
Error:
|
|
1400
|
+
type: object
|
|
1401
|
+
required: [error, code]
|
|
1402
|
+
properties:
|
|
1403
|
+
error:
|
|
1404
|
+
type: string
|
|
1405
|
+
code:
|
|
1406
|
+
type: string
|
|
1407
|
+
description: Machine-readable error code — see `ErrorCode` and docs/errors.md.
|
|
1408
|
+
hint:
|
|
1409
|
+
type: string
|
|
1410
|
+
|
|
1411
|
+
# BEGIN GENERATED ERROR CODES (src/plugin/error-registry.ts)
|
|
1412
|
+
ErrorCode:
|
|
1413
|
+
type: string
|
|
1414
|
+
description: |
|
|
1415
|
+
Machine-readable `code` on JSON error bodies. See docs/errors.md.
|
|
1416
|
+
|
|
1417
|
+
- `access_link_missing` (401): No Bearer / path token
|
|
1418
|
+
- `bearer_scheme_required` (401): Auth used Basic (or non-Bearer)
|
|
1419
|
+
- `token_expired` (401): Known token past `expiresAt` (body includes `expiredAt` and `accessLinksUrl`)
|
|
1420
|
+
- `access_link_invalid` (401): Unknown, revoked, or cleared after restart
|
|
1421
|
+
- `wrong_token_kind` (403): Token kind does not match this surface
|
|
1422
|
+
- `ai_features_disabled` (403): Plugin AI features off
|
|
1423
|
+
- `auth_required` (401): Admin session required
|
|
1424
|
+
- `insufficient_permissions` (403): Admin lacks required scope
|
|
1425
|
+
- `invalid_link_kind` (400): Unknown `kind` on create
|
|
1426
|
+
- `invalid_ttl` (400): `ttlSeconds` is zero, negative, not a number, or over the cap for the kind
|
|
1427
|
+
- `access_link_limit` (409): Too many active links
|
|
1428
|
+
- `access_link_not_found` (404): Unknown link id on revoke
|
|
1429
|
+
- `link_create_not_supported` (405): Attempted mint under a GET link
|
|
1430
|
+
- `rate_limited` (429): Per-IP or per-token limit
|
|
1431
|
+
- `payload_too_large` (413): Body / proposal over ~1.5MB
|
|
1432
|
+
- `invalid_json` (400): Malformed JSON body
|
|
1433
|
+
- `unsupported_media_type` (415): POST body is not `application/json`
|
|
1434
|
+
- `url_too_long` (414): GET URL over soft cap (~8KB)
|
|
1435
|
+
- `not_found` (404): Unknown path or resource
|
|
1436
|
+
- `missing_query` (400): Required search `q` missing
|
|
1437
|
+
- `invalid_filter` (400): Invalid list filter (`status`, `limit`, `offset`, `mine`); body names `parameter` and `allowed`
|
|
1438
|
+
- `invalid_arguments` (400): MCP tool arguments failed schema validation; body names `parameter`
|
|
1439
|
+
- `invalid_diff_format` (400): Unknown `format` on proposal diff
|
|
1440
|
+
- `confirm_required` (400): GET withdraw without `confirm=1`
|
|
1441
|
+
- `missing_expected_rev` (400): Proposal submitted without `expectedRev` / `rev`
|
|
1442
|
+
- `version_mismatch` (409): Stale `rev` / `expectedRev`
|
|
1443
|
+
- `proposal_not_found` (404): Unknown proposal id
|
|
1444
|
+
- `proposal_not_pending` (409): Action needs a pending proposal
|
|
1445
|
+
- `proposal_not_owned` (403): Withdraw of another token’s proposal
|
|
1446
|
+
- `validation_failed` (400): Several validation errors at once (message `N validation errors`)
|
|
1447
|
+
- `duplicate_node_id` (400): The same node id appears more than once in a full tab or batch
|
|
1448
|
+
- `missing_node_id` (400): Node / op without an id
|
|
1449
|
+
- `missing_node_type` (400): Node without a `type`
|
|
1450
|
+
- `invalid_node_type` (400): `type` is not a string
|
|
1451
|
+
- `invalid_nodes` (400): `flow.nodes` / `flow.configs` not an array
|
|
1452
|
+
- `invalid_node` (400): Node entry is not an object
|
|
1453
|
+
- `invalid_wires` (400): Bad wires shape, missing target, or more ports than outputs
|
|
1454
|
+
- `z_mismatch` (400): Node `z` differs from the flow id
|
|
1455
|
+
- `node_already_exists` (400): `add` op for an id already in the flow
|
|
1456
|
+
- `protected_property` (400): Tried to change id / type / z
|
|
1457
|
+
- `node_not_found` (404): Unknown node (single error only; several errors → `validation_failed`)
|
|
1458
|
+
- `field_not_found` (404): Unknown field on a node
|
|
1459
|
+
- `text_not_found` (400): `replace_text` find had no match
|
|
1460
|
+
- `ambiguous_match` (409): `replace_text` matched more than once
|
|
1461
|
+
- `unknown_node_type` (400): Proposal uses a type that is not installed
|
|
1462
|
+
- `node_type_not_found` (404): Unknown type on node-types detail
|
|
1463
|
+
- `chunk_conflict` (409): Same draft seq re-sent with different data
|
|
1464
|
+
- `missing_chunks` (400): Draft submit with gaps in seq
|
|
1465
|
+
- `draft_hash_mismatch` (400): Draft sha256 does not match assembled bytes
|
|
1466
|
+
- `upstream_error` (502): Node-RED runtime/flows error
|
|
1467
|
+
- `internal_error` (500): Unexpected server error
|
|
1468
|
+
enum:
|
|
1469
|
+
- access_link_missing
|
|
1470
|
+
- bearer_scheme_required
|
|
1471
|
+
- token_expired
|
|
1472
|
+
- access_link_invalid
|
|
1473
|
+
- wrong_token_kind
|
|
1474
|
+
- ai_features_disabled
|
|
1475
|
+
- auth_required
|
|
1476
|
+
- insufficient_permissions
|
|
1477
|
+
- invalid_link_kind
|
|
1478
|
+
- invalid_ttl
|
|
1479
|
+
- access_link_limit
|
|
1480
|
+
- access_link_not_found
|
|
1481
|
+
- link_create_not_supported
|
|
1482
|
+
- rate_limited
|
|
1483
|
+
- payload_too_large
|
|
1484
|
+
- invalid_json
|
|
1485
|
+
- unsupported_media_type
|
|
1486
|
+
- url_too_long
|
|
1487
|
+
- not_found
|
|
1488
|
+
- missing_query
|
|
1489
|
+
- invalid_filter
|
|
1490
|
+
- invalid_arguments
|
|
1491
|
+
- invalid_diff_format
|
|
1492
|
+
- confirm_required
|
|
1493
|
+
- missing_expected_rev
|
|
1494
|
+
- version_mismatch
|
|
1495
|
+
- proposal_not_found
|
|
1496
|
+
- proposal_not_pending
|
|
1497
|
+
- proposal_not_owned
|
|
1498
|
+
- validation_failed
|
|
1499
|
+
- duplicate_node_id
|
|
1500
|
+
- missing_node_id
|
|
1501
|
+
- missing_node_type
|
|
1502
|
+
- invalid_node_type
|
|
1503
|
+
- invalid_nodes
|
|
1504
|
+
- invalid_node
|
|
1505
|
+
- invalid_wires
|
|
1506
|
+
- z_mismatch
|
|
1507
|
+
- node_already_exists
|
|
1508
|
+
- protected_property
|
|
1509
|
+
- node_not_found
|
|
1510
|
+
- field_not_found
|
|
1511
|
+
- text_not_found
|
|
1512
|
+
- ambiguous_match
|
|
1513
|
+
- unknown_node_type
|
|
1514
|
+
- node_type_not_found
|
|
1515
|
+
- chunk_conflict
|
|
1516
|
+
- missing_chunks
|
|
1517
|
+
- draft_hash_mismatch
|
|
1518
|
+
- upstream_error
|
|
1519
|
+
- internal_error
|
|
1520
|
+
# END GENERATED ERROR CODES
|
|
1521
|
+
|
|
1522
|
+
WrongTokenKindError:
|
|
1523
|
+
allOf:
|
|
1524
|
+
- $ref: '#/components/schemas/Error'
|
|
1525
|
+
- type: object
|
|
1526
|
+
required: [code, expectedKind, actualKind]
|
|
1527
|
+
properties:
|
|
1528
|
+
code:
|
|
1529
|
+
type: string
|
|
1530
|
+
const: wrong_token_kind
|
|
1531
|
+
expectedKind:
|
|
1532
|
+
$ref: '#/components/schemas/AccessLinkKind'
|
|
1533
|
+
actualKind:
|
|
1534
|
+
$ref: '#/components/schemas/AccessLinkKind'
|
|
1535
|
+
hint:
|
|
1536
|
+
type: string
|
|
1537
|
+
|
|
1538
|
+
InvalidLinkKindError:
|
|
1539
|
+
allOf:
|
|
1540
|
+
- $ref: '#/components/schemas/Error'
|
|
1541
|
+
- type: object
|
|
1542
|
+
required: [code]
|
|
1543
|
+
properties:
|
|
1544
|
+
code:
|
|
1545
|
+
type: string
|
|
1546
|
+
const: invalid_link_kind
|
|
1547
|
+
error:
|
|
1548
|
+
type: string
|
|
1549
|
+
example: "kind must be one of: 'get_link', 'rest', 'mcp'"
|
|
1550
|
+
|
|
1551
|
+
LinkCreateNotSupportedError:
|
|
1552
|
+
allOf:
|
|
1553
|
+
- $ref: '#/components/schemas/Error'
|
|
1554
|
+
- type: object
|
|
1555
|
+
required: [code]
|
|
1556
|
+
properties:
|
|
1557
|
+
code:
|
|
1558
|
+
type: string
|
|
1559
|
+
const: link_create_not_supported
|
|
1560
|
+
error:
|
|
1561
|
+
type: string
|
|
1562
|
+
example: GET access links cannot create tokens
|
|
1563
|
+
hint:
|
|
1564
|
+
type: string
|
|
1565
|
+
|
|
1566
|
+
NodeTypeListItem:
|
|
1567
|
+
type: object
|
|
1568
|
+
required: [type, module]
|
|
1569
|
+
properties:
|
|
1570
|
+
type:
|
|
1571
|
+
type: string
|
|
1572
|
+
module:
|
|
1573
|
+
type: string
|
|
1574
|
+
category:
|
|
1575
|
+
type: string
|
|
1576
|
+
description:
|
|
1577
|
+
type: string
|
|
1578
|
+
enabled:
|
|
1579
|
+
type: boolean
|
|
1580
|
+
version:
|
|
1581
|
+
type: string
|
|
1582
|
+
setId:
|
|
1583
|
+
type: string
|
|
1584
|
+
|
|
1585
|
+
NodeTypeListResult:
|
|
1586
|
+
type: object
|
|
1587
|
+
required: [types, total, truncated]
|
|
1588
|
+
properties:
|
|
1589
|
+
types:
|
|
1590
|
+
type: array
|
|
1591
|
+
items:
|
|
1592
|
+
$ref: '#/components/schemas/NodeTypeListItem'
|
|
1593
|
+
total:
|
|
1594
|
+
type: integer
|
|
1595
|
+
truncated:
|
|
1596
|
+
type: boolean
|
|
1597
|
+
q:
|
|
1598
|
+
type: string
|
|
1599
|
+
|
|
1600
|
+
NodeTypeDetail:
|
|
1601
|
+
type: object
|
|
1602
|
+
required: [type, module]
|
|
1603
|
+
properties:
|
|
1604
|
+
type:
|
|
1605
|
+
type: string
|
|
1606
|
+
module:
|
|
1607
|
+
type: string
|
|
1608
|
+
setId:
|
|
1609
|
+
type: string
|
|
1610
|
+
version:
|
|
1611
|
+
type: string
|
|
1612
|
+
enabled:
|
|
1613
|
+
type: boolean
|
|
1614
|
+
category:
|
|
1615
|
+
type: string
|
|
1616
|
+
color:
|
|
1617
|
+
type: string
|
|
1618
|
+
inputs:
|
|
1619
|
+
type: number
|
|
1620
|
+
outputs:
|
|
1621
|
+
type: number
|
|
1622
|
+
properties:
|
|
1623
|
+
type: object
|
|
1624
|
+
additionalProperties: true
|
|
1625
|
+
defaults:
|
|
1626
|
+
type: object
|
|
1627
|
+
additionalProperties: true
|
|
1628
|
+
requiredConfigNodeTypes:
|
|
1629
|
+
type: array
|
|
1630
|
+
items:
|
|
1631
|
+
type: string
|
|
1632
|
+
help:
|
|
1633
|
+
type: string
|
|
1634
|
+
|
|
1635
|
+
PaletteSearchItem:
|
|
1636
|
+
type: object
|
|
1637
|
+
required: [module, installed]
|
|
1638
|
+
properties:
|
|
1639
|
+
module:
|
|
1640
|
+
type: string
|
|
1641
|
+
description:
|
|
1642
|
+
type: string
|
|
1643
|
+
version:
|
|
1644
|
+
type: string
|
|
1645
|
+
updated:
|
|
1646
|
+
type: string
|
|
1647
|
+
downloads:
|
|
1648
|
+
type: number
|
|
1649
|
+
link:
|
|
1650
|
+
type: string
|
|
1651
|
+
installed:
|
|
1652
|
+
type: boolean
|
|
1653
|
+
|
|
1654
|
+
PaletteSearchResult:
|
|
1655
|
+
type: object
|
|
1656
|
+
required: [q, modules, total, truncated, hint]
|
|
1657
|
+
properties:
|
|
1658
|
+
q:
|
|
1659
|
+
type: string
|
|
1660
|
+
modules:
|
|
1661
|
+
type: array
|
|
1662
|
+
items:
|
|
1663
|
+
$ref: '#/components/schemas/PaletteSearchItem'
|
|
1664
|
+
total:
|
|
1665
|
+
type: integer
|
|
1666
|
+
truncated:
|
|
1667
|
+
type: boolean
|
|
1668
|
+
hint:
|
|
1669
|
+
type: string
|
|
1670
|
+
|
|
1671
|
+
PayloadTooLargeError:
|
|
1672
|
+
allOf:
|
|
1673
|
+
- $ref: '#/components/schemas/Error'
|
|
1674
|
+
- type: object
|
|
1675
|
+
properties:
|
|
1676
|
+
code:
|
|
1677
|
+
type: string
|
|
1678
|
+
const: payload_too_large
|
|
1679
|
+
|
|
1680
|
+
VersionConflictError:
|
|
1681
|
+
allOf:
|
|
1682
|
+
- $ref: '#/components/schemas/Error'
|
|
1683
|
+
- type: object
|
|
1684
|
+
properties:
|
|
1685
|
+
code:
|
|
1686
|
+
type: string
|
|
1687
|
+
const: version_mismatch
|
|
1688
|
+
expectedRev:
|
|
1689
|
+
type: string
|
|
1690
|
+
currentRev:
|
|
1691
|
+
type: string
|
|
1692
|
+
|
|
1693
|
+
AiAccessSettingsPatch:
|
|
1694
|
+
type: object
|
|
1695
|
+
properties:
|
|
1696
|
+
enabled:
|
|
1697
|
+
type: boolean
|
|
1698
|
+
allowWithoutAdminAuth:
|
|
1699
|
+
type: boolean
|
|
1700
|
+
defaultLinkTtlSeconds:
|
|
1701
|
+
type: integer
|
|
1702
|
+
minimum: 1
|
|
1703
|
+
defaultDeploymentType:
|
|
1704
|
+
$ref: '#/components/schemas/DeploymentType'
|
|
1705
|
+
maxActiveLinks:
|
|
1706
|
+
type: integer
|
|
1707
|
+
minimum: 1
|
|
1708
|
+
additionalProperties: false
|
|
1709
|
+
|
|
1710
|
+
AiAccessSettingsResponse:
|
|
1711
|
+
type: object
|
|
1712
|
+
required:
|
|
1713
|
+
- ok
|
|
1714
|
+
- adminAuthEnabled
|
|
1715
|
+
- canEdit
|
|
1716
|
+
- mcpPageUrl
|
|
1717
|
+
- settings
|
|
1718
|
+
- sources
|
|
1719
|
+
- precedence
|
|
1720
|
+
properties:
|
|
1721
|
+
ok:
|
|
1722
|
+
type: boolean
|
|
1723
|
+
adminAuthEnabled:
|
|
1724
|
+
type: boolean
|
|
1725
|
+
canEdit:
|
|
1726
|
+
type: boolean
|
|
1727
|
+
permission:
|
|
1728
|
+
oneOf:
|
|
1729
|
+
- type: string
|
|
1730
|
+
enum: [read, '*']
|
|
1731
|
+
- type: 'null'
|
|
1732
|
+
mcpPageUrl:
|
|
1733
|
+
type: string
|
|
1734
|
+
apiBase:
|
|
1735
|
+
type: string
|
|
1736
|
+
settings:
|
|
1737
|
+
type: object
|
|
1738
|
+
required:
|
|
1739
|
+
- enabled
|
|
1740
|
+
- allowWithoutAdminAuth
|
|
1741
|
+
- defaultLinkTtlSeconds
|
|
1742
|
+
- defaultDeploymentType
|
|
1743
|
+
- maxActiveLinks
|
|
1744
|
+
properties:
|
|
1745
|
+
enabled:
|
|
1746
|
+
type: boolean
|
|
1747
|
+
allowWithoutAdminAuth:
|
|
1748
|
+
type: boolean
|
|
1749
|
+
defaultLinkTtlSeconds:
|
|
1750
|
+
type: integer
|
|
1751
|
+
defaultDeploymentType:
|
|
1752
|
+
$ref: '#/components/schemas/DeploymentType'
|
|
1753
|
+
maxActiveLinks:
|
|
1754
|
+
type: integer
|
|
1755
|
+
sources:
|
|
1756
|
+
type: object
|
|
1757
|
+
additionalProperties:
|
|
1758
|
+
type: string
|
|
1759
|
+
enum: [default, settings.js, disk]
|
|
1760
|
+
disk:
|
|
1761
|
+
type: object
|
|
1762
|
+
additionalProperties: true
|
|
1763
|
+
precedence:
|
|
1764
|
+
type: array
|
|
1765
|
+
items:
|
|
1766
|
+
type: string
|
|
1767
|
+
description: Highest-wins order for effective values
|
|
1768
|
+
|
|
1769
|
+
Status:
|
|
1770
|
+
type: object
|
|
1771
|
+
required:
|
|
1772
|
+
- ok
|
|
1773
|
+
- version
|
|
1774
|
+
- adminAuthEnabled
|
|
1775
|
+
- aiAccessEnabled
|
|
1776
|
+
- aiFeaturesEnabled
|
|
1777
|
+
- allowWithoutAdminAuth
|
|
1778
|
+
- permission
|
|
1779
|
+
- capabilities
|
|
1780
|
+
- storageDir
|
|
1781
|
+
- defaultLinkTtlSeconds
|
|
1782
|
+
- maxActiveLinks
|
|
1783
|
+
- defaultDeploymentType
|
|
1784
|
+
properties:
|
|
1785
|
+
ok:
|
|
1786
|
+
type: boolean
|
|
1787
|
+
version:
|
|
1788
|
+
type: string
|
|
1789
|
+
adminAuthEnabled:
|
|
1790
|
+
type: boolean
|
|
1791
|
+
aiAccessEnabled:
|
|
1792
|
+
type: boolean
|
|
1793
|
+
description: Master switch from resolved settings (disk / settings.js)
|
|
1794
|
+
aiFeaturesEnabled:
|
|
1795
|
+
type: boolean
|
|
1796
|
+
allowWithoutAdminAuth:
|
|
1797
|
+
type: boolean
|
|
1798
|
+
permission:
|
|
1799
|
+
oneOf:
|
|
1800
|
+
- type: string
|
|
1801
|
+
enum: [read, '*']
|
|
1802
|
+
- type: 'null'
|
|
1803
|
+
description: |
|
|
1804
|
+
Admin session permission, or `null` for access-link callers
|
|
1805
|
+
(AI tokens are not admin sessions — see `caller`).
|
|
1806
|
+
capabilities:
|
|
1807
|
+
$ref: '#/components/schemas/Capabilities'
|
|
1808
|
+
storageDir:
|
|
1809
|
+
type: string
|
|
1810
|
+
defaultLinkTtlSeconds:
|
|
1811
|
+
type: integer
|
|
1812
|
+
maxActiveLinks:
|
|
1813
|
+
type: integer
|
|
1814
|
+
defaultDeploymentType:
|
|
1815
|
+
$ref: '#/components/schemas/DeploymentType'
|
|
1816
|
+
caller:
|
|
1817
|
+
description: Who is calling — admin session or access-link Bearer.
|
|
1818
|
+
oneOf:
|
|
1819
|
+
- type: object
|
|
1820
|
+
required: [kind]
|
|
1821
|
+
properties:
|
|
1822
|
+
kind:
|
|
1823
|
+
type: string
|
|
1824
|
+
const: admin
|
|
1825
|
+
permission:
|
|
1826
|
+
type: string
|
|
1827
|
+
enum: [read, '*']
|
|
1828
|
+
- type: object
|
|
1829
|
+
required: [kind, linkId]
|
|
1830
|
+
properties:
|
|
1831
|
+
kind:
|
|
1832
|
+
type: string
|
|
1833
|
+
const: access-link
|
|
1834
|
+
linkId:
|
|
1835
|
+
type: string
|
|
1836
|
+
label:
|
|
1837
|
+
type: string
|
|
1838
|
+
expiresAt:
|
|
1839
|
+
type: string
|
|
1840
|
+
format: date-time
|
|
1841
|
+
description: ISO expiry time of this access-link token.
|
|
1842
|
+
minutesRemaining:
|
|
1843
|
+
type: integer
|
|
1844
|
+
minimum: 0
|
|
1845
|
+
description: >
|
|
1846
|
+
Whole minutes until `expiresAt`, rounded up so it is never 0 while
|
|
1847
|
+
the token is still valid (never negative).
|
|
1848
|
+
secondsRemaining:
|
|
1849
|
+
type: integer
|
|
1850
|
+
minimum: 0
|
|
1851
|
+
description: >
|
|
1852
|
+
Whole seconds until `expiresAt` (rounded up; never negative).
|
|
1853
|
+
permissions:
|
|
1854
|
+
type: array
|
|
1855
|
+
items:
|
|
1856
|
+
type: string
|
|
1857
|
+
description: >
|
|
1858
|
+
Access-link capabilities (e.g. status:read, flows:read,
|
|
1859
|
+
proposals:create, proposals:read-own)
|
|
1860
|
+
baseUrls:
|
|
1861
|
+
type: object
|
|
1862
|
+
required: [api, rpc, openapi, discovery]
|
|
1863
|
+
properties:
|
|
1864
|
+
api:
|
|
1865
|
+
type: string
|
|
1866
|
+
rpc:
|
|
1867
|
+
type: string
|
|
1868
|
+
openapi:
|
|
1869
|
+
type: string
|
|
1870
|
+
discovery:
|
|
1871
|
+
type: string
|
|
1872
|
+
|
|
1873
|
+
Capabilities:
|
|
1874
|
+
type: object
|
|
1875
|
+
required: [view, manageLinks, approveProposals]
|
|
1876
|
+
properties:
|
|
1877
|
+
view:
|
|
1878
|
+
type: boolean
|
|
1879
|
+
manageLinks:
|
|
1880
|
+
type: boolean
|
|
1881
|
+
approveProposals:
|
|
1882
|
+
type: boolean
|
|
1883
|
+
|
|
1884
|
+
DeploymentType:
|
|
1885
|
+
type: string
|
|
1886
|
+
enum: [nodes, flows, full]
|
|
1887
|
+
default: nodes
|
|
1888
|
+
|
|
1889
|
+
AccessLinkKind:
|
|
1890
|
+
type: string
|
|
1891
|
+
enum: [get_link, rest, mcp]
|
|
1892
|
+
description: |
|
|
1893
|
+
Surface this token is minted for (admin `POST /links` only):
|
|
1894
|
+
- `get_link` → path token on `/mcp/a/<token>/…` (GET-only; **cannot**
|
|
1895
|
+
create further tokens)
|
|
1896
|
+
- `rest` → `Authorization: Bearer` on this REST API
|
|
1897
|
+
- `mcp` → `Authorization: Bearer` on `/mcp/rpc`
|
|
1898
|
+
Default on create when omitted: `get_link`. Wrong surface → **403**
|
|
1899
|
+
`wrong_token_kind`.
|
|
1900
|
+
|
|
1901
|
+
AccessLinkPublic:
|
|
1902
|
+
type: object
|
|
1903
|
+
required: [id, kind, createdAt, expiresAt]
|
|
1904
|
+
properties:
|
|
1905
|
+
id:
|
|
1906
|
+
type: string
|
|
1907
|
+
kind:
|
|
1908
|
+
$ref: '#/components/schemas/AccessLinkKind'
|
|
1909
|
+
label:
|
|
1910
|
+
type: string
|
|
1911
|
+
createdAt:
|
|
1912
|
+
type: string
|
|
1913
|
+
format: date-time
|
|
1914
|
+
expiresAt:
|
|
1915
|
+
type: string
|
|
1916
|
+
format: date-time
|
|
1917
|
+
createdBy:
|
|
1918
|
+
type: string
|
|
1919
|
+
|
|
1920
|
+
AccessLinkCreated:
|
|
1921
|
+
allOf:
|
|
1922
|
+
- $ref: '#/components/schemas/AccessLinkPublic'
|
|
1923
|
+
- type: object
|
|
1924
|
+
required: [token]
|
|
1925
|
+
properties:
|
|
1926
|
+
token:
|
|
1927
|
+
type: string
|
|
1928
|
+
description: Raw bearer token (returned only at creation)
|
|
1929
|
+
url:
|
|
1930
|
+
type: string
|
|
1931
|
+
nullable: true
|
|
1932
|
+
description: >
|
|
1933
|
+
Self-contained GET link (`http(s)://{host}{httpAdminRoot}/mcp/a/{token}`).
|
|
1934
|
+
Present for kind=get_link; omitted for rest/mcp. The get-link
|
|
1935
|
+
URL itself has no create-link operation.
|
|
1936
|
+
|
|
1937
|
+
CreateLinkRequest:
|
|
1938
|
+
type: object
|
|
1939
|
+
properties:
|
|
1940
|
+
ttlSeconds:
|
|
1941
|
+
type: number
|
|
1942
|
+
exclusiveMinimum: 0
|
|
1943
|
+
description: |
|
|
1944
|
+
Lifetime in seconds (default 14400 = 4h). Caps by `kind`:
|
|
1945
|
+
`get_link` ≤ 86400 (24h); `rest` / `mcp` ≤ 2592000 (30d).
|
|
1946
|
+
Over the cap → **400**.
|
|
1947
|
+
label:
|
|
1948
|
+
type: string
|
|
1949
|
+
kind:
|
|
1950
|
+
$ref: '#/components/schemas/AccessLinkKind'
|
|
1951
|
+
description: |
|
|
1952
|
+
Token surface (default `get_link`). Unknown values return
|
|
1953
|
+
**400** `invalid_link_kind`.
|
|
1954
|
+
|
|
1955
|
+
FlowsSnapshot:
|
|
1956
|
+
type: object
|
|
1957
|
+
required: [rev, flows]
|
|
1958
|
+
properties:
|
|
1959
|
+
rev:
|
|
1960
|
+
type: string
|
|
1961
|
+
flows:
|
|
1962
|
+
type: array
|
|
1963
|
+
items:
|
|
1964
|
+
type: object
|
|
1965
|
+
additionalProperties: true
|
|
1966
|
+
|
|
1967
|
+
FlowSnapshot:
|
|
1968
|
+
type: object
|
|
1969
|
+
required: [rev, flow]
|
|
1970
|
+
properties:
|
|
1971
|
+
rev:
|
|
1972
|
+
type: string
|
|
1973
|
+
flow:
|
|
1974
|
+
$ref: '#/components/schemas/FlowTab'
|
|
1975
|
+
|
|
1976
|
+
FlowSummary:
|
|
1977
|
+
type: object
|
|
1978
|
+
description: Lightweight flow view from `GET /flows/{id}?summary=1`
|
|
1979
|
+
required: [rev, flowId, nodes, nodeCount]
|
|
1980
|
+
properties:
|
|
1981
|
+
rev:
|
|
1982
|
+
type: string
|
|
1983
|
+
flowId:
|
|
1984
|
+
type: string
|
|
1985
|
+
label:
|
|
1986
|
+
type: string
|
|
1987
|
+
type:
|
|
1988
|
+
type: string
|
|
1989
|
+
nodeCount:
|
|
1990
|
+
type: integer
|
|
1991
|
+
nodes:
|
|
1992
|
+
type: array
|
|
1993
|
+
items:
|
|
1994
|
+
type: object
|
|
1995
|
+
required: [id, type, bytes]
|
|
1996
|
+
properties:
|
|
1997
|
+
id:
|
|
1998
|
+
type: string
|
|
1999
|
+
type:
|
|
2000
|
+
type: string
|
|
2001
|
+
name:
|
|
2002
|
+
type: string
|
|
2003
|
+
wires: {}
|
|
2004
|
+
bytes:
|
|
2005
|
+
type: integer
|
|
2006
|
+
description: UTF-8 byte size of the full node JSON
|
|
2007
|
+
|
|
2008
|
+
NodeSnapshot:
|
|
2009
|
+
type: object
|
|
2010
|
+
required: [rev, flowId, node]
|
|
2011
|
+
properties:
|
|
2012
|
+
rev:
|
|
2013
|
+
type: string
|
|
2014
|
+
flowId:
|
|
2015
|
+
oneOf:
|
|
2016
|
+
- type: string
|
|
2017
|
+
- type: 'null'
|
|
2018
|
+
node:
|
|
2019
|
+
type: object
|
|
2020
|
+
additionalProperties: true
|
|
2021
|
+
|
|
2022
|
+
NodeFieldSnapshot:
|
|
2023
|
+
type: object
|
|
2024
|
+
required: [rev, flowId, nodeId, field, value, totalLines, bytes]
|
|
2025
|
+
properties:
|
|
2026
|
+
rev:
|
|
2027
|
+
type: string
|
|
2028
|
+
flowId:
|
|
2029
|
+
oneOf:
|
|
2030
|
+
- type: string
|
|
2031
|
+
- type: 'null'
|
|
2032
|
+
nodeId:
|
|
2033
|
+
type: string
|
|
2034
|
+
field:
|
|
2035
|
+
type: string
|
|
2036
|
+
value:
|
|
2037
|
+
type: string
|
|
2038
|
+
totalLines:
|
|
2039
|
+
type: integer
|
|
2040
|
+
bytes:
|
|
2041
|
+
type: integer
|
|
2042
|
+
lineRange:
|
|
2043
|
+
type: object
|
|
2044
|
+
required: [start, end]
|
|
2045
|
+
properties:
|
|
2046
|
+
start:
|
|
2047
|
+
type: integer
|
|
2048
|
+
end:
|
|
2049
|
+
type: integer
|
|
2050
|
+
|
|
2051
|
+
SearchResult:
|
|
2052
|
+
type: object
|
|
2053
|
+
required: [rev, q, hits, hitCount, truncated, cap]
|
|
2054
|
+
properties:
|
|
2055
|
+
rev:
|
|
2056
|
+
type: string
|
|
2057
|
+
q:
|
|
2058
|
+
type: string
|
|
2059
|
+
flowId:
|
|
2060
|
+
type: string
|
|
2061
|
+
hitCount:
|
|
2062
|
+
type: integer
|
|
2063
|
+
truncated:
|
|
2064
|
+
type: boolean
|
|
2065
|
+
cap:
|
|
2066
|
+
type: integer
|
|
2067
|
+
hits:
|
|
2068
|
+
type: array
|
|
2069
|
+
items:
|
|
2070
|
+
type: object
|
|
2071
|
+
required: [flowId, nodeId, field, lines, snippet]
|
|
2072
|
+
properties:
|
|
2073
|
+
flowId:
|
|
2074
|
+
oneOf:
|
|
2075
|
+
- type: string
|
|
2076
|
+
- type: 'null'
|
|
2077
|
+
nodeId:
|
|
2078
|
+
type: string
|
|
2079
|
+
field:
|
|
2080
|
+
type: string
|
|
2081
|
+
lines:
|
|
2082
|
+
type: array
|
|
2083
|
+
items:
|
|
2084
|
+
type: integer
|
|
2085
|
+
snippet:
|
|
2086
|
+
type: string
|
|
2087
|
+
|
|
2088
|
+
FlowTab:
|
|
2089
|
+
type: object
|
|
2090
|
+
description: |
|
|
2091
|
+
Nested tab shape from `GET /flows/{id}` / `get_flow`. Reuse this object
|
|
2092
|
+
as `payload.flow` on `submit_proposal`. `get_flow` always includes
|
|
2093
|
+
`type` (defaults to `tab` when Node-RED omits it) and `nodes` (array,
|
|
2094
|
+
possibly empty). `configs` is optional.
|
|
2095
|
+
required: [id, type, nodes]
|
|
2096
|
+
properties:
|
|
2097
|
+
id:
|
|
2098
|
+
type: string
|
|
2099
|
+
type:
|
|
2100
|
+
type: string
|
|
2101
|
+
description: Tab/subflow type; `get_flow` defaults omitted values to `tab`
|
|
2102
|
+
default: tab
|
|
2103
|
+
label:
|
|
2104
|
+
type: string
|
|
2105
|
+
disabled:
|
|
2106
|
+
type: boolean
|
|
2107
|
+
info:
|
|
2108
|
+
type: string
|
|
2109
|
+
env:
|
|
2110
|
+
type: array
|
|
2111
|
+
items: {}
|
|
2112
|
+
nodes:
|
|
2113
|
+
type: array
|
|
2114
|
+
description: Child nodes for the tab (required on submit_proposal)
|
|
2115
|
+
items:
|
|
2116
|
+
type: object
|
|
2117
|
+
additionalProperties: true
|
|
2118
|
+
configs:
|
|
2119
|
+
type: array
|
|
2120
|
+
items:
|
|
2121
|
+
type: object
|
|
2122
|
+
additionalProperties: true
|
|
2123
|
+
additionalProperties: true
|
|
2124
|
+
|
|
2125
|
+
ProposalStatus:
|
|
2126
|
+
type: string
|
|
2127
|
+
enum: [pending, approved, rejected, failed, withdrawn, expired, superseded]
|
|
2128
|
+
|
|
2129
|
+
Proposal:
|
|
2130
|
+
type: object
|
|
2131
|
+
required:
|
|
2132
|
+
- id
|
|
2133
|
+
- type
|
|
2134
|
+
- status
|
|
2135
|
+
- expectedRev
|
|
2136
|
+
- payload
|
|
2137
|
+
- createdBy
|
|
2138
|
+
- createdAt
|
|
2139
|
+
- updatedAt
|
|
2140
|
+
- title
|
|
2141
|
+
- displayName
|
|
2142
|
+
- reviewUrl
|
|
2143
|
+
- reviewPath
|
|
2144
|
+
properties:
|
|
2145
|
+
id:
|
|
2146
|
+
type: string
|
|
2147
|
+
type:
|
|
2148
|
+
type: string
|
|
2149
|
+
const: replace_flow
|
|
2150
|
+
status:
|
|
2151
|
+
$ref: '#/components/schemas/ProposalStatus'
|
|
2152
|
+
expectedRev:
|
|
2153
|
+
type: string
|
|
2154
|
+
payload:
|
|
2155
|
+
$ref: '#/components/schemas/ReplaceFlowPayload'
|
|
2156
|
+
baseFlow:
|
|
2157
|
+
$ref: '#/components/schemas/FlowTab'
|
|
2158
|
+
description: |
|
|
2159
|
+
Flow tab snapshot at `expectedRev` when the proposal was submitted.
|
|
2160
|
+
Diffs compare `payload.flow` against this base. Omitted on legacy
|
|
2161
|
+
proposals created before this field existed.
|
|
2162
|
+
title:
|
|
2163
|
+
type: string
|
|
2164
|
+
maxLength: 80
|
|
2165
|
+
description: |
|
|
2166
|
+
Short human display title. Set at submit (AI-supplied or
|
|
2167
|
+
auto-generated); ` in <flowLabel>` is appended when the title does
|
|
2168
|
+
not already name the flow. Always present on API responses
|
|
2169
|
+
(resolved for legacy rows).
|
|
2170
|
+
storedTitle:
|
|
2171
|
+
type: string
|
|
2172
|
+
maxLength: 80
|
|
2173
|
+
description: |
|
|
2174
|
+
The title as saved. With an AI-supplied title this is that title
|
|
2175
|
+
(sanitized) without the ` in <flowLabel>` suffix that `title` may
|
|
2176
|
+
add. When no title was given it is the auto-generated title, which
|
|
2177
|
+
may already contain the flow name. Present on every proposal
|
|
2178
|
+
response, including submit.
|
|
2179
|
+
flowLabel:
|
|
2180
|
+
type: string
|
|
2181
|
+
description: |
|
|
2182
|
+
Human label of the target flow tab (stored snapshot, else the live
|
|
2183
|
+
flows). Empty string when unknown.
|
|
2184
|
+
displayName:
|
|
2185
|
+
type: string
|
|
2186
|
+
description: |
|
|
2187
|
+
`title · <submitted datetime>` in Node-RED **server local time**
|
|
2188
|
+
with an explicit UTC offset (e.g. `Edit fn1 (function) · 16 Oct 01:35 +00:00`).
|
|
2189
|
+
Prefer this when referring to the proposal with a human. UIs with a
|
|
2190
|
+
browser should render `createdAt` in the client's local timezone
|
|
2191
|
+
instead of parsing this embedded server clock.
|
|
2192
|
+
reviewUrl:
|
|
2193
|
+
type: string
|
|
2194
|
+
description: |
|
|
2195
|
+
Absolute deep link (`https://host/mcp#proposal=<id>`) built from the
|
|
2196
|
+
request Host/protocol. Uses `X-Forwarded-Host` / `X-Forwarded-Proto`
|
|
2197
|
+
only when Express `trust proxy` is enabled; otherwise falls back to
|
|
2198
|
+
the relative `reviewPath` when Host is unavailable.
|
|
2199
|
+
reviewPath:
|
|
2200
|
+
type: string
|
|
2201
|
+
description: |
|
|
2202
|
+
Relative deep link `/mcp#proposal=<id>` (under `httpAdminRoot` when
|
|
2203
|
+
set) that opens the admin page, switches to Proposals, and
|
|
2204
|
+
scrolls/highlights this item.
|
|
2205
|
+
createdBy:
|
|
2206
|
+
$ref: '#/components/schemas/ProposalCreator'
|
|
2207
|
+
createdAt:
|
|
2208
|
+
type: string
|
|
2209
|
+
format: date-time
|
|
2210
|
+
description: |
|
|
2211
|
+
ISO-8601 instant when the proposal was created. Prefer this for
|
|
2212
|
+
client-side local-time rendering.
|
|
2213
|
+
updatedAt:
|
|
2214
|
+
type: string
|
|
2215
|
+
format: date-time
|
|
2216
|
+
mine:
|
|
2217
|
+
type: boolean
|
|
2218
|
+
description: |
|
|
2219
|
+
Present on access-link responses: true when this token created the
|
|
2220
|
+
proposal.
|
|
2221
|
+
conflictingProposals:
|
|
2222
|
+
type: array
|
|
2223
|
+
description: |
|
|
2224
|
+
Pending proposals on the same flow (any token). Included on submit
|
|
2225
|
+
and on read of pending proposals when siblings exist.
|
|
2226
|
+
items:
|
|
2227
|
+
type: object
|
|
2228
|
+
required: [id, title, displayName, mine]
|
|
2229
|
+
properties:
|
|
2230
|
+
id:
|
|
2231
|
+
type: string
|
|
2232
|
+
title:
|
|
2233
|
+
type: string
|
|
2234
|
+
displayName:
|
|
2235
|
+
type: string
|
|
2236
|
+
mine:
|
|
2237
|
+
type: boolean
|
|
2238
|
+
overlappingNodeIds:
|
|
2239
|
+
type: array
|
|
2240
|
+
items:
|
|
2241
|
+
type: string
|
|
2242
|
+
overlappingFields:
|
|
2243
|
+
type: array
|
|
2244
|
+
items:
|
|
2245
|
+
type: object
|
|
2246
|
+
required: [nodeId, fields]
|
|
2247
|
+
properties:
|
|
2248
|
+
nodeId:
|
|
2249
|
+
type: string
|
|
2250
|
+
fields:
|
|
2251
|
+
type: array
|
|
2252
|
+
items:
|
|
2253
|
+
type: string
|
|
2254
|
+
decidedBy:
|
|
2255
|
+
$ref: '#/components/schemas/ProposalDecisionBy'
|
|
2256
|
+
reason:
|
|
2257
|
+
type: string
|
|
2258
|
+
description: |
|
|
2259
|
+
Present when rejected, failed, or withdrawn. On revision conflict
|
|
2260
|
+
after approve, explains that expectedRev is stale and the AI should
|
|
2261
|
+
re-read flows and submit a new proposal.
|
|
2262
|
+
resultRev:
|
|
2263
|
+
type: string
|
|
2264
|
+
|
|
2265
|
+
ReplaceFlowPayload:
|
|
2266
|
+
type: object
|
|
2267
|
+
required: [flowId]
|
|
2268
|
+
description: |
|
|
2269
|
+
**Canonical form (preferred):** `{ flowId, nodes: [op…] }` with no
|
|
2270
|
+
`flow`. Ops apply to the live tab at top-level `expectedRev`.
|
|
2271
|
+
|
|
2272
|
+
Supported `nodes[]` ops:
|
|
2273
|
+
- `replace_text` — `{ op:"replace_text", node, field, find, replace, occurrence? }`
|
|
2274
|
+
- `set` — `{ id, set:{…} }` or `{ op:"set", id, …props }`
|
|
2275
|
+
- `add` — `{ op:"add", node:{ id, type, … } }`
|
|
2276
|
+
- `remove` — `{ op:"remove", id }` (or `{ id }` remove shape)
|
|
2277
|
+
|
|
2278
|
+
**Alternative:** top-level `nodes` on `CreateProposalRequest` is an
|
|
2279
|
+
alias for `payload.nodes` (same shapes).
|
|
2280
|
+
|
|
2281
|
+
**Full tab:** `{ flowId, flow }` from `GET /flows/{id}` still works
|
|
2282
|
+
(optionally with `nodes` applied first when both are present).
|
|
2283
|
+
properties:
|
|
2284
|
+
flowId:
|
|
2285
|
+
type: string
|
|
2286
|
+
flow:
|
|
2287
|
+
$ref: '#/components/schemas/FlowTab'
|
|
2288
|
+
nodes:
|
|
2289
|
+
type: array
|
|
2290
|
+
description: |
|
|
2291
|
+
Canonical node ops (`replace_text` / `set` / `add` / `remove`)
|
|
2292
|
+
applied to the live tab when `flow` is omitted
|
|
2293
|
+
items:
|
|
2294
|
+
type: object
|
|
2295
|
+
additionalProperties: true
|
|
2296
|
+
|
|
2297
|
+
ProposalCreator:
|
|
2298
|
+
type: object
|
|
2299
|
+
required: [kind, linkId]
|
|
2300
|
+
properties:
|
|
2301
|
+
kind:
|
|
2302
|
+
type: string
|
|
2303
|
+
const: access-link
|
|
2304
|
+
linkId:
|
|
2305
|
+
type: string
|
|
2306
|
+
label:
|
|
2307
|
+
type: string
|
|
2308
|
+
|
|
2309
|
+
ProposalDecisionBy:
|
|
2310
|
+
type: object
|
|
2311
|
+
required: [kind]
|
|
2312
|
+
description: |
|
|
2313
|
+
Admin for approve/reject/fail; access-link for withdraw.
|
|
2314
|
+
properties:
|
|
2315
|
+
kind:
|
|
2316
|
+
type: string
|
|
2317
|
+
enum: [admin, access-link]
|
|
2318
|
+
username:
|
|
2319
|
+
type: string
|
|
2320
|
+
linkId:
|
|
2321
|
+
type: string
|
|
2322
|
+
label:
|
|
2323
|
+
type: string
|
|
2324
|
+
|
|
2325
|
+
CreateProposalRequest:
|
|
2326
|
+
type: object
|
|
2327
|
+
required: [type, expectedRev, payload]
|
|
2328
|
+
properties:
|
|
2329
|
+
type:
|
|
2330
|
+
type: string
|
|
2331
|
+
const: replace_flow
|
|
2332
|
+
expectedRev:
|
|
2333
|
+
type: string
|
|
2334
|
+
description: Preferred field name for the current flow revision (required)
|
|
2335
|
+
expected_rev:
|
|
2336
|
+
type: string
|
|
2337
|
+
description: Alias for expectedRev
|
|
2338
|
+
title:
|
|
2339
|
+
type: string
|
|
2340
|
+
maxLength: 80
|
|
2341
|
+
description: |
|
|
2342
|
+
Optional short human title (≤80 chars, sanitized). Auto-generated
|
|
2343
|
+
from the change set when omitted.
|
|
2344
|
+
payload:
|
|
2345
|
+
$ref: '#/components/schemas/ReplaceFlowPayload'
|
|
2346
|
+
nodes:
|
|
2347
|
+
type: array
|
|
2348
|
+
description: Optional top-level alias for payload.nodes
|
|
2349
|
+
items:
|
|
2350
|
+
type: object
|
|
2351
|
+
additionalProperties: true
|
|
2352
|
+
example:
|
|
2353
|
+
type: replace_flow
|
|
2354
|
+
expectedRev: 'abc123rev'
|
|
2355
|
+
title: Rename inject to Hello
|
|
2356
|
+
payload:
|
|
2357
|
+
flowId: tab1
|
|
2358
|
+
nodes:
|
|
2359
|
+
- op: replace_text
|
|
2360
|
+
node: n1
|
|
2361
|
+
field: template
|
|
2362
|
+
find: Hello
|
|
2363
|
+
replace: Hi
|
|
2364
|
+
|
|
2365
|
+
ApproveProposalRequest:
|
|
2366
|
+
type: object
|
|
2367
|
+
properties:
|
|
2368
|
+
deploymentType:
|
|
2369
|
+
$ref: '#/components/schemas/DeploymentType'
|
|
2370
|
+
|
|
2371
|
+
RejectProposalRequest:
|
|
2372
|
+
type: object
|
|
2373
|
+
properties:
|
|
2374
|
+
reason:
|
|
2375
|
+
type: string
|
|
2376
|
+
default: Rejected by admin
|
|
2377
|
+
|
|
2378
|
+
ProposalDiffLine:
|
|
2379
|
+
type: object
|
|
2380
|
+
required: [op, text]
|
|
2381
|
+
properties:
|
|
2382
|
+
op:
|
|
2383
|
+
type: string
|
|
2384
|
+
enum: [equal, add, remove]
|
|
2385
|
+
text:
|
|
2386
|
+
type: string
|
|
2387
|
+
|
|
2388
|
+
ProposalDiffPropertyChange:
|
|
2389
|
+
oneOf:
|
|
2390
|
+
- type: object
|
|
2391
|
+
required: [key, kind, before, after]
|
|
2392
|
+
properties:
|
|
2393
|
+
key:
|
|
2394
|
+
type: string
|
|
2395
|
+
kind:
|
|
2396
|
+
const: scalar
|
|
2397
|
+
before: {}
|
|
2398
|
+
after: {}
|
|
2399
|
+
- type: object
|
|
2400
|
+
required: [key, kind, before, after, lines]
|
|
2401
|
+
properties:
|
|
2402
|
+
key:
|
|
2403
|
+
type: string
|
|
2404
|
+
kind:
|
|
2405
|
+
const: text
|
|
2406
|
+
before:
|
|
2407
|
+
type: string
|
|
2408
|
+
after:
|
|
2409
|
+
type: string
|
|
2410
|
+
lines:
|
|
2411
|
+
type: array
|
|
2412
|
+
items:
|
|
2413
|
+
$ref: '#/components/schemas/ProposalDiffLine'
|
|
2414
|
+
|
|
2415
|
+
ProposalDiffModifiedNode:
|
|
2416
|
+
type: object
|
|
2417
|
+
required: [id, type, changes]
|
|
2418
|
+
properties:
|
|
2419
|
+
id:
|
|
2420
|
+
type: string
|
|
2421
|
+
type:
|
|
2422
|
+
type: string
|
|
2423
|
+
name:
|
|
2424
|
+
type: string
|
|
2425
|
+
changes:
|
|
2426
|
+
type: array
|
|
2427
|
+
description: Changed properties only (modified nodes).
|
|
2428
|
+
items:
|
|
2429
|
+
$ref: '#/components/schemas/ProposalDiffPropertyChange'
|
|
2430
|
+
|
|
2431
|
+
ProposalDiffPresenceProperty:
|
|
2432
|
+
type: object
|
|
2433
|
+
required: [key, kind, value]
|
|
2434
|
+
properties:
|
|
2435
|
+
key:
|
|
2436
|
+
type: string
|
|
2437
|
+
kind:
|
|
2438
|
+
type: string
|
|
2439
|
+
enum: [scalar, text]
|
|
2440
|
+
value: {}
|
|
2441
|
+
preview:
|
|
2442
|
+
type: string
|
|
2443
|
+
enum: [html]
|
|
2444
|
+
description: When set, the /mcp UI may offer a sandboxed HTML preview.
|
|
2445
|
+
|
|
2446
|
+
ProposalDiffPresenceNode:
|
|
2447
|
+
type: object
|
|
2448
|
+
required: [id, type, summary, keyFields, properties]
|
|
2449
|
+
description: |
|
|
2450
|
+
Added or removed node as one readable block: summary sentence, key
|
|
2451
|
+
fields (name, wires by node name, code/HTML), and full properties.
|
|
2452
|
+
properties:
|
|
2453
|
+
id:
|
|
2454
|
+
type: string
|
|
2455
|
+
type:
|
|
2456
|
+
type: string
|
|
2457
|
+
name:
|
|
2458
|
+
type: string
|
|
2459
|
+
summary:
|
|
2460
|
+
type: string
|
|
2461
|
+
description: |
|
|
2462
|
+
Human sentence, e.g. Adds template node "demo-html" to flow "Test Flow".
|
|
2463
|
+
keyFields:
|
|
2464
|
+
type: array
|
|
2465
|
+
items:
|
|
2466
|
+
$ref: '#/components/schemas/ProposalDiffPresenceProperty'
|
|
2467
|
+
properties:
|
|
2468
|
+
type: array
|
|
2469
|
+
description: 'Full property list (UI: under All properties).'
|
|
2470
|
+
items:
|
|
2471
|
+
$ref: '#/components/schemas/ProposalDiffPresenceProperty'
|
|
2472
|
+
|
|
2473
|
+
ProposalDiffSections:
|
|
2474
|
+
type: object
|
|
2475
|
+
required: [summary, added, removed, modified]
|
|
2476
|
+
properties:
|
|
2477
|
+
summary:
|
|
2478
|
+
type: object
|
|
2479
|
+
required: [added, removed, modified, tabChanges]
|
|
2480
|
+
properties:
|
|
2481
|
+
added:
|
|
2482
|
+
type: integer
|
|
2483
|
+
removed:
|
|
2484
|
+
type: integer
|
|
2485
|
+
modified:
|
|
2486
|
+
type: integer
|
|
2487
|
+
tabChanges:
|
|
2488
|
+
type: array
|
|
2489
|
+
items:
|
|
2490
|
+
type: string
|
|
2491
|
+
added:
|
|
2492
|
+
type: array
|
|
2493
|
+
items:
|
|
2494
|
+
$ref: '#/components/schemas/ProposalDiffPresenceNode'
|
|
2495
|
+
removed:
|
|
2496
|
+
type: array
|
|
2497
|
+
items:
|
|
2498
|
+
$ref: '#/components/schemas/ProposalDiffPresenceNode'
|
|
2499
|
+
modified:
|
|
2500
|
+
type: array
|
|
2501
|
+
items:
|
|
2502
|
+
$ref: '#/components/schemas/ProposalDiffModifiedNode'
|
|
2503
|
+
|
|
2504
|
+
ProposalDiff:
|
|
2505
|
+
allOf:
|
|
2506
|
+
- $ref: '#/components/schemas/ProposalDiffSections'
|
|
2507
|
+
- type: object
|
|
2508
|
+
required:
|
|
2509
|
+
- proposalId
|
|
2510
|
+
- flowId
|
|
2511
|
+
- expectedRev
|
|
2512
|
+
- currentRev
|
|
2513
|
+
- baseRevStale
|
|
2514
|
+
- compareAgainst
|
|
2515
|
+
properties:
|
|
2516
|
+
proposalId:
|
|
2517
|
+
type: string
|
|
2518
|
+
flowId:
|
|
2519
|
+
type: string
|
|
2520
|
+
expectedRev:
|
|
2521
|
+
type: string
|
|
2522
|
+
currentRev:
|
|
2523
|
+
type: string
|
|
2524
|
+
baseRevStale:
|
|
2525
|
+
type: boolean
|
|
2526
|
+
description: |
|
|
2527
|
+
True only for pending proposals when expectedRev ≠ live rev.
|
|
2528
|
+
Never true for approved/rejected/failed.
|
|
2529
|
+
compareAgainst:
|
|
2530
|
+
type: string
|
|
2531
|
+
enum: [base, current]
|
|
2532
|
+
baseSnapshotMissing:
|
|
2533
|
+
type: boolean
|
|
2534
|
+
description: True for legacy proposals without a stored baseFlow
|
|
2535
|
+
vsCurrent:
|
|
2536
|
+
$ref: '#/components/schemas/ProposalDiffSections'
|
|
2537
|
+
description: |
|
|
2538
|
+
Present when pending and baseRevStale — proposal vs live flow.
|
|
2539
|
+
|
|
2540
|
+
AuditActor:
|
|
2541
|
+
oneOf:
|
|
2542
|
+
- type: object
|
|
2543
|
+
required: [kind, linkId]
|
|
2544
|
+
properties:
|
|
2545
|
+
kind:
|
|
2546
|
+
const: access-link
|
|
2547
|
+
linkId:
|
|
2548
|
+
type: string
|
|
2549
|
+
label:
|
|
2550
|
+
type: string
|
|
2551
|
+
- type: object
|
|
2552
|
+
required: [kind]
|
|
2553
|
+
properties:
|
|
2554
|
+
kind:
|
|
2555
|
+
const: admin
|
|
2556
|
+
username:
|
|
2557
|
+
type: string
|
|
2558
|
+
- type: object
|
|
2559
|
+
required: [kind]
|
|
2560
|
+
properties:
|
|
2561
|
+
kind:
|
|
2562
|
+
const: system
|
|
2563
|
+
|
|
2564
|
+
AuditEntry:
|
|
2565
|
+
type: object
|
|
2566
|
+
required: [id, at, action, actor]
|
|
2567
|
+
properties:
|
|
2568
|
+
id:
|
|
2569
|
+
type: string
|
|
2570
|
+
at:
|
|
2571
|
+
type: string
|
|
2572
|
+
format: date-time
|
|
2573
|
+
action:
|
|
2574
|
+
type: string
|
|
2575
|
+
actor:
|
|
2576
|
+
$ref: '#/components/schemas/AuditActor'
|
|
2577
|
+
details:
|
|
2578
|
+
type: object
|
|
2579
|
+
additionalProperties: true
|