@com55/node-red-ai-access 0.0.0-stage → 0.1.1

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/README.md CHANGED
@@ -1,3 +1,334 @@
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
+ // Public base URL for absolute links (access-link url, reviewUrl,
78
+ // accessLinksUrl). Set this behind a TLS-terminating proxy (default: unset)
79
+ // publicBaseUrl: 'https://nodered.example.com',
80
+ // Browser origins allowed to call /mcp/a, /mcp/api, and /mcp/rpc
81
+ // cross-origin (default: none — no CORS headers are sent)
82
+ // corsAllowedOrigins: ['https://app.example.com'],
83
+ },
84
+ };
85
+ ```
86
+
87
+ ### Absolute URLs behind a reverse proxy
88
+
89
+ Access-link `url`, proposal `reviewUrl`, and `accessLinksUrl` (in
90
+ `token_expired`) are absolute URLs built by one shared helper:
91
+
92
+ 1. `aiAccess.publicBaseUrl`, when set, is used as-is (scheme, host, and an
93
+ optional path prefix). It is read from `settings.js` only.
94
+ 2. Otherwise, when Express `trust proxy` is enabled (for example
95
+ `httpServerOptions: { 'trust proxy': true }`), the first `X-Forwarded-Proto`
96
+ and `X-Forwarded-Host` values are used.
97
+ 3. Otherwise the request `Host` header and the connection protocol are used;
98
+ `X-Forwarded-*` headers are ignored so clients cannot spoof link origins.
99
+
100
+ Forwarded and `Host` values are validated: the scheme must be `http` or `https`
101
+ (else the connection scheme is used) and the host must be a DNS name, IPv4, or
102
+ bracketed IPv6 literal with an optional port. An invalid forwarded host falls
103
+ back to `Host`; an invalid `Host` yields relative paths. The MCP client snippets
104
+ in the GET-link guide use the same origin, and show a `https://YOUR-NODE-RED`
105
+ placeholder only when no origin can be determined.
106
+
107
+ Behind a TLS-terminating proxy (for example Cloudflare) with neither option set,
108
+ links therefore start with `http://`.
109
+
110
+ ### CORS
111
+
112
+ CORS only affects code running in a browser (a web page calling the plugin from
113
+ another origin). Server-side AI fetchers and agents (for example Gemini,
114
+ ChatGPT, or an MCP client running outside a browser) ignore CORS and are not
115
+ affected by this setting.
116
+
117
+ By default the AI-facing endpoints (`/mcp/a/*`, `/mcp/api/*`, `/mcp/rpc`) send
118
+ **no** CORS headers, overriding Node-RED's built-in
119
+ `Access-Control-Allow-Origin: *` for those paths. To allow a browser app, list
120
+ its origin in `aiAccess.corsAllowedOrigins` (`settings.js` only, restart
121
+ required). For a listed `Origin` the response echoes it in
122
+ `Access-Control-Allow-Origin` with `Vary: Origin`, and `OPTIONS` preflight
123
+ returns `204` with allowed methods (`GET, POST, DELETE, OPTIONS`) and headers
124
+ (`Authorization`, `Content-Type`, `Accept`, `Mcp-Session-Id`,
125
+ `Mcp-Protocol-Version`, `Last-Event-ID`). Origins match case-insensitively and
126
+ the configured origin is echoed; malformed origins are rejected. Exposed headers
127
+ include `Mcp-Session-Id`, `WWW-Authenticate`, `Retry-After`, and the
128
+ `RateLimit*` headers. Other origins get no CORS headers.
129
+
130
+ ### Settings precedence
131
+
132
+ Effective values are resolved **without restart** in this order (highest wins):
133
+
134
+ 1. **Disk** — `<userDir>/.ai-access/settings.json` (editor sidebar **AI Access**
135
+ tab, or `PUT /mcp/api/v1/settings`)
136
+ 2. **`settings.js`** — `aiAccess` block (install / bootstrap defaults)
137
+ 3. **Built-in defaults** — as documented above
138
+
139
+ Only keys present on disk override; omitted keys fall through to `settings.js`,
140
+ then defaults. The editor right-hand sidebar tab (**AI Access**, next to
141
+ Information / Help / Debug / Configuration nodes / Context Data) lets `*` admins
142
+ change enable/disable, default deploy type, and default access-link TTL; `read`
143
+ users see the same values read-only. A button opens the `/mcp` page. When
144
+ `adminAuth` is off, the tab exposes the `allowWithoutAdminAuth` opt-in with a
145
+ clear local-only warning.
146
+
147
+ Storage lives under **`<userDir>/.ai-access`** (`settings.json`,
148
+ `proposals/*.json`, and append-only `logs/audit.jsonl`). Directories use mode
149
+ `0700`, files `0600`. Proposal JSON is capped at **1_500_000 bytes**; oversized
150
+ proposal JSON or audit lines return HTTP **413** with `code: payload_too_large`
151
+ (never silently truncated). Malformed JSON returns **400** JSON (REST
152
+ `invalid_json`, MCP JSON-RPC `-32700`) — never HTML stack traces (middleware is
153
+ attached to the editor-api app that owns body-parser, not only the nested plugin
154
+ `httpAdmin`). Unauthenticated `/mcp/api/v1` admin calls return JSON
155
+ `{ code: "auth_required" }` rather than plain-text `Unauthorized`.
156
+
157
+ ## Docs
158
+
159
+ - [Channel index](docs/README.md) — GET link / REST / MCP overview
160
+ - [MCP client setup](docs/mcp-clients.md) — Claude Desktop, Cursor, VS Code, CLI
161
+ - [MCP tools reference](docs/mcp-tools.md) — generated from live `tools/list`
162
+ - [Error codes](docs/errors.md) — code, status, hint, channels
163
+ - [OpenAPI 3.1](docs/api/openapi.yaml) — also `GET /mcp/api/v1/openapi.yaml`
164
+ (JSON: `GET /mcp/api/v1/openapi.json`)
165
+
166
+ Regenerate the MCP tools doc after tool changes: `yarn generate:mcp-tools`.
167
+
168
+ ## REST API
169
+
170
+ OpenAPI: **`GET /mcp/api/v1/openapi.yaml`** or
171
+ **`GET /mcp/api/v1/openapi.json`** (also in
172
+ [`docs/api/openapi.yaml`](docs/api/openapi.yaml)).
173
+
174
+ | Path | Purpose |
175
+ | ---------------------------------------- | ------------------------------------------------ |
176
+ | `GET /mcp` | Admin UI (status, links, proposals, audit log) |
177
+ | `GET /mcp/a/<token>` | Self-contained GET link (markdown docs) |
178
+ | `GET /mcp/a/<token>/…` | Token-scoped GET status/flows/propose/drafts |
179
+ | `POST /mcp/rpc` | MCP Streamable HTTP (access-link Bearer; tools) |
180
+ | `GET /mcp/api/v1` | Discovery index (no auth; endpoints + auth hint) |
181
+ | `GET /mcp/api/v1/openapi.yaml` | OpenAPI 3.1 specification (YAML) |
182
+ | `GET /mcp/api/v1/openapi.json` | OpenAPI 3.1 specification (JSON) |
183
+ | `GET /mcp/api/v1/status` | Plugin status JSON (admin or access link) |
184
+ | `GET /mcp/api/v1/settings` | Effective AI Access settings (admin `read`) |
185
+ | `PUT /mcp/api/v1/settings` | Update settings (`*`; persists to disk, hot) |
186
+ | `GET /mcp/api/v1/links` | List active access links (`*` only) |
187
+ | `POST /mcp/api/v1/links` | Create access link (`*` only; token once) |
188
+ | `DELETE /mcp/api/v1/links/:id` | Revoke access link (`*` only) |
189
+ | `GET /mcp/api/v1/flows` | List flows + rev (admin `read` or access link) |
190
+ | `GET /mcp/api/v1/flows/:id` | Get one flow + rev (admin `read` or access link) |
191
+ | `POST /mcp/api/v1/proposals` | Create proposal (access-link Bearer only) |
192
+ | `GET /mcp/api/v1/proposals` | List proposals (admin: all; link: own) |
193
+ | `GET /mcp/api/v1/proposals/:id` | Get proposal (admin: any; link: own) |
194
+ | `GET /mcp/api/v1/proposals/:id/diff` | Diff proposal vs current flow (admin `read`/own) |
195
+ | `POST /mcp/api/v1/proposals/:id/approve` | Approve + deploy (`*`; optional deploymentType) |
196
+ | `POST /mcp/api/v1/proposals/:id/reject` | Reject with reason (`*`; no flow mutate) |
197
+ | `GET /mcp/api/v1/logs` | Append-only audit log (admin `read`) |
198
+
199
+ ### MCP JSON-RPC (`POST /mcp/rpc`)
200
+
201
+ AI clients can use the official MCP Streamable HTTP transport against
202
+ `{httpAdminRoot}/mcp/rpc` with the same access-link Bearer token as the REST
203
+ API. Tools mirror the AI-facing REST surface only:
204
+
205
+ `get_status`, `list_flows`, `get_flow`, `submit_proposal` (including
206
+ `replace_flow` with `expectedRev`; `expected_rev` accepted as alias; optional
207
+ `title` ≤80 chars), `list_proposals` (alias `list_my_proposals`),
208
+ `get_proposal`. Tool descriptions document purpose, inputs, returns, and when to
209
+ use them. Proposal create/list/get responses include `title`, `displayName`
210
+ (`title ·` **server-local** datetime with explicit offset, e.g.
211
+ `16 Oct 01:35 +00:00`), `reviewUrl` (absolute URL using the request
212
+ Host/protocol — `X-Forwarded-*` only when Express `trust proxy` is set), and
213
+ `reviewPath` (relative `/mcp#proposal=<id>`). `createdAt` remains ISO-8601. The
214
+ `/mcp` page renders times in the browser's local timezone. AI clients should
215
+ refer to proposals by `displayName` + `reviewUrl`, not the raw id. For
216
+ access-link callers, `get_status` reports `permission: null` and
217
+ `caller.kind: "access-link"` (expected — AI tokens are not admin sessions).
218
+
219
+ Approve, reject, and access-link management stay human-only on the `/mcp` admin
220
+ page (and the corresponding REST routes). Rate limits, payload size limits, and
221
+ audit logging match REST. Missing tokens return **401** `access_link_missing`;
222
+ known-expired tokens return **401** `token_expired`; other invalid/revoked
223
+ tokens return **401** `access_link_invalid` (all with `WWW-Authenticate`)
224
+ telling the AI to ask the user for a new access link. Every error JSON includes
225
+ a `code`. `token_expired` also includes `expiredAt` and `accessLinksUrl` (the
226
+ `/mcp` page's Access links section). Access-link `/status` includes `expiresAt`,
227
+ `minutesRemaining` (rounded up), and `secondsRemaining`. See `docs/errors.md`
228
+ (generated from `src/plugin/error-registry.ts`).
229
+
230
+ Access links are **in memory** (default TTL **4h**, max 10). Caps: **get_link ≤
231
+ 24h**, **rest/mcp ≤ 30d**. Creating a link returns the raw `token` plus a
232
+ self-contained `url` for `get_link` (`/mcp/a/<token>`). While the link remains
233
+ active, `GET /links` includes `url` again so admins can re-copy from `/mcp` (the
234
+ raw `token` field stays create-only). After a restart every old token is
235
+ invalid.
236
+
237
+ Call `list_flows` or `get_flow` first and pass the returned `rev` as
238
+ `expectedRev`. Submit checks the live rev (**409** if stale). Approve may send
239
+ `deploymentType`: `nodes` (default), `flows`, or `full`. A stale `expectedRev`
240
+ at approve marks the proposal **rejected** with a resubmit message; other deploy
241
+ errors mark it **failed**. Reject never mutates flows.
242
+
243
+ ### Flow runtime adapter
244
+
245
+ - **RedAdapter** — in-process flows API (`RED.runtime.flows`, or the initialized
246
+ `@node-red/runtime` singleton when the plugin RED object has no `.runtime`)
247
+ - **HttpAdapter** — localhost Admin API v2 fallback only when credentials are
248
+ available (or adminAuth is off). Never selected silently without credentials
249
+ when adminAuth is on; upstream 401/403 becomes `plugin_misconfigured`
250
+ - **`updateFlow`** — requires `expectedRev`; default deployment type `nodes`
251
+
252
+ ## AI quickstart
253
+
254
+ ### Self-contained GET link (chat AIs that can only HTTP GET)
255
+
256
+ 1. An owner with `*` opens `/mcp` → **Access links** → create a **GET link**
257
+ (default TTL **4 hours**, max **24 hours**). Copy the full URL
258
+ `http(s)://<host><httpAdminRoot>/mcp/a/<token>` (or **Copy link for AI**).
259
+ 2. The AI opens that URL for markdown docs, then uses GET-only routes under the
260
+ same path (`/status`, `/flows`, `/flows/<id>`, `/proposals`, `/propose` with
261
+ preferred `replace_text` for small string edits, chunked `/draft/…` for large
262
+ HTML/fields; URL soft-cap ~8KB; prefer ~4–5 KB raw chunks — base64url adds
263
+ ~33%). Patch / `mode=json` drafts support
264
+ `nodes:[{op:'add'|'remove'|'set',...}]` (or a full `replace_flow` document).
265
+ Field drafts validate node/field at `draft/new`. Unknown `/mcp/a/*` paths
266
+ return JSON `not_found` without echoing the token.
267
+
268
+ Responses on GET links default to HTML, because some AI web fetchers only
269
+ read `text/html`. The HTML is a minimal document (no scripts or external
270
+ resources) holding the same markdown or pretty-printed JSON, fully escaped in
271
+ one `<pre>`, with relative `./…` links clickable. Request the real type via
272
+ Accept or `?format=`: `Accept: text/markdown` or `?format=md` for the guide,
273
+ `Accept: application/json` or `?format=json` for JSON. An explicit
274
+ `Accept: text/plain` returns the same raw text as `text/plain`. No Accept,
275
+ `*/*`, `text/html`, or browser-style lists get HTML. Status codes and data
276
+ are unchanged; every response carries `Vary: Accept`,
277
+ `Cache-Control: no-store`, `Referrer-Policy: no-referrer`,
278
+ `X-Robots-Tag: noindex, nofollow`, and `X-Content-Type-Options: nosniff`, and
279
+ HTML adds a restrictive `Content-Security-Policy`. REST and MCP endpoints are
280
+ not affected.
281
+
282
+ 3. Changes become **pending proposals**; a human approves on `/mcp`. JSON
283
+ responses use `Cache-Control: no-store`.
284
+
285
+ ### Bearer REST / MCP
286
+
287
+ 1. Optional: `GET /mcp/api/v1` (no auth) lists endpoints, auth scheme, OpenAPI,
288
+ and MCP URLs. Spec: `GET /mcp/api/v1/openapi.yaml`.
289
+ 2. Send `Authorization: Bearer <token>` to REST and/or MCP:
290
+
291
+ - REST: `GET /mcp/api/v1/status`, `GET /mcp/api/v1/flows`,
292
+ `GET /mcp/api/v1/flows/{id}?summary=1`, `GET /mcp/api/v1/nodes/{id}`,
293
+ `GET /mcp/api/v1/nodes/{id}/field/{name}`, `GET /mcp/api/v1/search`,
294
+ `POST /mcp/api/v1/proposals`, `GET /mcp/api/v1/proposals`
295
+ - MCP: `POST /mcp/rpc` tools `list_flows`, `get_flow` (optional `summary`),
296
+ `get_node`, `get_node_field`, `search_flows`, `submit_proposal`,
297
+ `list_proposals` / alias `list_my_proposals` (same access-link auth and
298
+ limits)
299
+
300
+ 3. The owner reviews the proposal on `/mcp` and approves or rejects it.
301
+ 4. If the token is invalid, expired, revoked, or cleared by a restart, the API
302
+ returns **401** telling the AI to **ask the user for a new access link**.
303
+
304
+ Example create body:
305
+
306
+ ```json
307
+ {
308
+ "type": "replace_flow",
309
+ "expectedRev": "abc123",
310
+ "title": "Rename inject to Hello",
311
+ "payload": {
312
+ "flowId": "tab-1",
313
+ "flow": {
314
+ "id": "tab-1",
315
+ "type": "tab",
316
+ "label": "Main",
317
+ "nodes": []
318
+ }
319
+ }
320
+ }
321
+ ```
322
+
323
+ ## Development
324
+
325
+ ```bash
326
+ yarn install
327
+ yarn build
328
+ yarn test
329
+ yarn quality
330
+ ```
331
+
332
+ ## License
333
+
334
+ 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.1';
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>