@com55/node-red-ai-access 0.1.1 → 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 +28 -0
- package/README.md +81 -23
- package/ai-access.html +1 -1
- package/dist/plugin.cjs +411 -83
- package/dist/plugin.d.cts +68 -10
- package/docs/api/openapi.yaml +50 -0
- package/docs/mcp-clients.md +34 -0
- package/package.json +5 -5
package/dist/plugin.d.cts
CHANGED
|
@@ -176,7 +176,7 @@ interface AiAccessPaths {
|
|
|
176
176
|
root: string;
|
|
177
177
|
proposals: string;
|
|
178
178
|
logs: string;
|
|
179
|
-
/** Persisted AI Access settings (`
|
|
179
|
+
/** Persisted AI Access settings (`config.json`). */
|
|
180
180
|
settings: string;
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -221,11 +221,16 @@ declare class AuditLog {
|
|
|
221
221
|
|
|
222
222
|
type ProposalStatus = 'pending' | 'approved' | 'rejected' | 'failed' | 'withdrawn' | 'expired' | 'superseded';
|
|
223
223
|
type ProposalType = 'replace_flow';
|
|
224
|
-
interface
|
|
224
|
+
interface AccessLinkProposalCreator {
|
|
225
225
|
kind: 'access-link';
|
|
226
226
|
linkId: string;
|
|
227
227
|
label?: string;
|
|
228
228
|
}
|
|
229
|
+
type ProposalCreator = AccessLinkProposalCreator | {
|
|
230
|
+
kind: 'oauth';
|
|
231
|
+
clientId: string;
|
|
232
|
+
user: string;
|
|
233
|
+
};
|
|
229
234
|
type ProposalDecisionBy = {
|
|
230
235
|
kind: 'admin';
|
|
231
236
|
username?: string;
|
|
@@ -329,11 +334,11 @@ declare class ProposalStore {
|
|
|
329
334
|
}
|
|
330
335
|
|
|
331
336
|
/**
|
|
332
|
-
* Persisted AI Access settings under `<userDir>/.ai-access/
|
|
337
|
+
* Persisted AI Access settings under `<userDir>/.ai-access/config.json`.
|
|
333
338
|
*
|
|
334
339
|
* Precedence for effective values (highest wins):
|
|
335
|
-
* 1.
|
|
336
|
-
* 2.
|
|
340
|
+
* 1. `settings.js` `aiAccess` block (locked keys)
|
|
341
|
+
* 2. Disk file keys present in `.ai-access/config.json` (sidebar AI Access UI)
|
|
337
342
|
* 3. Built-in defaults
|
|
338
343
|
*
|
|
339
344
|
* Changes via PUT /mcp/api/v1/settings update memory + disk immediately
|
|
@@ -347,6 +352,18 @@ interface PersistedAiAccessSettings {
|
|
|
347
352
|
defaultLinkTtlSeconds?: number;
|
|
348
353
|
defaultDeploymentType?: DeploymentType;
|
|
349
354
|
maxActiveLinks?: number;
|
|
355
|
+
publicBaseUrl?: string;
|
|
356
|
+
corsAllowedOrigins?: string[];
|
|
357
|
+
rateLimitWindowMs?: number;
|
|
358
|
+
rateLimitMaxPerIp?: number;
|
|
359
|
+
rateLimitMaxPerToken?: number;
|
|
360
|
+
oauth?: {
|
|
361
|
+
enabled?: boolean;
|
|
362
|
+
accessTokenTtlSec?: number;
|
|
363
|
+
refreshIdleTtlSec?: number;
|
|
364
|
+
refreshMaxLifetimeSec?: number;
|
|
365
|
+
dcrMaxClients?: number;
|
|
366
|
+
};
|
|
350
367
|
}
|
|
351
368
|
interface EffectiveAiAccessSettings {
|
|
352
369
|
enabled: boolean;
|
|
@@ -354,18 +371,31 @@ interface EffectiveAiAccessSettings {
|
|
|
354
371
|
defaultLinkTtlSeconds: number;
|
|
355
372
|
defaultDeploymentType: DeploymentType;
|
|
356
373
|
maxActiveLinks: number;
|
|
374
|
+
publicBaseUrl: string;
|
|
375
|
+
corsAllowedOrigins: string[];
|
|
376
|
+
rateLimitWindowMs: number;
|
|
377
|
+
rateLimitMaxPerIp: number;
|
|
378
|
+
rateLimitMaxPerToken: number;
|
|
379
|
+
oauth: {
|
|
380
|
+
enabled: boolean;
|
|
381
|
+
accessTokenTtlSec: number;
|
|
382
|
+
refreshIdleTtlSec: number;
|
|
383
|
+
refreshMaxLifetimeSec: number;
|
|
384
|
+
dcrMaxClients: number;
|
|
385
|
+
};
|
|
357
386
|
}
|
|
358
387
|
type SettingsSource = 'default' | 'settings.js' | 'disk';
|
|
359
388
|
interface EffectiveAiAccessSettingsWithMeta extends EffectiveAiAccessSettings {
|
|
360
389
|
sources: Record<keyof EffectiveAiAccessSettings, SettingsSource>;
|
|
361
390
|
}
|
|
391
|
+
type Warn = (message: string) => void;
|
|
362
392
|
declare class SettingsStore {
|
|
363
393
|
private readonly paths;
|
|
364
394
|
private readonly filePath;
|
|
365
395
|
private disk;
|
|
366
396
|
private settingsJs;
|
|
367
397
|
private cached;
|
|
368
|
-
constructor(paths: AiAccessPaths, settingsJsAiAccess?: PersistedAiAccessSettings);
|
|
398
|
+
constructor(paths: AiAccessPaths, settingsJsAiAccess?: PersistedAiAccessSettings, warn?: Warn);
|
|
369
399
|
getEffective(): EffectiveAiAccessSettingsWithMeta;
|
|
370
400
|
getDisk(): PersistedAiAccessSettings;
|
|
371
401
|
/**
|
|
@@ -473,17 +503,25 @@ interface AiAccessSettings {
|
|
|
473
503
|
/**
|
|
474
504
|
* Explicit public base URL (e.g. `https://nodered.example.com`) used for
|
|
475
505
|
* absolute URLs (access-link `url`, proposal `reviewUrl`, `accessLinksUrl`).
|
|
476
|
-
* Overrides `Host` / `X-Forwarded-*`. settings.js
|
|
506
|
+
* Overrides `Host` / `X-Forwarded-*`. Editable in the AI Access settings sidebar unless locked by settings.js.
|
|
477
507
|
*/
|
|
478
508
|
publicBaseUrl?: string;
|
|
479
509
|
/**
|
|
480
510
|
* Browser origins allowed to call the AI-facing endpoints (`/mcp/a`,
|
|
481
511
|
* `/mcp/api`, `/mcp/rpc`) cross-origin, e.g. `['https://app.example.com']`.
|
|
482
|
-
* Default: none (no CORS headers). settings.js
|
|
512
|
+
* Default: none (no CORS headers). Editable in the AI Access settings sidebar unless locked by settings.js.
|
|
483
513
|
*/
|
|
484
514
|
corsAllowedOrigins?: string[];
|
|
515
|
+
oauth?: {
|
|
516
|
+
enabled?: boolean;
|
|
517
|
+
accessTokenTtlSec?: number;
|
|
518
|
+
refreshIdleTtlSec?: number;
|
|
519
|
+
/** Absolute refresh-family lifetime in seconds; default 7776000 (90 days), 0 = unlimited. */
|
|
520
|
+
refreshMaxLifetimeSec?: number;
|
|
521
|
+
dcrMaxClients?: number;
|
|
522
|
+
};
|
|
485
523
|
}
|
|
486
|
-
type AiAccessPermission = 'read' | '*';
|
|
524
|
+
type AiAccessPermission = 'read' | 'propose' | '*';
|
|
487
525
|
interface AiAccessCapabilities {
|
|
488
526
|
view: boolean;
|
|
489
527
|
manageLinks: boolean;
|
|
@@ -529,7 +567,13 @@ interface AiAccessStatusCallerAccessLink {
|
|
|
529
567
|
*/
|
|
530
568
|
baseUrls?: Partial<AiAccessStatusCallerBaseUrls>;
|
|
531
569
|
}
|
|
532
|
-
|
|
570
|
+
interface AiAccessStatusCallerOAuth {
|
|
571
|
+
kind: 'oauth';
|
|
572
|
+
clientId: string;
|
|
573
|
+
user: string;
|
|
574
|
+
scopes: string[];
|
|
575
|
+
}
|
|
576
|
+
type AiAccessStatusCaller = AiAccessStatusCallerAdmin | AiAccessStatusCallerAccessLink | AiAccessStatusCallerOAuth;
|
|
533
577
|
interface AiAccessStatus {
|
|
534
578
|
ok: boolean;
|
|
535
579
|
version: string;
|
|
@@ -557,6 +601,8 @@ interface AiAccessStatus {
|
|
|
557
601
|
interface NodeRedPluginSettings {
|
|
558
602
|
userDir: string;
|
|
559
603
|
httpAdminRoot?: string;
|
|
604
|
+
httpNodeRoot?: string | false;
|
|
605
|
+
httpStatic?: string | string[];
|
|
560
606
|
adminAuth?: unknown;
|
|
561
607
|
aiAccess?: AiAccessSettings;
|
|
562
608
|
}
|
|
@@ -566,6 +612,16 @@ interface NodeRedAuthAPI {
|
|
|
566
612
|
interface NodeRedPluginAPI {
|
|
567
613
|
settings: NodeRedPluginSettings;
|
|
568
614
|
httpAdmin: Express;
|
|
615
|
+
httpNode?: Express;
|
|
616
|
+
nodes?: {
|
|
617
|
+
eachNode(listener: (node: {
|
|
618
|
+
type?: string;
|
|
619
|
+
url?: string;
|
|
620
|
+
}) => void): void;
|
|
621
|
+
};
|
|
622
|
+
events?: {
|
|
623
|
+
on(event: string, listener: () => void): void;
|
|
624
|
+
};
|
|
569
625
|
auth: NodeRedAuthAPI;
|
|
570
626
|
plugins: {
|
|
571
627
|
registerPlugin: (id: string, definition?: Record<string, unknown>) => void;
|
|
@@ -584,6 +640,8 @@ interface AccessLinkAuthInfo {
|
|
|
584
640
|
createdAt: string;
|
|
585
641
|
expiresAt: string;
|
|
586
642
|
createdBy?: string;
|
|
643
|
+
oauthClientId?: string;
|
|
644
|
+
oauthUser?: string;
|
|
587
645
|
}
|
|
588
646
|
interface AuthenticatedRequest extends Request {
|
|
589
647
|
user?: {
|
package/docs/api/openapi.yaml
CHANGED
|
@@ -425,6 +425,42 @@ paths:
|
|
|
425
425
|
'413':
|
|
426
426
|
$ref: '#/components/responses/PayloadTooLarge'
|
|
427
427
|
|
|
428
|
+
/settings/check-public-url:
|
|
429
|
+
post:
|
|
430
|
+
tags: [Status]
|
|
431
|
+
summary: Check the configured public URL
|
|
432
|
+
description: Probes the MCP endpoint at the supplied HTTPS origin without following redirects.
|
|
433
|
+
operationId: checkPublicUrl
|
|
434
|
+
security:
|
|
435
|
+
- AdminSession: []
|
|
436
|
+
requestBody:
|
|
437
|
+
required: true
|
|
438
|
+
content:
|
|
439
|
+
application/json:
|
|
440
|
+
schema:
|
|
441
|
+
type: object
|
|
442
|
+
required: [publicBaseUrl]
|
|
443
|
+
properties:
|
|
444
|
+
publicBaseUrl:
|
|
445
|
+
type: string
|
|
446
|
+
format: uri
|
|
447
|
+
responses:
|
|
448
|
+
'200':
|
|
449
|
+
description: Probe result
|
|
450
|
+
content:
|
|
451
|
+
application/json:
|
|
452
|
+
schema:
|
|
453
|
+
type: object
|
|
454
|
+
properties:
|
|
455
|
+
ok: { type: boolean }
|
|
456
|
+
status: { type: [integer, 'null'] }
|
|
457
|
+
url: { type: string }
|
|
458
|
+
error: { type: string }
|
|
459
|
+
'400':
|
|
460
|
+
$ref: '#/components/responses/BadRequest'
|
|
461
|
+
'401':
|
|
462
|
+
$ref: '#/components/responses/Unauthorized'
|
|
463
|
+
|
|
428
464
|
/links:
|
|
429
465
|
get:
|
|
430
466
|
tags: [Access links]
|
|
@@ -1711,6 +1747,14 @@ components:
|
|
|
1711
1747
|
maxActiveLinks:
|
|
1712
1748
|
type: integer
|
|
1713
1749
|
minimum: 1
|
|
1750
|
+
publicBaseUrl:
|
|
1751
|
+
type: string
|
|
1752
|
+
format: uri
|
|
1753
|
+
corsAllowedOrigins:
|
|
1754
|
+
type: array
|
|
1755
|
+
items:
|
|
1756
|
+
type: string
|
|
1757
|
+
format: uri
|
|
1714
1758
|
additionalProperties: false
|
|
1715
1759
|
|
|
1716
1760
|
AiAccessSettingsResponse:
|
|
@@ -1758,6 +1802,12 @@ components:
|
|
|
1758
1802
|
$ref: '#/components/schemas/DeploymentType'
|
|
1759
1803
|
maxActiveLinks:
|
|
1760
1804
|
type: integer
|
|
1805
|
+
publicBaseUrl:
|
|
1806
|
+
type: string
|
|
1807
|
+
corsAllowedOrigins:
|
|
1808
|
+
type: array
|
|
1809
|
+
items:
|
|
1810
|
+
type: string
|
|
1761
1811
|
sources:
|
|
1762
1812
|
type: object
|
|
1763
1813
|
additionalProperties:
|
package/docs/mcp-clients.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# MCP client setup
|
|
2
2
|
|
|
3
|
+
## OAuth (for clients with OAuth discovery)
|
|
4
|
+
|
|
5
|
+
Enable MCP OAuth in the AI Access editor sidebar and set a public HTTPS origin
|
|
6
|
+
in **Public URL**. Give the client the MCP endpoint
|
|
7
|
+
`https://YOUR-NODE-RED/mcp/rpc`. It can discover the authorization server from
|
|
8
|
+
the protected resource metadata, register itself, and request `read` or
|
|
9
|
+
`propose` scope. Sign in to Node-RED and approve the client on the consent page.
|
|
10
|
+
The sidebar lists registered clients and lets an admin revoke them and their
|
|
11
|
+
tokens. Localhost HTTP is supported for development. If `httpNodeRoot` is
|
|
12
|
+
disabled, see the sidebar warning for the metadata fallback path.
|
|
13
|
+
|
|
14
|
+
Notes:
|
|
15
|
+
|
|
16
|
+
- Redirect URIs must use `https://`, or `http://` on a loopback host
|
|
17
|
+
(`localhost`, `127.0.0.1`, `[::1]`). Custom native-app schemes such as
|
|
18
|
+
`myapp://callback` are not supported.
|
|
19
|
+
- The consent page uses your Node-RED editor login from the same browser (the
|
|
20
|
+
editor keeps its token in browser storage, not a cookie). Open the editor on
|
|
21
|
+
the same origin as the Public URL and sign in first; the consent page offers a
|
|
22
|
+
Retry button after you sign in.
|
|
23
|
+
- Users with read-only Node-RED permission can approve only the `read` scope;
|
|
24
|
+
`propose` requires full (`*`) permission.
|
|
25
|
+
- Clients that register but are never approved are removed after one hour and do
|
|
26
|
+
not count toward the client limit.
|
|
27
|
+
- Refresh tokens are sliding: each refresh renews the idle lifetime. A token
|
|
28
|
+
chain also has a maximum lifetime of 90 days from approval by default; after
|
|
29
|
+
that the client must sign in again. Change it in the sidebar (0 = unlimited).
|
|
30
|
+
Lowering the maximum applies immediately to existing token chains. Raising it
|
|
31
|
+
or setting 0 does not extend the refresh token a client already holds; the new
|
|
32
|
+
limit takes effect from that chain's next successful refresh (and for new
|
|
33
|
+
chains).
|
|
34
|
+
|
|
35
|
+
## Access-link bearer tokens
|
|
36
|
+
|
|
3
37
|
Use an **mcp** access-link token from `/mcp` → Access links (kind = MCP token).
|
|
4
38
|
Endpoint: `POST https://YOUR-NODE-RED/mcp/rpc` with
|
|
5
39
|
`Authorization: Bearer <mcp-token>`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@com55/node-red-ai-access",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
4
|
"description": "Node-RED plugin that gives AI agents temporary, owner-approved access via REST and MCP",
|
|
5
5
|
"main": "dist/plugin.cjs",
|
|
6
6
|
"types": "dist/plugin.d.cts",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"author": "com55",
|
|
83
83
|
"license": "MIT",
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
85
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
86
86
|
"express": "^5.3.0",
|
|
87
87
|
"express-rate-limit": "^8.5.2",
|
|
88
88
|
"yaml": "^2.8.0",
|
|
@@ -99,18 +99,18 @@
|
|
|
99
99
|
"eslint-plugin-import": "^2.32.0",
|
|
100
100
|
"husky": "^9.1.7",
|
|
101
101
|
"lint-staged": "^17.6.0",
|
|
102
|
-
"node-red": "4.1.
|
|
102
|
+
"node-red": "4.1.17",
|
|
103
103
|
"prettier": "^3.9.10",
|
|
104
104
|
"rimraf": "^6.1.3",
|
|
105
105
|
"supertest": "^7.3.1",
|
|
106
106
|
"tsup": "^8.5.1",
|
|
107
107
|
"typescript": "^5.9.3",
|
|
108
108
|
"typescript-eslint": "^8.71.1",
|
|
109
|
-
"vite": "^
|
|
109
|
+
"vite": "^8.3.5",
|
|
110
110
|
"vitest": "^5.0.3"
|
|
111
111
|
},
|
|
112
112
|
"engines": {
|
|
113
|
-
"node": ">=20.
|
|
113
|
+
"node": ">=20.19.0",
|
|
114
114
|
"npm": ">=10.0.0"
|
|
115
115
|
},
|
|
116
116
|
"repository": {
|