@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/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
- /** Raw token kept in memory so admins can re-copy the GET URL. */
27
- token: string;
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 (`settings.json`). */
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 ProposalCreator {
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/settings.json`.
335
+ * Persisted AI Access settings under `<userDir>/.ai-access/config.json`.
333
336
  *
334
337
  * 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
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 only.
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 only.
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
- type AiAccessStatusCaller = AiAccessStatusCallerAdmin | AiAccessStatusCallerAccessLink;
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?: {
@@ -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.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.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": {