@com55/node-red-ai-access 0.1.0 → 0.2.0-beta.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/CHANGELOG.md CHANGED
@@ -6,6 +6,100 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## Unreleased
10
+
11
+ ## [0.2.0-beta.1] - 2026-10-28
12
+
13
+ First pre-release of 0.2.0, published under the npm `beta` dist-tag.
14
+
15
+ ### Added
16
+
17
+ - Node-RED settings and sidebar for configuring AI Access (#91).
18
+ - MCP OAuth authorization (#99): dynamic client registration (DCR), PKCE, an
19
+ owner consent page, and sliding refresh tokens with defaults of 1 hour (access
20
+ token), 30 days (refresh idle) and 90 days (refresh maximum).
21
+ - Route-collision check for the OAuth and metadata endpoints.
22
+ - MCP OAuth 2.1 authorization server and protected resource support with PKCE
23
+ S256, dynamic client registration, resource-bound tokens, rotating refresh
24
+ tokens, and `read`/`propose` scopes. Existing access-link tokens still work.
25
+ - Sidebar controls for OAuth lifetimes and client limits, registered-client
26
+ revocation, route-collision and metadata-fallback warnings, and a Public URL
27
+ warning when enabling OAuth, plus a refresh-token maximum lifetime (default 90
28
+ days, 0 = unlimited).
29
+
30
+ ### Changed
31
+
32
+ - CI now tests against a Node-RED 4 and Node-RED 5 matrix.
33
+ - Requires Node.js >= 20.19.
34
+
35
+ ## [0.1.1] - 2026-10-17
36
+
37
+ ### Added
38
+
39
+ - `/status` (REST and MCP `get_status`) now includes `openapiJson` in
40
+ `caller.baseUrls` for `rest` tokens, pointing at the served
41
+ `GET /mcp/api/v1/openapi.json`.
42
+ - Optional `aiAccess.corsAllowedOrigins` setting (settings.js): list of browser
43
+ origins allowed to call the AI-facing endpoints cross-origin, with
44
+ `Vary: Origin` and `OPTIONS` preflight support. Origins match
45
+ case-insensitively and the configured canonical origin is echoed; malformed
46
+ origins are rejected. `WWW-Authenticate`, `Retry-After`, and the `RateLimit*`
47
+ headers are exposed to allowed origins.
48
+ - Optional `aiAccess.publicBaseUrl` setting (settings.js) that sets the public
49
+ base URL for absolute links (access-link `url`, proposal `reviewUrl`,
50
+ `accessLinksUrl`), overriding `Host` and `X-Forwarded-*`.
51
+ - The MCP client snippets in the GET-link guide use the public origin
52
+ (`publicBaseUrl`, else the validated request origin) instead of the
53
+ `https://YOUR-NODE-RED` placeholder, which remains only when the origin cannot
54
+ be determined.
55
+
56
+ ### Changed
57
+
58
+ - **Behavior change:** every response on the GET access-link channel
59
+ (`/mcp/a/<token>/*`), including the markdown guide and JSON success and error
60
+ bodies, now defaults to `Content-Type: text/html; charset=utf-8`, because some
61
+ AI web fetchers only read `text/html`. The HTML is a minimal document with the
62
+ same markdown or pretty-printed JSON, fully escaped in one `<pre>`, with safe
63
+ relative `./…` links linkified (no scripts, no external resources). Status
64
+ codes and data are unchanged. The previous representation is returned when
65
+ `Accept` explicitly lists `text/markdown` or `application/json`, or
66
+ `?format=md` / `?format=json` is set; an explicit `Accept: text/plain` returns
67
+ the raw text as `text/plain`. All `/mcp/a/*` responses carry `Vary: Accept`,
68
+ `Cache-Control: no-store`, `Referrer-Policy: no-referrer`,
69
+ `X-Robots-Tag: noindex, nofollow`, and `X-Content-Type-Options: nosniff`; HTML
70
+ adds a restrictive `Content-Security-Policy`. REST and MCP are unchanged.
71
+ - **Behavior change:** the AI-facing endpoints (`/mcp/a/*`, `/mcp/api/*`,
72
+ `/mcp/rpc`) no longer send `Access-Control-Allow-Origin: *` (inherited from
73
+ Node-RED's admin app). No CORS headers are sent unless the request `Origin` is
74
+ listed in `aiAccess.corsAllowedOrigins`. Only browser-based callers are
75
+ affected; server-side clients are not.
76
+ - Attempts to create, list, or revoke access links under a GET access link
77
+ (`/mcp/a/<token>/links`) now return **403** `link_create_not_supported`
78
+ instead of 405. Error registry, `docs/errors.md`, OpenAPI, and get-link docs
79
+ updated.
80
+ - `/status` (GET link, REST, and MCP `get_status`) no longer includes
81
+ `storageDir` for access-link callers; it remains in admin-session responses
82
+ (shown on the `/mcp` page).
83
+
84
+ ### Fixed
85
+
86
+ - Proposal titles no longer repeat the flow name when a long `<title> in <flow>`
87
+ was capped at 80 characters.
88
+ - GET-link HTML no longer linkifies template paths such as `./nodes/<id>`.
89
+
90
+ - **Security:** absolute URLs no longer copy unvalidated `X-Forwarded-Proto` /
91
+ `X-Forwarded-Host` (or `Host`) values. The scheme must be exactly `http` or
92
+ `https` (else the connection scheme is used); hosts must be a DNS name, IPv4,
93
+ or bracketed IPv6 literal with an optional port 1-65535, with no userinfo,
94
+ path, query, fragment, or whitespace. Invalid forwarded hosts fall back to
95
+ `Host`; an invalid `Host` falls back to a relative path.
96
+
97
+ - Absolute URLs (access-link `url`, proposal `reviewUrl`, `accessLinksUrl`) used
98
+ `http://` behind a TLS-terminating proxy. With Express `trust proxy` enabled
99
+ the first `X-Forwarded-Proto` value is now used explicitly; without it, set
100
+ `aiAccess.publicBaseUrl`. Forwarded headers stay ignored when `trust proxy` is
101
+ off.
102
+
9
103
  ## [0.1.0] - 2026-10-16
10
104
 
11
105
  First public release of **Node-RED AI Access**: an admin-gated Node-RED 4 plugin
@@ -316,4 +410,7 @@ Requires Node.js 20+.
316
410
  auth, 413 limits) served at `GET /mcp/api/v1/openapi.yaml`; README install,
317
411
  config, `/mcp` usage, and AI quickstart.
318
412
 
413
+ [0.2.0-beta.1]:
414
+ https://github.com/com55/node-red-ai-access/compare/v0.1.1...v0.2.0-beta.1
415
+ [0.1.1]: https://github.com/com55/node-red-ai-access/compare/v0.1.0...v0.1.1
319
416
  [0.1.0]: https://github.com/com55/node-red-ai-access/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Node-RED AI Access
2
2
 
3
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/)
4
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.19.0-brightgreen.svg)](https://nodejs.org/)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7%2B-blue.svg)](https://www.typescriptlang.org/)
6
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
7
 
@@ -24,9 +24,10 @@ Derived from
24
24
 
25
25
  ## Install (Node-RED plugin)
26
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).
27
+ Requirements: **Node-RED 4.x or newer**, Node.js 20.19+ (tested: Node-RED 4.1 on
28
+ Node 20/22, Node-RED 5.0 on Node 22/24), and **`adminAuth` enabled**
29
+ (recommended). Without `adminAuth`, AI features stay off unless you opt in (see
30
+ Configuration).
30
31
 
