@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 ADDED
@@ -0,0 +1,319 @@
1
+ # Changelog
2
+
3
+ All notable changes to Node-RED AI Access will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to
7
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+
9
+ ## [0.1.0] - 2026-10-16
10
+
11
+ First public release of **Node-RED AI Access**: an admin-gated Node-RED 4 plugin
12
+ that lets AI agents read flows and submit reviewable change proposals over REST,
13
+ MCP, and GET access links, with scoped, expiring tokens, a review UI, conflict
14
+ detection, an audit log, and an error-code registry shared by all channels.
15
+ Requires Node.js 20+.
16
+
17
+ ### Release
18
+
19
+ - Initial independent release of **Node-RED AI Access**
20
+ (`@com55/node-red-ai-access`), derived from
21
+ [ziv-daniel/node-red-mcp](https://github.com/ziv-daniel/node-red-mcp).
22
+ - Package metadata, Docker image names, documentation, and CI retargeted to
23
+ `com55/node-red-ai-access`.
24
+ - GitHub Actions CI runs on `ubuntu-latest` for all events.
25
+ - Requires Node.js `>=20`; CI tests Node 20, 22, and 23.
26
+ - `@modelcontextprotocol/sdk` raised to `^1.31.0`.
27
+ - Published package is a minified production build without source maps;
28
+ `prepublishOnly` runs quality checks, tests, and the production build. The
29
+ whole `docs/` folder ships; the OpenAPI spec exists once
30
+ (`docs/api/openapi.yaml`) and is served at `GET /mcp/api/v1/openapi.yaml`.
31
+ - MCP `list_proposals` `status: []` (or `""`) means no status filter; an invalid
32
+ status member returns the same `invalid_filter` message on every channel.
33
+
34
+ ### Fixed (pre-release review)
35
+
36
+ - `RateLimit-*` headers on token requests report the per-token bucket
37
+ (`300;w=60` by default) on every route; requests without a token report the
38
+ per-IP bucket.
39
+ - Spec also served as JSON at `GET /mcp/api/v1/openapi.json`.
40
+ - Proposal responses include `storedTitle` (the title as saved).
41
+ - `duplicate_node_id` checks the whole resulting flows file: the tab id, nodes
42
+ on other tabs, config nodes, and subflows.
43
+ - A single validation error returns its specific message at the top level.
44
+ - Hints come from the error registry on every channel (including
45
+ `node_already_exists` and a top-level `validation_failed` hint); `errors[]`
46
+ entries no longer carry `statusCode`.
47
+ - `status` list filter accepts several values (comma-separated or repeated on
48
+ REST/GET; array or comma string on MCP); any invalid value is
49
+ `invalid_filter`. Repeating a single-value filter is `invalid_filter`.
50
+ - `mine=false` returns only proposals created by other links.
51
+ - GET guide lists every error code, warnings, statuses (including `superseded`),
52
+ and `autoClosedCount`.
53
+
54
+ ### Changed
55
+
56
+ - List filters are validated on GET, REST, and MCP: bad `status`, `limit` (not
57
+ 1–200; no longer clamped), `offset`, or `mine` return **400** `invalid_filter`
58
+ naming the parameter and allowed values. MCP argument schema errors return the
59
+ same JSON body (`invalid_filter` / `invalid_arguments`) instead of raw Zod
60
+ output.
61
+ - List responses on every channel share `total`, `offset`, `limit` (when set),
62
+ `hasMore`, and `nextOffset` (offset-based pagination).
63
+ - Several validation errors in one proposal return **400** `validation_failed`
64
+ (`N validation errors`, all listed in `errors[]`); a single error keeps its
65
+ own code (e.g. **404** `node_not_found`).
66
+ - Out-of-range or non-positive `ttlSeconds` returns **400** `invalid_ttl` with a
67
+ hint stating the per-kind caps (was `access_link_invalid`).
68
+ - `staleOnOlderRev` removed (stale peers are auto-closed on read); responses
69
+ include `autoClosedCount` when a request auto-closed pending proposals.
70
+ - `minutesRemaining` rounds up; `/status` also reports `secondsRemaining`.
71
+ - `token_expired` includes `expiredAt` and `accessLinksUrl`; GET-link 401s
72
+ include `WWW-Authenticate`.
73
+
74
+ ### Added
75
+
76
+ - Duplicate node ids in a full tab or batch are rejected with
77
+ `duplicate_node_id`.
78
+ - Central error-code registry (`src/plugin/error-registry.ts`) generating
79
+ `docs/errors.md`, the GET guide error table, and the OpenAPI `ErrorCode` list
80
+ (`yarn generate:errors`; CI staleness check `yarn check:errors`).
81
+ - Channel onboarding on `/mcp`: REST copies a ready-to-paste AI prompt (base
82
+ URL, Bearer, OpenAPI + discovery, human-approve note, and a stop-and-ask-for-
83
+ GET-link line when custom headers are unavailable); MCP shows a setup panel
84
+ with Claude Desktop / Cursor / VS Code / Claude Code CLI / Streamable HTTP
85
+ tabs (each with its own copy button); GET link still copies the URL. Docs:
86
+ `docs/README.md`, `docs/mcp-clients.md`, `docs/errors.md`, generated
87
+ `docs/mcp-tools.md` (`yarn generate:mcp-tools` / CI stale check).
88
+ - Access-link TTL UX: `/mcp` expiry dropdown (15m / 1h / 4h / 24h / 7d /
89
+ Custom); `get_link` capped at 24h (7d hidden; switching to GET link with
90
+ expiry above 24h — including Custom — resets to 1 hour with a short note);
91
+ `rest` / `mcp` capped at 30d with a red warning above 24h. Server enforces the
92
+ same caps. Status for access-link callers includes `expiresAt` and
93
+ `minutesRemaining`. Known-expired tokens return **401** `token_expired`
94
+ (distinct from `access_link_invalid`).
95
+ - Auto-close stale pending proposals: `expired` (live rev moved) and
96
+ `superseded` (another proposal on the same flow was approved).
97
+ - Conflict trim: `conflictingProposals` lists same-rev or overlapping peers
98
+ only; other older-rev peers appear as `staleOnOlderRev` count. List items omit
99
+ `conflictingProposals`.
100
+ - GET withdraw requires `confirm=1` (prefetch-safe).
101
+ - Proposal list pagination and filters on GET/REST/MCP: `status`, `flowId`,
102
+ `limit`/`offset` (response `total`; max limit 200). MCP tool renamed to
103
+ `list_proposals` (`list_my_proposals` kept as alias).
104
+ - Proposal visibility: every access-link token can list/read **all** proposals
105
+ with `mine: true|false`; filter with `?mine=true` / MCP `list_proposals`
106
+ `{ mine: true }`.
107
+ - Cross-token conflict warnings on submit/read: `conflictingProposals` with
108
+ optional `overlappingNodeIds` / `overlappingFields` (replaces private
109
+ `otherLinkSiblingCount`).
110
+ - Withdraw own pending proposals (`withdrawn` status): GET
111
+ `…/proposals/<id>/withdraw?confirm=1`, REST `POST /proposals/{id}/withdraw`,
112
+ MCP `withdraw_proposal` (+ `/mcp` pill + audit `proposal.withdrawn`).
113
+ - Docs: Known issues (per-token rate limits stack), 1.5MB cap on every channel,
114
+ canonical REST/MCP `payload.nodes` op shapes.
115
+ - Phase 2C: GET `/mcp/a/<token>` cannot mint tokens — explicit **405**
116
+ `link_create_not_supported` on `/mcp/a/<token>/links`; OpenAPI documents
117
+ `AccessLinkKind`, `CreateLinkRequest.kind`, `wrong_token_kind` /
118
+ `invalid_link_kind`, and that get-link has no create-link operation; discovery
119
+ advertises admin-only `POST /links` mint and `getLink.canMintTokens: false`.
120
+
121
+ ### Fixed
122
+
123
+ - Access-link rate limits share one per-token bucket across GET
124
+ `/mcp/a/<token>`, REST Bearer, and MCP (same limiter instances + unified token
125
+ key).
126
+ - Layered-read JSON responses always set `X-Content-Type-Options: nosniff` and
127
+ never echo path/query/exception text into error bodies (CodeQL XSS / HTML).
128
+
129
+ ### Changed
130
+
131
+ - `/mcp` human-UX pass: status dashboard (AI on, active links, pending count,
132
+ expiry in hours), Pending/Decided proposal filters with tab badge, compact
133
+ decided cards, plain one-line diff summaries, stale dismiss + large-delete
134
+ typed confirm, Approve & deploy with deploy-type Advanced help, reject dialog,
135
+ access-link TTL presets (1h/4h/24h), Created/Last used link table, audit
136
+ labels without event codes, sticky tab nav, sign-in gate (no silent bounce),
137
+ and sidebar TTL hours + Review deep link to `/mcp#proposals`.
138
+
139
+ ### Added
140
+
141
+ - Targeted `replace_text` op for string fields (`template`, `func`, …): unique
142
+ `find` or 1-based `occurrence`; errors `400 text_not_found` /
143
+ `409 ambiguous_match` (with `matchCount`). Available via GET propose
144
+ (`node=&field=&find=&replace=`), patch JSON, and REST/MCP `submit_proposal`
145
+ (`payload.nodes` without a full tab). Multiple ops apply in order. AI guide
146
+ recommends it for small edits to long fields and documents a last-resort
147
+ placeholder→paste workflow when content exceeds draft limits.
148
+ - Layered reads on GET link, REST, and MCP (same auth/rate limits):
149
+ `GET …/flows/{id}?summary=1` (node id/type/name/wires + bytes, no field
150
+ bodies); `GET …/nodes/{id}` (one full node + flowId);
151
+ `GET …/nodes/{id}/field/{name}?lines=A-B` (optional line range +
152
+ `totalLines`/`bytes`); `GET …/search?q=&flow=` (hits with node/field/lines/
153
+ snippet; capped with `truncated:true`). Recommended loop: summary → search →
154
+ read line range → `replace_text` → propose. MCP tools: `get_flow` (`summary`),
155
+ `get_node`, `get_node_field`, `search_flows`.
156
+
157
+ ### Changed
158
+
159
+ - `/mcp/api` and `/mcp/a` use dual in-memory rate limits (per IP and per access
160
+ token) with configurable `aiAccess.rateLimitWindowMs` / `rateLimitMaxPerIp` /
161
+ `rateLimitMaxPerToken` (generous defaults: 600/min IP, 300/min token).
162
+ Exceeded limits return **429** `{ code: 'rate_limited', retryAfter }` and a
163
+ `Retry-After` header.
164
+ - Proposal `reviewUrl` is now an **absolute** URL from the request Host/protocol
165
+ (`X-Forwarded-*` only when Express `trust proxy` is set). Responses also
166
+ include relative `reviewPath` (`/mcp#proposal=<id>`).
167
+ - `displayName` datetime includes an explicit UTC offset and is documented as
168
+ **server local time** (for AI clients). The `/mcp` UI never shows that server
169
+ string: proposal headings and audit labels rebuild the title and format
170
+ `createdAt` / `at` with `Intl.DateTimeFormat` in the **browser** timezone.
171
+ `createdAt` remains ISO-8601.
172
+ - `/mcp` is served with `Cache-Control: no-store` so plugin upgrades do not keep
173
+ serving a cached admin client (inline CSS/JS).
174
+ - `/mcp` location hash reflects the active tab (`#status`, `#links`,
175
+ `#proposals`, `#audit`) via `history.replaceState`. `#proposal=<id>` is only
176
+ used while focusing a proposal deep link and is replaced with `#proposals`
177
+ after the scroll/highlight is consumed (so reload / returning to Proposals
178
+ does not re-trigger).
179
+
180
+ ### Fixed
181
+
182
+ - Side-by-side (split) diffs for large field replaces pair remove/add runs so
183
+ the AFTER column is visible instead of a long blank placeholder stack.
184
+ - Unknown flow ids return **404** `flow_not_found` (with hint to use ids from
185
+ `GET ./flows`) instead of **502** `upstream_error` on RedAdapter.
186
+ - Invalid draft chunk base64url (e.g. `@@@`) returns **400**
187
+ `invalid_chunk_data` and does not store the seq.
188
+ - `set.wires` is allowed in patch/propose when targets exist in the resulting
189
+ flow (validated); id/type/z stay protected.
190
+ - Added/removed node diffs include `wires` (readable) and `x`/`y`.
191
+ - `draft/new` checks live rev immediately (**409** `version_mismatch`); stale
192
+ submit responses set `draftDeleted: false` when the draft was kept.
193
+ - REST `Bearer bogus` / short tokens return `access_link_invalid` (with a
194
+ truncated-URL hint when applicable), not `access_link_missing`.
195
+ - Proposal diffs default to compact `format=summary` (changed lines + context +
196
+ byte sizes); `format=full` restores complete text.
197
+ - Re-submitting a draft returns **409** `draft_already_submitted` with
198
+ `proposalId`; malformed sha256 returns **400** `invalid_sha256`.
199
+ - Propose/draft responses warn with `siblingsOnSameRev` when other pending
200
+ proposals share the same flow revision.
201
+
202
+ ### Added
203
+
204
+ - Human-friendly proposals: optional `title` (≤80 chars) on all submit paths
205
+ (REST, MCP `submit_proposal`, GET `propose`, draft new/submit); responses
206
+ always include `title`, `displayName` (`title ·` server-local datetime), and
207
+ `reviewUrl` (`/mcp#proposal=<id>`). AI guides tell clients to cite
208
+ `displayName` + `reviewUrl` (not the raw id). `/mcp` list headings and audit
209
+ details use `displayName`; deep links open/scroll/highlight the proposal.
210
+
211
+ ### Changed
212
+
213
+ - Node added/removed diff blocks use a readable summary sentence, show key
214
+ fields (name, wires by node name, code/HTML), and hide the rest under “All
215
+ properties”. HTML templates offer a sandboxed iframe Preview toggle (`sandbox`
216
+ with no `allow-scripts`, `srcdoc`) plus the code view.
217
+ - `/mcp` Access Links: Copy link on each active row and on create (brief Copied
218
+ state + clipboard fallback); empty states; relative timestamps with absolute
219
+ on hover; confirm before revoke/approve/reject. List API includes `url` for
220
+ active in-memory links (raw `token` still create-only). Diff lines wrap in
221
+ unified and side-by-side views.
222
+
223
+ ### Fixed
224
+
225
+ - `/mcp` proposal diffs default to side-by-side on wide screens (unified below
226
+ the narrow breakpoint) and remember the toggle in `localStorage`; short
227
+ single-line fields in Node added/removed blocks stay inline (only long
228
+ multi-line text collapses).
229
+ - Proposal diffs compare against the **base flow snapshot** stored at submit
230
+ (`baseFlow`), so later approvals no longer make unrelated nodes look
231
+ “removed”. Pending+stale still warns and may include `vsCurrent`;
232
+ approved/rejected/failed never show the approve-would-fail banner.
233
+ - Added/removed nodes in the `/mcp` UI render as single “Node added” / “Node
234
+ removed” blocks (properties collapsed; long text expandable).
235
+ - Unknown paths under `/mcp/a/*` return JSON `{ code: not_found }` without
236
+ echoing the access-link token (no HTML `Cannot GET`).
237
+ - Draft `append` `totalBytes` counts **decoded** chunk bytes vs `maxBytes` (not
238
+ base64url character length).
239
+ - Field-mode `draft/new` validates that `node`/`field` exist on the live flow
240
+ before upload (`mode=json` may add/remove nodes and skips that check).
241
+ - Proposal diffs for added/removed nodes include all properties; long text
242
+ fields (`template`, `func`, …) are line-diffed as all-added / all-removed.
243
+ - Draft append “no gaps” hint no longer reads like the upload is finished; error
244
+ hints are code-specific (e.g. `chunk_conflict` vs stale rev).
245
+ - `/mcp` audit log actor cells expose the full label via `title` when truncated;
246
+ sidebar AI Access TTL/deploy labels stack above their inputs.
247
+ - `/mcp` page uses a wide responsive container (`max-width` ~1400px / 95vw);
248
+ proposal diffs default to side-by-side on wide screens with a unified toggle;
249
+ long code/diff blocks scroll horizontally with monospace line numbers.
250
+
251
+ ### Added
252
+
253
+ - GET patch / `mode=json` drafts support `nodes:[{op:'add'|'remove'|'set',...}]`
254
+ (plus full `replace_flow`); docs cover exact JSON shapes and ~4–5 KB raw chunk
255
+ guidance.
256
+ - Self-contained GET access links at `/mcp/a/<token>` for chat AIs that can only
257
+ HTTP GET (markdown docs; scoped status/flows/proposals/propose/drafts; chunked
258
+ drafts for large fields; `Cache-Control: no-store`; per-link rate limit; token
259
+ redaction in paths). Create-link responses include the full `url`; default
260
+ link TTL is **4 hours** (configurable).
261
+
262
+ ### Fixed
263
+
264
+ - Shared-read REST routes (`/flows`, `/proposals`, …) return
265
+ `access_link_missing` / `access_link_invalid` with Bearer `WWW-Authenticate`
266
+ (not passport `auth_required` / realm=`Users`) when an access-link token is
267
+ missing or invalid.
268
+ - `GET /flows/{id}` always includes `flow.type` (defaults to `tab`) so REST
269
+ matches OpenAPI `FlowTab` and MCP `get_flow`.
270
+ - Unknown paths under `/mcp/api/v1` return JSON `{ code: not_found }` instead of
271
+ HTML `Cannot GET`.
272
+
273
+ ### Added
274
+
275
+ - `GET /mcp/api/v1` unauthenticated discovery index (endpoints, auth scheme,
276
+ OpenAPI URL, MCP endpoint).
277
+ - `GET /status` accepts access-link Bearer and returns caller label, expiry,
278
+ permissions, and base URLs.
279
+ - OpenAPI “Quick start for access-link clients” with example
280
+ `CreateProposalRequest`; `/mcp` **Copy link for AI** includes API base + docs
281
+ hint with the token.
282
+
283
+ ### Removed
284
+
285
+ - Legacy standalone MCP server (Express/OAuth/SSE entrypoints, Docker images,
286
+ Playwright e2e against `/health`, and related dependencies). The Node-RED
287
+ plugin (`/mcp`, `/mcp/api/v1`, `/mcp/rpc`) is the supported surface.
288
+
289
+ ### Added
290
+
291
+ - Node-RED plugin scaffold (`ai-access.cjs` / `dist/plugin.cjs`) with `GET /mcp`
292
+ admin page and `GET /mcp/api/v1/status`.
293
+ - `aiAccess` settings hook (`allowWithoutAdminAuth` and reserved fields for
294
+ later link TTL, max links, and deployment type).
295
+ - Optional `node-red` peerDependency (`>=4.0.0`).
296
+ - MCP Streamable HTTP endpoint at `POST /mcp/rpc` (access-link Bearer tools
297
+ mirroring the AI-facing REST API).
298
+ - Flow runtime adapter (`RedAdapter` via `RED.runtime`, `HttpAdapter` Admin API
299
+ fallback, factory selection) with rev-checked `updateFlow` (default deployment
300
+ type `nodes`).
301
+ - In-memory access-link store and admin routes under `/mcp/api/v1/links`
302
+ (default TTL 24h, max 10 active, token shown once; Bearer middleware for AI
303
+ clients).
304
+ - On-disk proposal store and audit log under `<userDir>/.ai-access`, plus REST
305
+ routes for proposals, flows (with rev), and logs. Approve deploys via
306
+ FlowAdapter with rev checks; concurrent approve guarded.
307
+ - Trusted write helper for `.ai-access` (call-site file-write suppressions
308
+ removed; sole trust-boundary annotations live in the helper): directory mode
309
+ `0o700`, file mode `0o600`, and size caps on proposal JSON and audit appends
310
+ (`413` when exceeded).
311
+ - `/mcp` admin UI for access links (TTL, show-once token, revoke), proposals
312
+ (summary, approve with deploy type, reject, rev-conflict messages), audit log,
313
+ and status. Read-only for `read` users; mutating controls for `*` only.
314
+ Optional `deploymentType` on approve; status exposes link/deploy defaults.
315
+ - OpenAPI 3.1 spec for `/mcp/api/v1` (status, links, flows, proposals, logs,
316
+ auth, 413 limits) served at `GET /mcp/api/v1/openapi.yaml`; README install,
317
+ config, `/mcp` usage, and AI quickstart.
318
+
319
+ [0.1.0]: https://github.com/com55/node-red-ai-access/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) ziv-daniel
4
+ Copyright (c) 2026 com55
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,270 @@
1
- # Temporary Holding Version
1
+ # Node-RED AI Access
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen.svg)](https://nodejs.org/)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7%2B-blue.svg)](https://www.typescriptlang.org/)
6
+ [![CI](https://github.com/com55/node-red-ai-access/actions/workflows/ci.yml/badge.svg)](https://github.com/com55/node-red-ai-access/actions)
7
+
8
+ Temporary, owner-approved Node-RED access for AI clients. Ships as a **Node-RED
9
+ 4.x+ plugin** with:
10
+
11
+ - an admin page at `/mcp`
12
+ - a versioned REST API under `/mcp/api/v1`
13
+ - an MCP JSON-RPC endpoint at `/mcp/rpc` (Streamable HTTP) over the same
14
+ services
15
+
16
+ **Git mental model:** a proposal is like a pull request. Human approve = merge;
17
+ reject = close; withdraw = the author closes their own PR. If `rev` /
18
+ `expectedRev` does not match, treat it as a conflict: re-read the flow, rebase
19
+ the edit, and resubmit. Every access link can see all proposals
20
+ (`mine: true|false`).
21
+
22
+ Derived from
23
+ [ziv-daniel/node-red-mcp](https://github.com/ziv-daniel/node-red-mcp).
24
+
25
+ ## Install (Node-RED plugin)
26
+
27
+ Requirements: **Node-RED 4.x or newer**, Node.js 20+, and **`adminAuth`
28
+ enabled** (recommended). Without `adminAuth`, AI features stay off unless you
29
+ opt in (see Configuration).
30
+
31
+ 1. Install into your Node-RED user directory (or via the palette):
32
+
33
+ ```bash
34
+ cd ~/.node-red
35
+ npm install @com55/node-red-ai-access
36
+ ```
37
+
38
+ 2. Restart Node-RED.
39
+ 3. Log in to the editor, then open **`/mcp`** on the admin root (for example
40
+ `http://localhost:1880/mcp`, or `{httpAdminRoot}/mcp` when `httpAdminRoot` is
41
+ set).
42
+
43
+ The `/mcp` HTML shell is served without adminAuth so the page can read the
44
+ editor token from browser `localStorage` (Node-RED does not use cookies) and
45
+ call APIs with `Authorization: Bearer`. If no valid token is present, the page
46
+ redirects to the editor login. All data and actions stay behind permission
47
+ checks.
48
+
49
+ On `/mcp` you can create access links, review proposals (with a readable
50
+ node/property diff against the live flow), approve or reject changes, and read a
51
+ human-readable audit log. Users with `read` (including guest, if configured) get
52
+ a read-only view; only users with `*` can create/revoke links and approve/reject
53
+ proposals.
54
+
55
+ ## Configuration
56
+
57
+ In `settings.js`:
58
+
59
+ ```js
60
+ module.exports = {
61
+ // adminAuth: { ... }, // required for AI features in normal setups
62
+ aiAccess: {
63
+ // Master switch (default: true). Also editable in the editor sidebar tab.
64
+ enabled: true,
65
+ // Local-only opt-in when adminAuth is disabled (default: false)
66
+ allowWithoutAdminAuth: false,
67
+ // Access-link default TTL in seconds (default: 14400 = 4h)
68
+ defaultLinkTtlSeconds: 14400,
69
+ // Max concurrent active access links (default: 10)
70
+ maxActiveLinks: 10,
71
+ // Deploy type used on approve (default: "nodes"; also "flows" | "full")
72
+ defaultDeploymentType: 'nodes',
73
+ // In-memory rate limits for /mcp/api and /mcp/a (per IP + per access token)
74
+ rateLimitWindowMs: 60000,
75
+ rateLimitMaxPerIp: 600,
76
+ rateLimitMaxPerToken: 300,
77
+ },
78
+ };
79
+ ```
80
+
81
+ ### Settings precedence
82
+
83
+ Effective values are resolved **without restart** in this order (highest wins):
84
+
85
+ 1. **Disk** — `<userDir>/.ai-access/settings.json` (editor sidebar **AI Access**
86
+ tab, or `PUT /mcp/api/v1/settings`)
87
+ 2. **`settings.js`** — `aiAccess` block (install / bootstrap defaults)
88
+ 3. **Built-in defaults** — as documented above
89
+
90
+ Only keys present on disk override; omitted keys fall through to `settings.js`,
91
+ then defaults. The editor right-hand sidebar tab (**AI Access**, next to
92
+ Information / Help / Debug / Configuration nodes / Context Data) lets `*` admins
93
+ change enable/disable, default deploy type, and default access-link TTL; `read`
94
+ users see the same values read-only. A button opens the `/mcp` page. When
95
+ `adminAuth` is off, the tab exposes the `allowWithoutAdminAuth` opt-in with a
96
+ clear local-only warning.
97
+
98
+ Storage lives under **`<userDir>/.ai-access`** (`settings.json`,
99
+ `proposals/*.json`, and append-only `logs/audit.jsonl`). Directories use mode
100
+ `0700`, files `0600`. Proposal JSON is capped at **1_500_000 bytes**; oversized
101
+ proposal JSON or audit lines return HTTP **413** with `code: payload_too_large`
102
+ (never silently truncated). Malformed JSON returns **400** JSON (REST
103
+ `invalid_json`, MCP JSON-RPC `-32700`) — never HTML stack traces (middleware is
104
+ attached to the editor-api app that owns body-parser, not only the nested plugin
105
+ `httpAdmin`). Unauthenticated `/mcp/api/v1` admin calls return JSON
106
+ `{ code: "auth_required" }` rather than plain-text `Unauthorized`.
107
+
108
+ ## Docs
109
+
110
+ - [Channel index](docs/README.md) — GET link / REST / MCP overview
111
+ - [MCP client setup](docs/mcp-clients.md) — Claude Desktop, Cursor, VS Code, CLI
112
+ - [MCP tools reference](docs/mcp-tools.md) — generated from live `tools/list`
113
+ - [Error codes](docs/errors.md) — code, status, hint, channels
114
+ - [OpenAPI 3.1](docs/api/openapi.yaml) — also `GET /mcp/api/v1/openapi.yaml`
115
+ (JSON: `GET /mcp/api/v1/openapi.json`)
116
+
117
+ Regenerate the MCP tools doc after tool changes: `yarn generate:mcp-tools`.
118
+
119
+ ## REST API
120
+
121
+ OpenAPI: **`GET /mcp/api/v1/openapi.yaml`** or
122
+ **`GET /mcp/api/v1/openapi.json`** (also in
123
+ [`docs/api/openapi.yaml`](docs/api/openapi.yaml)).
124
+
125
+ | Path | Purpose |
126
+ | ---------------------------------------- | ------------------------------------------------ |
127
+ | `GET /mcp` | Admin UI (status, links, proposals, audit log) |
128
+ | `GET /mcp/a/<token>` | Self-contained GET link (markdown docs) |
129
+ | `GET /mcp/a/<token>/…` | Token-scoped GET status/flows/propose/drafts |
130
+ | `POST /mcp/rpc` | MCP Streamable HTTP (access-link Bearer; tools) |
131
+ | `GET /mcp/api/v1` | Discovery index (no auth; endpoints + auth hint) |
132
+ | `GET /mcp/api/v1/openapi.yaml` | OpenAPI 3.1 specification (YAML) |
133
+ | `GET /mcp/api/v1/openapi.json` | OpenAPI 3.1 specification (JSON) |
134
+ | `GET /mcp/api/v1/status` | Plugin status JSON (admin or access link) |
135
+ | `GET /mcp/api/v1/settings` | Effective AI Access settings (admin `read`) |
136
+ | `PUT /mcp/api/v1/settings` | Update settings (`*`; persists to disk, hot) |
137
+ | `GET /mcp/api/v1/links` | List active access links (`*` only) |
138
+ | `POST /mcp/api/v1/links` | Create access link (`*` only; token once) |
139
+ | `DELETE /mcp/api/v1/links/:id` | Revoke access link (`*` only) |
140
+ | `GET /mcp/api/v1/flows` | List flows + rev (admin `read` or access link) |
141
+ | `GET /mcp/api/v1/flows/:id` | Get one flow + rev (admin `read` or access link) |
142
+ | `POST /mcp/api/v1/proposals` | Create proposal (access-link Bearer only) |
143
+ | `GET /mcp/api/v1/proposals` | List proposals (admin: all; link: own) |
144
+ | `GET /mcp/api/v1/proposals/:id` | Get proposal (admin: any; link: own) |
145
+ | `GET /mcp/api/v1/proposals/:id/diff` | Diff proposal vs current flow (admin `read`/own) |
146
+ | `POST /mcp/api/v1/proposals/:id/approve` | Approve + deploy (`*`; optional deploymentType) |
147
+ | `POST /mcp/api/v1/proposals/:id/reject` | Reject with reason (`*`; no flow mutate) |
148
+ | `GET /mcp/api/v1/logs` | Append-only audit log (admin `read`) |
149
+
150
+ ### MCP JSON-RPC (`POST /mcp/rpc`)
151
+
152
+ AI clients can use the official MCP Streamable HTTP transport against
153
+ `{httpAdminRoot}/mcp/rpc` with the same access-link Bearer token as the REST
154
+ API. Tools mirror the AI-facing REST surface only:
155
+
156
+ `get_status`, `list_flows`, `get_flow`, `submit_proposal` (including
157
+ `replace_flow` with `expectedRev`; `expected_rev` accepted as alias; optional
158
+ `title` ≤80 chars), `list_proposals` (alias `list_my_proposals`),
159
+ `get_proposal`. Tool descriptions document purpose, inputs, returns, and when to
160
+ use them. Proposal create/list/get responses include `title`, `displayName`
161
+ (`title ·` **server-local** datetime with explicit offset, e.g.
162
+ `16 Oct 01:35 +00:00`), `reviewUrl` (absolute URL using the request
163
+ Host/protocol — `X-Forwarded-*` only when Express `trust proxy` is set), and
164
+ `reviewPath` (relative `/mcp#proposal=<id>`). `createdAt` remains ISO-8601. The
165
+ `/mcp` page renders times in the browser's local timezone. AI clients should
166
+ refer to proposals by `displayName` + `reviewUrl`, not the raw id. For
167
+ access-link callers, `get_status` reports `permission: null` and
168
+ `caller.kind: "access-link"` (expected — AI tokens are not admin sessions).
169
+
170
+ Approve, reject, and access-link management stay human-only on the `/mcp` admin
171
+ page (and the corresponding REST routes). Rate limits, payload size limits, and
172
+ audit logging match REST. Missing tokens return **401** `access_link_missing`;
173
+ known-expired tokens return **401** `token_expired`; other invalid/revoked
174
+ tokens return **401** `access_link_invalid` (all with `WWW-Authenticate`)
175
+ telling the AI to ask the user for a new access link. Every error JSON includes
176
+ a `code`. `token_expired` also includes `expiredAt` and `accessLinksUrl` (the
177
+ `/mcp` page's Access links section). Access-link `/status` includes `expiresAt`,
178
+ `minutesRemaining` (rounded up), and `secondsRemaining`. See `docs/errors.md`
179
+ (generated from `src/plugin/error-registry.ts`).
180
+
181
+ Access links are **in memory** (default TTL **4h**, max 10). Caps: **get_link ≤
182
+ 24h**, **rest/mcp ≤ 30d**. Creating a link returns the raw `token` plus a
183
+ self-contained `url` for `get_link` (`/mcp/a/<token>`). While the link remains
184
+ active, `GET /links` includes `url` again so admins can re-copy from `/mcp` (the
185
+ raw `token` field stays create-only). After a restart every old token is
186
+ invalid.
187
+
188
+ Call `list_flows` or `get_flow` first and pass the returned `rev` as
189
+ `expectedRev`. Submit checks the live rev (**409** if stale). Approve may send
190
+ `deploymentType`: `nodes` (default), `flows`, or `full`. A stale `expectedRev`
191
+ at approve marks the proposal **rejected** with a resubmit message; other deploy
192
+ errors mark it **failed**. Reject never mutates flows.
193
+
194
+ ### Flow runtime adapter
195
+
196
+ - **RedAdapter** — in-process flows API (`RED.runtime.flows`, or the initialized
197
+ `@node-red/runtime` singleton when the plugin RED object has no `.runtime`)
198
+ - **HttpAdapter** — localhost Admin API v2 fallback only when credentials are
199
+ available (or adminAuth is off). Never selected silently without credentials
200
+ when adminAuth is on; upstream 401/403 becomes `plugin_misconfigured`
201
+ - **`updateFlow`** — requires `expectedRev`; default deployment type `nodes`
202
+
203
+ ## AI quickstart
204
+
205
+ ### Self-contained GET link (chat AIs that can only HTTP GET)
206
+
207
+ 1. An owner with `*` opens `/mcp` → **Access links** → create a **GET link**
208
+ (default TTL **4 hours**, max **24 hours**). Copy the full URL
209
+ `http(s)://<host><httpAdminRoot>/mcp/a/<token>` (or **Copy link for AI**).
210
+ 2. The AI opens that URL for markdown docs, then uses GET-only routes under the
211
+ same path (`/status`, `/flows`, `/flows/<id>`, `/proposals`, `/propose` with
212
+ preferred `replace_text` for small string edits, chunked `/draft/…` for large
213
+ HTML/fields; URL soft-cap ~8KB; prefer ~4–5 KB raw chunks — base64url adds
214
+ ~33%). Patch / `mode=json` drafts support
215
+ `nodes:[{op:'add'|'remove'|'set',...}]` (or a full `replace_flow` document).
216
+ Field drafts validate node/field at `draft/new`. Unknown `/mcp/a/*` paths
217
+ return JSON `not_found` without echoing the token.
218
+ 3. Changes become **pending proposals**; a human approves on `/mcp`. JSON
219
+ responses use `Cache-Control: no-store`.
220
+
221
+ ### Bearer REST / MCP
222
+
223
+ 1. Optional: `GET /mcp/api/v1` (no auth) lists endpoints, auth scheme, OpenAPI,
224
+ and MCP URLs. Spec: `GET /mcp/api/v1/openapi.yaml`.
225
+ 2. Send `Authorization: Bearer <token>` to REST and/or MCP:
226
+
227
+ - REST: `GET /mcp/api/v1/status`, `GET /mcp/api/v1/flows`,
228
+ `GET /mcp/api/v1/flows/{id}?summary=1`, `GET /mcp/api/v1/nodes/{id}`,
229
+ `GET /mcp/api/v1/nodes/{id}/field/{name}`, `GET /mcp/api/v1/search`,
230
+ `POST /mcp/api/v1/proposals`, `GET /mcp/api/v1/proposals`
231
+ - MCP: `POST /mcp/rpc` tools `list_flows`, `get_flow` (optional `summary`),
232
+ `get_node`, `get_node_field`, `search_flows`, `submit_proposal`,
233
+ `list_proposals` / alias `list_my_proposals` (same access-link auth and
234
+ limits)
235
+
236
+ 3. The owner reviews the proposal on `/mcp` and approves or rejects it.
237
+ 4. If the token is invalid, expired, revoked, or cleared by a restart, the API
238
+ returns **401** telling the AI to **ask the user for a new access link**.
239
+
240
+ Example create body:
241
+
242
+ ```json
243
+ {
244
+ "type": "replace_flow",
245
+ "expectedRev": "abc123",
246
+ "title": "Rename inject to Hello",
247
+ "payload": {
248
+ "flowId": "tab-1",
249
+ "flow": {
250
+ "id": "tab-1",
251
+ "type": "tab",
252
+ "label": "Main",
253
+ "nodes": []
254
+ }
255
+ }
256
+ }
257
+ ```
258
+
259
+ ## Development
260
+
261
+ ```bash
262
+ yarn install
263
+ yarn build
264
+ yarn test
265
+ yarn quality
266
+ ```
267
+
268
+ ## License
269
+
270
+ MIT — see [LICENSE](LICENSE).
package/ai-access.cjs ADDED
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CommonJS shim for Node-RED plugin loading.
5
+ * Node-RED requires() this file; the implementation lives in dist/plugin.cjs.
6
+ */
7
+ const plugin = require('./dist/plugin.cjs');
8
+ module.exports = plugin.default ?? plugin;
package/ai-access.html ADDED
@@ -0,0 +1,34 @@
1
+ <script>
2
+ /**
3
+ * Thin loader for the AI Access editor sidebar.
4
+ * Logic lives at GET /mcp/editor/ai-access-sidebar.js (Cache-Control: no-store)
5
+ * so companion-HTML caching does not freeze sidebar UX across plugin updates.
6
+ * Asset version is derived from package.json at build time.
7
+ */
8
+ (function () {
9
+ if (typeof RED === 'undefined' || !RED.sidebar || !RED.sidebar.addTab) {
10
+ return;
11
+ }
12
+ if (window.__com55AiAccessSidebarLoading) {
13
+ return;
14
+ }
15
+ window.__com55AiAccessSidebarLoading = true;
16
+
17
+ var EDITOR_SIDEBAR_ASSET_VERSION = '0.1.0';
18
+ var root = (RED.settings.httpAdminRoot || '/').replace(/\/$/, '');
19
+ var src =
20
+ (root || '') +
21
+ '/mcp/editor/ai-access-sidebar.js?v=' +
22
+ encodeURIComponent(EDITOR_SIDEBAR_ASSET_VERSION);
23
+
24
+ var s = document.createElement('script');
25
+ s.src = src;
26
+ s.async = false;
27
+ s.onerror = function () {
28
+ if (typeof console !== 'undefined' && console.error) {
29
+ console.error('[ai-access] failed to load sidebar script:', src);
30
+ }
31
+ };
32
+ (document.head || document.documentElement).appendChild(s);
33
+ })();
34
+ </script>