@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/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 (`settings.json`). */
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 ProposalCreator {
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/settings.json`.
337
+ * Persisted AI Access settings under `<userDir>/.ai-access/config.json`.
333
338
  *
334
339
  * Precedence for effective values (highest wins):
335
- * 1. Disk file keys present in `.ai-access/settings.json` (sidebar AI Access UI)
336
- * 2. `settings.js` `aiAccess` block
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 only.
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 only.
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
- type AiAccessStatusCaller = AiAccessStatusCallerAdmin | AiAccessStatusCallerAccessLink;
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?: {
@@ -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:
@@ -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.1.1",
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.31.0",
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.12",
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": "^6.4.0",
109
+ "vite": "^8.3.5",
110
110
  "vitest": "^5.0.3"
111
111
  },
112
112
  "engines": {
113
- "node": ">=20.0.0",
113
+ "node": ">=20.19.0",
114
114
  "npm": ">=10.0.0"
115
115
  },
116
116
  "repository": {