31
32
  1. Install into your Node-RED user directory (or via the palette):
32
33
 
@@ -74,28 +75,134 @@ module.exports = {
74
75
  rateLimitWindowMs: 60000,
75
76
  rateLimitMaxPerIp: 600,
76
77
  rateLimitMaxPerToken: 300,
78
+ // Public base URL for absolute links (access-link url, reviewUrl,
79
+ // accessLinksUrl). Set this behind a TLS-terminating proxy (default: unset)
80
+ // publicBaseUrl: 'https://nodered.example.com',
81
+ // Browser origins allowed to call /mcp/a, /mcp/api, and /mcp/rpc
82
+ // cross-origin (default: none — no CORS headers are sent)
83
+ // corsAllowedOrigins: ['https://app.example.com'],
84
+ // MCP OAuth (disabled by default; publicBaseUrl is the issuer)
85
+ // oauth: {
86
+ // enabled: true,
87
+ // accessTokenTtlSec: 3600,
88
+ // refreshIdleTtlSec: 2592000,
89
+ // dcrMaxClients: 20,
90
+ // },
77
91
  },
78
92
  };
79
93
  ```
80
94
 
95
+ ### Absolute URLs behind a reverse proxy
96
+
97
+ Access-link `url`, proposal `reviewUrl`, and `accessLinksUrl` (in
98
+ `token_expired`) are absolute URLs built by one shared helper:
99
+
100
+ 1. Effective `aiAccess.publicBaseUrl`, when set, overrides the request origin.
101
+ The sidebar/settings API accepts an HTTPS origin without a path, query,
102
+ fragment, or credentials (HTTP localhost / loopback is allowed for
103
+ development). An empty value uses the request origin.
104
+ 2. Otherwise, when Express `trust proxy` is enabled (for example
105
+ `httpServerOptions: { 'trust proxy': true }`), the first `X-Forwarded-Proto`
106
+ and `X-Forwarded-Host` values are used.
107
+ 3. Otherwise the request `Host` header and the connection protocol are used;
108
+ `X-Forwarded-*` headers are ignored so clients cannot spoof link origins.
109
+
110
+ Forwarded and `Host` values are validated: the scheme must be `http` or `https`
111
+ (else the connection scheme is used) and the host must be a DNS name, IPv4, or
112
+ bracketed IPv6 literal with an optional port. An invalid forwarded host falls
113
+ back to `Host`; an invalid `Host` yields relative paths. The MCP client snippets
114
+ in the GET-link guide use the same origin, and show a `https://YOUR-NODE-RED`
115
+ placeholder only when no origin can be determined.
116
+
117
+ Behind a TLS-terminating proxy (for example Cloudflare) with neither option set,
118
+ links therefore start with `http://`.
119
+
120
+ ### CORS
121
+
122
+ CORS only affects code running in a browser (a web page calling the plugin from
123
+ another origin). Server-side AI fetchers and agents (for example Gemini,
124
+ ChatGPT, or an MCP client running outside a browser) ignore CORS and are not
125
+ affected by this setting.
126
+
127
+ By default the AI-facing endpoints (`/mcp/a/*`, `/mcp/api/*`, `/mcp/rpc`) send
128
+ **no** CORS headers, overriding Node-RED's built-in
129
+ `Access-Control-Allow-Origin: *` for those paths. To allow a browser app, list
130
+ its origin in `aiAccess.corsAllowedOrigins` via the AI Access sidebar or
131
+ settings API. Changes apply immediately. The API rejects any invalid origin in
132
+ the supplied list (including paths, credentials, queries, and fragments). For a
133
+ listed `Origin` the response echoes it in `Access-Control-Allow-Origin` with
134
+ `Vary: Origin`, and `OPTIONS` preflight returns `204` with allowed methods
135
+ (`GET, POST, DELETE, OPTIONS`) and headers (`Authorization`, `Content-Type`,
136
+ `Accept`, `Mcp-Session-Id`, `Mcp-Protocol-Version`, `Last-Event-ID`). Origins
137
+ match case-insensitively and the configured origin is echoed; malformed origins
138
+ are rejected. Exposed headers include `Mcp-Session-Id`, `WWW-Authenticate`,
139
+ `Retry-After`, and the `RateLimit*` headers. Other origins get no CORS headers.
140
+
81
141
  ### Settings precedence
