@com55/node-red-ai-access 0.1.1 → 0.2.0-beta.2
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 +74 -0
- package/README.md +84 -26
- package/ai-access.html +1 -1
- package/dist/plugin.cjs +753 -145
- package/dist/plugin.d.cts +82 -17
- 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
|
@@ -23,8 +23,11 @@ interface AccessLinkCreated extends AccessLinkPublic {
|
|
|
23
23
|
interface AccessLinkRecord {
|
|
24
24
|
id: string;
|
|
25
25
|
tokenHash: string;
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* @deprecated Legacy field: raw tokens are no longer kept. Only `tokenHash` is
|
|
28
|
+
* used for validation; any `token` on an inserted record is dropped.
|
|
29
|
+
*/
|
|
30
|
+
token?: string;
|
|
28
31
|
/**
|
|
29
32
|
* Token surface. Optional for in-memory migration of pre-kind records
|
|
30
33
|
* (treated as `get_link` by {@link resolveAccessLinkKind}).
|
|
@@ -82,11 +85,6 @@ declare class AccessLinkStore {
|
|
|
82
85
|
private isActive;
|
|
83
86
|
list(): AccessLinkPublic[];
|
|
84
87
|
create(input?: CreateAccessLinkInput): AccessLinkCreated;
|
|
85
|
-
/**
|
|
86
|
-
* Raw token for an active link (in-memory only). Used to rebuild the GET URL
|
|
87
|
-
* for admin "Copy link" — never logs the value.
|
|
88
|
-
*/
|
|
89
|
-
peekToken(linkId: string): string | null;
|
|
90
88
|
revoke(linkId: string): AccessLinkPublic;
|
|
91
89
|
/**
|
|
92
90
|
* Look up a link by id even if revoked/expired (still in memory).
|
|
@@ -176,7 +174,7 @@ interface AiAccessPaths {
|
|
|
176
174
|
root: string;
|
|
177
175
|
proposals: string;
|
|
178
176
|
logs: string;
|
|
179
|
-
/** Persisted AI Access settings (`
|
|
177
|
+
/** Persisted AI Access settings (`config.json`). */
|
|
180
178
|
settings: string;
|
|
181
179
|
}
|
|
182
180
|
|
|
@@ -221,11 +219,16 @@ declare class AuditLog {
|
|
|
221
219
|
|
|
222
220
|
type ProposalStatus = 'pending' | 'approved' | 'rejected' | 'failed' | 'withdrawn' | 'expired' | 'superseded';
|
|
223
221
|
type ProposalType = 'replace_flow';
|
|
224
|
-
interface
|
|
222
|
+
interface AccessLinkProposalCreator {
|
|
225
223
|
kind: 'access-link';
|
|
226
224
|
linkId: string;
|
|
227
225
|
label?: string;
|
|
228
226
|
}
|
|
227
|
+
type ProposalCreator = AccessLinkProposalCreator | {
|
|
228
|
+
kind: 'oauth';
|
|
229
|
+
clientId: string;
|
|
230
|
+
user: string;
|
|
231
|
+
};
|
|
229
232
|
type ProposalDecisionBy = {
|
|
230
233
|
kind: 'admin';
|
|
231
234
|
username?: string;
|
|
@@ -329,11 +332,11 @@ declare class ProposalStore {
|
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
/**
|
|
332
|
-
* Persisted AI Access settings under `<userDir>/.ai-access/
|
|
335
|
+
* Persisted AI Access settings under `<userDir>/.ai-access/config.json`.
|
|
333
336
|
*
|
|
334
337
|
* Precedence for effective values (highest wins):
|
|
335
|
-
* 1.
|
|
336
|
-
* 2.
|
|
338
|
+
* 1. `settings.js` `aiAccess` block (locked keys)
|
|
339
|
+
* 2. Disk file keys present in `.ai-access/config.json` (sidebar AI Access UI)
|
|
337
340
|
* 3. Built-in defaults
|
|
338
341
|
*
|
|
339
342
|
* Changes via PUT /mcp/api/v1/settings update memory + disk immediately
|
|
@@ -347,6 +350,18 @@ interface PersistedAiAccessSettings {
|
|
|
347
350
|
defaultLinkTtlSeconds?: number;
|
|
348
351
|
defaultDeploymentType?: DeploymentType;
|
|
349
352
|
maxActiveLinks?: number;
|
|
353
|
+
publicBaseUrl?: string;
|
|
354
|
+
corsAllowedOrigins?: string[];
|
|
355
|
+
rateLimitWindowMs?: number;
|
|
356
|
+
rateLimitMaxPerIp?: number;
|
|
357
|
+
rateLimitMaxPerToken?: number;
|
|
358
|
+
oauth?: {
|
|
359
|
+
enabled?: boolean;
|
|
360
|
+
accessTokenTtlSec?: number;
|
|
361
|
+
refreshIdleTtlSec?: number;
|
|
362
|
+
refreshMaxLifetimeSec?: number;
|
|
363
|
+
dcrMaxClients?: number;
|
|
364
|
+
};
|
|
350
365
|
}
|
|
351
366
|
interface EffectiveAiAccessSettings {
|
|
352
367
|
enabled: boolean;
|
|
@@ -354,18 +369,31 @@ interface EffectiveAiAccessSettings {
|
|
|
354
369
|
defaultLinkTtlSeconds: number;
|
|
355
370
|
defaultDeploymentType: DeploymentType;
|
|
356
371
|
maxActiveLinks: number;
|
|
372
|
+
publicBaseUrl: string;
|
|
373
|
+
corsAllowedOrigins: string[];
|
|
374
|
+
rateLimitWindowMs: number;
|
|
375
|
+
rateLimitMaxPerIp: number;
|
|
376
|
+
rateLimitMaxPerToken: number;
|
|
377
|
+
oauth: {
|
|
378
|
+
enabled: boolean;
|
|
379
|
+
accessTokenTtlSec: number;
|
|
380
|
+
refreshIdleTtlSec: number;
|
|
381
|
+
refreshMaxLifetimeSec: number;
|
|
382
|
+
dcrMaxClients: number;
|
|
383
|
+
};
|
|
357
384
|
}
|
|
358
385
|
type SettingsSource = 'default' | 'settings.js' | 'disk';
|
|
359
386
|
interface EffectiveAiAccessSettingsWithMeta extends EffectiveAiAccessSettings {
|
|
360
387
|
sources: Record<keyof EffectiveAiAccessSettings, SettingsSource>;
|
|
361
388
|
}
|
|
389
|
+
type Warn = (message: string) => void;
|
|
362
390
|
declare class SettingsStore {
|
|
363
391
|
private readonly paths;
|
|
364
392
|
private readonly filePath;
|
|
365
393
|
private disk;
|
|
366
394
|
private settingsJs;
|
|
367
395
|
private cached;
|
|
368
|
-
constructor(paths: AiAccessPaths, settingsJsAiAccess?: PersistedAiAccessSettings);
|
|
396
|
+
constructor(paths: AiAccessPaths, settingsJsAiAccess?: PersistedAiAccessSettings, warn?: Warn);
|
|
369
397
|
getEffective(): EffectiveAiAccessSettingsWithMeta;
|
|
370
398
|
getDisk(): PersistedAiAccessSettings;
|
|
371
399
|
/**
|
|
@@ -473,17 +501,25 @@ interface AiAccessSettings {
|
|
|
473
501
|
/**
|
|
474
502
|
* Explicit public base URL (e.g. `https://nodered.example.com`) used for
|
|
475
503
|
* absolute URLs (access-link `url`, proposal `reviewUrl`, `accessLinksUrl`).
|
|
476
|
-
* Overrides `Host` / `X-Forwarded-*`. settings.js
|
|
504
|
+
* Overrides `Host` / `X-Forwarded-*`. Editable in the AI Access settings sidebar unless locked by settings.js.
|
|
477
505
|
*/
|
|
478
506
|
publicBaseUrl?: string;
|
|
479
507
|
/**
|
|
480
508
|
* Browser origins allowed to call the AI-facing endpoints (`/mcp/a`,
|
|
481
509
|
* `/mcp/api`, `/mcp/rpc`) cross-origin, e.g. `['https://app.example.com']`.
|
|
482
|
-
* Default: none (no CORS headers). settings.js
|
|
510
|
+
* Default: none (no CORS headers). Editable in the AI Access settings sidebar unless locked by settings.js.
|
|
483
511
|
*/
|
|
484
512
|
corsAllowedOrigins?: string[];
|
|
513
|
+
oauth?: {
|
|
514
|
+
enabled?: boolean;
|
|
515
|
+
accessTokenTtlSec?: number;
|
|
516
|
+
refreshIdleTtlSec?: number;
|
|
517
|
+
/** Absolute refresh-family lifetime in seconds; default 7776000 (90 days), 0 = unlimited. */
|
|
518
|
+
refreshMaxLifetimeSec?: number;
|
|
519
|
+
dcrMaxClients?: number;
|
|
520
|
+
};
|
|
485
521
|
}
|
|
486
|
-
type AiAccessPermission = 'read' | '*';
|
|
522
|
+
type AiAccessPermission = 'read' | 'propose' | '*';
|
|
487
523
|
interface AiAccessCapabilities {
|
|
488
524
|
view: boolean;
|
|
489
525
|
manageLinks: boolean;
|
|
@@ -529,7 +565,13 @@ interface AiAccessStatusCallerAccessLink {
|
|
|
529
565
|
*/
|
|
530
566
|
baseUrls?: Partial<AiAccessStatusCallerBaseUrls>;
|
|
531
567
|
}
|
|
532
|
-
|
|
568
|
+
interface AiAccessStatusCallerOAuth {
|
|
569
|
+
kind: 'oauth';
|
|
570
|
+
clientId: string;
|
|
571
|
+
user: string;
|
|
572
|
+
scopes: string[];
|
|
573
|
+
}
|
|
574
|
+
type AiAccessStatusCaller = AiAccessStatusCallerAdmin | AiAccessStatusCallerAccessLink | AiAccessStatusCallerOAuth;
|
|
533
575
|
interface AiAccessStatus {
|
|
534
576
|
ok: boolean;
|
|
535
577
|
version: string;
|
|
@@ -557,6 +599,12 @@ interface AiAccessStatus {
|
|
|
557
599
|
interface NodeRedPluginSettings {
|
|
558
600
|
userDir: string;
|
|
559
601
|
httpAdminRoot?: string;
|
|
602
|
+
httpNodeRoot?: string | false;
|
|
603
|
+
httpStatic?: string | (string | {
|
|
604
|
+
path: string;
|
|
605
|
+
root?: string;
|
|
606
|
+
})[];
|
|
607
|
+
httpStaticRoot?: string;
|
|
560
608
|
adminAuth?: unknown;
|
|
561
609
|
aiAccess?: AiAccessSettings;
|
|
562
610
|
}
|
|
@@ -566,6 +614,21 @@ interface NodeRedAuthAPI {
|
|
|
566
614
|
interface NodeRedPluginAPI {
|
|
567
615
|
settings: NodeRedPluginSettings;
|
|
568
616
|
httpAdmin: Express;
|
|
617
|
+
httpNode?: Express;
|
|
618
|
+
nodes?: {
|
|
619
|
+
eachNode(listener: (node: {
|
|
620
|
+
id?: string;
|
|
621
|
+
type?: string;
|
|
622
|
+
url?: string;
|
|
623
|
+
name?: string;
|
|
624
|
+
z?: string;
|
|
625
|
+
d?: boolean;
|
|
626
|
+
disabled?: boolean;
|
|
627
|
+
}) => void): void;
|
|
628
|
+
};
|
|
629
|
+
events?: {
|
|
630
|
+
on(event: string, listener: () => void): void;
|
|
631
|
+
};
|
|
569
632
|
auth: NodeRedAuthAPI;
|
|
570
633
|
plugins: {
|
|
571
634
|
registerPlugin: (id: string, definition?: Record<string, unknown>) => void;
|
|
@@ -584,6 +647,8 @@ interface AccessLinkAuthInfo {
|
|
|
584
647
|
createdAt: string;
|
|
585
648
|
expiresAt: string;
|
|
586
649
|
createdBy?: string;
|
|
650
|
+
oauthClientId?: string;
|
|
651
|
+
oauthUser?: string;
|
|
587
652
|
}
|
|
588
653
|
interface AuthenticatedRequest extends Request {
|
|
589
654
|
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.2",
|
|
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": {
|