82
142
 
83
143
  Effective values are resolved **without restart** in this order (highest wins):
84
144
 
85
- 1. **Disk** — `<userDir>/.ai-access/settings.json` (editor sidebar **AI Access**
145
+ 1. **`settings.js`** — keys present in the `aiAccess` block are locked
146
+ 2. **Disk** — `<userDir>/.ai-access/config.json` (editor sidebar **AI Access**
86
147
  tab, or `PUT /mcp/api/v1/settings`)
87
- 2. **`settings.js`** — `aiAccess` block (install / bootstrap defaults)
88
148
  3. **Built-in defaults** — as documented above
89
149
 
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
150
+ This is a breaking precedence change in 0.2.0: settings.js now wins over disk.
151
+ Legacy `settings.json` is read when `config.json` is absent; the next save
152
+ writes `config.json`. Locked fields show a lock indicator and disabled input.
153
+ PUT ignores locked keys while applying unlocked ones; GET and PUT return
154
+ per-field `sources` (`settings.js`, `disk`, or `default`) and the precedence
155
+ order.
156
+
157
+ The editor right-hand sidebar tab (**AI Access**, next to Information / Help /
158
+ Debug / Configuration nodes / Context Data) lets `*` admins change
159
+ enable/disable, default deploy type, access-link TTL, maximum active links,
160
+ Public URL, CORS origins, and the three rate limits. `read` users see the same
161
+ values read-only. Express's effective `trust proxy` setting is displayed
162
+ read-only. Public URL, CORS, and rate limits apply immediately; changing limits
163
+ starts fresh rate-limit buckets. A button opens the `/mcp` page. When
95
164
  `adminAuth` is off, the tab exposes the `allowWithoutAdminAuth` opt-in with a
96
165
  clear local-only warning.
97
166
 
98
- Storage lives under **`<userDir>/.ai-access`** (`settings.json`,
167
+ The sidebar also configures MCP OAuth, shows registered clients with revoke
168
+ buttons, and reports route collisions and the `httpNodeRoot: false` metadata
169
+ fallback. Enabling OAuth with no Public URL or an HTTP non-loopback URL shows a
170
+ warning confirmation; it does not block the change. OAuth fields set in
171
+ `settings.js` are locked in the sidebar.
172
+
173
+ ### MCP OAuth
174
+
175
+ OAuth is disabled by default. Set `aiAccess.publicBaseUrl` to the public HTTPS
176
+ origin before enabling it; localhost and loopback HTTP are supported for local
177
+ development. Node-RED serves authorization, token, dynamic client registration,
178
+ and revocation under `/mcp/oauth/*`. It publishes authorization server metadata
179
+ at `/.well-known/oauth-authorization-server` and protected resource metadata at
180
+ `/.well-known/oauth-protected-resource/mcp` and
181
+ `/.well-known/oauth-protected-resource`. The issuer is `publicBaseUrl` and the
182
+ resource indicator is `{publicBaseUrl}/mcp`. When `httpNodeRoot` is false,
183
+ metadata is available under the admin `/mcp` path instead; the sidebar displays
184
+ this fallback. Route collisions disable the affected OAuth endpoints and appear
185
+ in the sidebar.
186
+
187
+ Clients use authorization code with PKCE S256 and can request `read` or
188
+ `propose`. Consent uses the Node-RED editor login. Access tokens last one hour
189
+ by default; refresh tokens expire after 30 days of inactivity and rotate on each
190
+ use. Reusing a rotated refresh token revokes its token family. Tokens are stored
191
+ as SHA-256 hashes in `<userDir>/.ai-access/oauth.json`. Access-link `aia_`
192
+ bearer tokens continue to work. See [MCP client setup](docs/mcp-clients.md).
193
+
194
+ Public URL **Check** uses `POST /mcp/api/v1/settings/check-public-url` and
195
+ requires `*` permission because it performs an outbound request. It validates
196
+ the form URL, fetches only `{publicBaseUrl}{httpAdminRoot}/mcp/api/v1`
197
+ (discovery), times out after 5 seconds, and never follows redirects. The result
198
+ reports the HTTP status or a connection/timeout error; it does not save the
199
+ form. Loopback checks use Node-RED's own listening port; behind a reverse proxy,
200
+ use the internal Node-RED port. Unix-socket listeners are not supported. HTTPS
201
+ checks using a self-signed certificate fail because TLS verification remains
202
+ enabled. Saving a Public URL with a private/internal IP literal is allowed, but
203
+ the settings response and sidebar show a warning.
204
+
205
+ Storage lives under **`<userDir>/.ai-access`** (`config.json`,
99
206
  `proposals/*.json`, and append-only `logs/audit.jsonl`). Directories use mode
100
207
  `0700`, files `0600`. Proposal JSON is capped at **1_500_000 bytes**; oversized
101
208
  proposal JSON or audit lines return HTTP **413** with `code: payload_too_large`
@@ -215,6 +322,21 @@ errors mark it **failed**. Reject never mutates flows.
215
322
  `nodes:[{op:'add'|'remove'|'set',...}]` (or a full `replace_flow` document).
216
323
  Field drafts validate node/field at `draft/new`. Unknown `/mcp/a/*` paths
217
324
  return JSON `not_found` without echoing the token.
325
+
326
+ Responses on GET links default to HTML, because some AI web fetchers only
327
+ read `text/html`. The HTML is a minimal document (no scripts or external
328
+ resources) holding the same markdown or pretty-printed JSON, fully escaped in
329
+ one `<pre>`, with relative `./…` links clickable. Request the real type via
330
+ Accept or `?format=`: `Accept: text/markdown` or `?format=md` for the guide,
331
+ `Accept: application/json` or `?format=json` for JSON. An explicit
332
+ `Accept: text/plain` returns the same raw text as `text/plain`. No Accept,
333
+ `*/*`, `text/html`, or browser-style lists get HTML. Status codes and data
334
+ are unchanged; every response carries `Vary: Accept`,
335
+ `Cache-Control: no-store`, `Referrer-Policy: no-referrer`,
336
+ `X-Robots-Tag: noindex, nofollow`, and `X-Content-Type-Options: nosniff`, and
337
+ HTML adds a restrictive `Content-Security-Policy`. REST and MCP endpoints are
338
+ not affected.
339
+
218
340
  3. Changes become **pending proposals**; a human approves on `/mcp`. JSON
219
341
  responses use `Cache-Control: no-store`.
220
342
 
package/ai-access.html CHANGED
@@ -14,7 +14,7 @@
14
14
  }
15
15
  window.__com55AiAccessSidebarLoading = true;
16
16
 
17
- var EDITOR_SIDEBAR_ASSET_VERSION = '0.1.0';
17
+ var EDITOR_SIDEBAR_ASSET_VERSION = '0.2.0-beta.1';
18
18
  var root = (RED.settings.httpAdminRoot || '/').replace(/\/$/, '');
19
19
  var src =
20
20
  (root || '') +