@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 +97 -0
- package/README.md +134 -12
- package/ai-access.html +1 -1
- package/dist/plugin.cjs +430 -86
- package/dist/plugin.d.cts +81 -9
- package/docs/README.md +2 -0
- package/docs/api/openapi.yaml +66 -6
- package/docs/errors.md +1 -1
- package/docs/mcp-clients.md +34 -0
- package/docs/releasing.md +82 -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
|
/**
|
|
@@ -470,8 +500,28 @@ interface AiAccessSettings {
|
|
|
470
500
|
* Max requests per access-link token per window (default: 300).
|
|
471
501
|
*/
|
|
472
502
|
rateLimitMaxPerToken?: number;
|
|
503
|
+
/**
|
|
504
|
+
* Explicit public base URL (e.g. `https://nodered.example.com`) used for
|
|
505
|
+
* absolute URLs (access-link `url`, proposal `reviewUrl`, `accessLinksUrl`).
|
|
506
|
+
* Overrides `Host` / `X-Forwarded-*`. Editable in the AI Access settings sidebar unless locked by settings.js.
|
|
507
|
+
*/
|
|
508
|
+
publicBaseUrl?: string;
|
|
509
|
+
/**
|
|
510
|
+
* Browser origins allowed to call the AI-facing endpoints (`/mcp/a`,
|
|
511
|
+
* `/mcp/api`, `/mcp/rpc`) cross-origin, e.g. `['https://app.example.com']`.
|
|
512
|
+
* Default: none (no CORS headers). Editable in the AI Access settings sidebar unless locked by settings.js.
|
|
513
|
+
*/
|
|
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
|
+
};
|
|
473
523
|
}
|
|
474
|
-
type AiAccessPermission = 'read' | '*';
|
|
524
|
+
type AiAccessPermission = 'read' | 'propose' | '*';
|
|
475
525
|
interface AiAccessCapabilities {
|
|
476
526
|
view: boolean;
|
|
477
527
|
manageLinks: boolean;
|
|
@@ -488,6 +538,8 @@ interface AiAccessStatusCallerBaseUrls {
|
|
|
488
538
|
rpc: string;
|
|
489
539
|
/** OpenAPI YAML URL. */
|
|
490
540
|
openapi: string;
|
|
541
|
+
/** OpenAPI JSON URL (same spec as `openapi`). */
|
|
542
|
+
openapiJson: string;
|
|
491
543
|
/** Unauthenticated JSON discovery index. */
|
|
492
544
|
discovery: string;
|
|
493
545
|
}
|
|
@@ -515,7 +567,13 @@ interface AiAccessStatusCallerAccessLink {
|
|
|
515
567
|
*/
|
|
516
568
|
baseUrls?: Partial<AiAccessStatusCallerBaseUrls>;
|
|
517
569
|
}
|
|
518
|
-
|
|
570
|
+
interface AiAccessStatusCallerOAuth {
|
|
571
|
+
kind: 'oauth';
|
|
572
|
+
clientId: string;
|
|
573
|
+
user: string;
|
|
574
|
+
scopes: string[];
|
|
575
|
+
}
|
|
576
|
+
type AiAccessStatusCaller = AiAccessStatusCallerAdmin | AiAccessStatusCallerAccessLink | AiAccessStatusCallerOAuth;
|
|
519
577
|
interface AiAccessStatus {
|
|
520
578
|
ok: boolean;
|
|
521
579
|
version: string;
|
|
@@ -530,7 +588,7 @@ interface AiAccessStatus {
|
|
|
530
588
|
*/
|
|
531
589
|
permission: AiAccessPermission | null;
|
|
532
590
|
capabilities: AiAccessCapabilities;
|
|
533
|
-
storageDir
|
|
591
|
+
storageDir?: string;
|
|
534
592
|
/** Effective default access-link TTL (seconds). */
|
|
535
593
|
defaultLinkTtlSeconds: number;
|
|
536
594
|
/** Effective max concurrent active access links. */
|
|
@@ -543,6 +601,8 @@ interface AiAccessStatus {
|
|
|
543
601
|
interface NodeRedPluginSettings {
|
|
544
602
|
userDir: string;
|
|
545
603
|
httpAdminRoot?: string;
|
|
604
|
+
httpNodeRoot?: string | false;
|
|
605
|
+
httpStatic?: string | string[];
|
|
546
606
|
adminAuth?: unknown;
|
|
547
607
|
aiAccess?: AiAccessSettings;
|
|
548
608
|
}
|
|
@@ -552,6 +612,16 @@ interface NodeRedAuthAPI {
|
|
|
552
612
|
interface NodeRedPluginAPI {
|
|
553
613
|
settings: NodeRedPluginSettings;
|
|
554
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
|
+
};
|
|
555
625
|
auth: NodeRedAuthAPI;
|
|
556
626
|
plugins: {
|
|
557
627
|
registerPlugin: (id: string, definition?: Record<string, unknown>) => void;
|
|
@@ -570,6 +640,8 @@ interface AccessLinkAuthInfo {
|
|
|
570
640
|
createdAt: string;
|
|
571
641
|
expiresAt: string;
|
|
572
642
|
createdBy?: string;
|
|
643
|
+
oauthClientId?: string;
|
|
644
|
+
oauthUser?: string;
|
|
573
645
|
}
|
|
574
646
|
interface AuthenticatedRequest extends Request {
|
|
575
647
|
user?: {
|
package/docs/README.md
CHANGED
|
@@ -18,6 +18,8 @@ bodies. Use a **GET link** only as a fallback for headerless GET-only clients.
|
|
|
18
18
|
- [MCP tools reference](./mcp-tools.md) — generated from live `tools/list`
|
|
19
19
|
schemas
|
|
20
20
|
- [Error codes](./errors.md) — code, HTTP status, meaning, hint, channels
|
|
21
|
+
- [Releasing](./releasing.md) — version bump, CHANGELOG, tag, npm trusted
|
|
22
|
+
publishing
|
|
21
23
|
- [OpenAPI 3.1](./api/openapi.yaml) — also served at
|
|
22
24
|
`GET /mcp/api/v1/openapi.yaml` (JSON: `GET /mcp/api/v1/openapi.json`)
|
|
23
25
|
- Discovery guide — `GET /mcp/api/v1` (unauthenticated JSON index)
|
package/docs/api/openapi.yaml
CHANGED
|
@@ -2,7 +2,7 @@ openapi: 3.1.0
|
|
|
2
2
|
|
|
3
3
|
info:
|
|
4
4
|
title: Node-RED AI Access Plugin API
|
|
5
|
-
version: 0.1.
|
|
5
|
+
version: 0.1.1
|
|
6
6
|
summary: Temporary, owner-approved Node-RED access for AI clients
|
|
7
7
|
description: |
|
|
8
8
|
REST surface exposed by the `@com55/node-red-ai-access` Node-RED plugin
|
|
@@ -30,10 +30,16 @@ info:
|
|
|
30
30
|
|
|
31
31
|
**GET `/mcp/a/<token>/…` never mints tokens.** There is no link-create
|
|
32
32
|
operation under the get-link surface (no `POST …/links`). Attempted mint
|
|
33
|
-
paths return **
|
|
33
|
+
paths return **403** `link_create_not_supported`. Only admins with `*`
|
|
34
34
|
may create tokens on `POST /mcp/api/v1/links`.
|
|
35
35
|
|
|
36
|
-
1. `GET {url}` — markdown docs (workflow, exact paths, errors, limits)
|
|
36
|
+
1. `GET {url}` — markdown docs (workflow, exact paths, errors, limits),
|
|
37
|
+
served by default as `text/html; charset=utf-8` (the same text escaped in
|
|
38
|
+
a `<pre>`), because some AI web fetchers only read HTML. Every response
|
|
39
|
+
under `/mcp/a/<token>` follows the same rule: `Accept: text/markdown`
|
|
40
|
+
or `application/json`, or `?format=md` / `?format=json`, return the
|
|
41
|
+
raw representation; `Accept: text/plain` returns the raw text as
|
|
42
|
+
`text/plain` (`Vary: Accept`).
|
|
37
43
|
2. `GET {url}/status`, `/flows`, …
|
|
38
44
|
3. Layered read (prefer before loading whole tabs):
|
|
39
45
|
`GET {url}/flows/{id}?summary=1` → `GET {url}/search?q=&flow=` →
|
|
@@ -419,6 +425,42 @@ paths:
|
|
|
419
425
|
'413':
|
|
420
426
|
$ref: '#/components/responses/PayloadTooLarge'
|
|
421
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
|
+
|
|
422
464
|
/links:
|
|
423
465
|
get:
|
|
424
466
|
tags: [Access links]
|
|
@@ -1426,7 +1468,7 @@ components:
|
|
|
1426
1468
|
- `invalid_ttl` (400): `ttlSeconds` is zero, negative, not a number, or over the cap for the kind
|
|
1427
1469
|
- `access_link_limit` (409): Too many active links
|
|
1428
1470
|
- `access_link_not_found` (404): Unknown link id on revoke
|
|
1429
|
-
- `link_create_not_supported` (
|
|
1471
|
+
- `link_create_not_supported` (403): Attempted mint under a GET link
|
|
1430
1472
|
- `rate_limited` (429): Per-IP or per-token limit
|
|
1431
1473
|
- `payload_too_large` (413): Body / proposal over ~1.5MB
|
|
1432
1474
|
- `invalid_json` (400): Malformed JSON body
|
|
@@ -1705,6 +1747,14 @@ components:
|
|
|
1705
1747
|
maxActiveLinks:
|
|
1706
1748
|
type: integer
|
|
1707
1749
|
minimum: 1
|
|
1750
|
+
publicBaseUrl:
|
|
1751
|
+
type: string
|
|
1752
|
+
format: uri
|
|
1753
|
+
corsAllowedOrigins:
|
|
1754
|
+
type: array
|
|
1755
|
+
items:
|
|
1756
|
+
type: string
|
|
1757
|
+
format: uri
|
|
1708
1758
|
additionalProperties: false
|
|
1709
1759
|
|
|
1710
1760
|
AiAccessSettingsResponse:
|
|
@@ -1752,6 +1802,12 @@ components:
|
|
|
1752
1802
|
$ref: '#/components/schemas/DeploymentType'
|
|
1753
1803
|
maxActiveLinks:
|
|
1754
1804
|
type: integer
|
|
1805
|
+
publicBaseUrl:
|
|
1806
|
+
type: string
|
|
1807
|
+
corsAllowedOrigins:
|
|
1808
|
+
type: array
|
|
1809
|
+
items:
|
|
1810
|
+
type: string
|
|
1755
1811
|
sources:
|
|
1756
1812
|
type: object
|
|
1757
1813
|
additionalProperties:
|
|
@@ -1777,7 +1833,6 @@ components:
|
|
|
1777
1833
|
- allowWithoutAdminAuth
|
|
1778
1834
|
- permission
|
|
1779
1835
|
- capabilities
|
|
1780
|
-
- storageDir
|
|
1781
1836
|
- defaultLinkTtlSeconds
|
|
1782
1837
|
- maxActiveLinks
|
|
1783
1838
|
- defaultDeploymentType
|
|
@@ -1807,6 +1862,9 @@ components:
|
|
|
1807
1862
|
$ref: '#/components/schemas/Capabilities'
|
|
1808
1863
|
storageDir:
|
|
1809
1864
|
type: string
|
|
1865
|
+
description: |
|
|
1866
|
+
Server-side plugin storage directory. Admin sessions only; omitted
|
|
1867
|
+
for access-link (AI token) callers.
|
|
1810
1868
|
defaultLinkTtlSeconds:
|
|
1811
1869
|
type: integer
|
|
1812
1870
|
maxActiveLinks:
|
|
@@ -1859,7 +1917,7 @@ components:
|
|
|
1859
1917
|
proposals:create, proposals:read-own)
|
|
1860
1918
|
baseUrls:
|
|
1861
1919
|
type: object
|
|
1862
|
-
required: [api, rpc, openapi, discovery]
|
|
1920
|
+
required: [api, rpc, openapi, openapiJson, discovery]
|
|
1863
1921
|
properties:
|
|
1864
1922
|
api:
|
|
1865
1923
|
type: string
|
|
@@ -1867,6 +1925,8 @@ components:
|
|
|
1867
1925
|
type: string
|
|
1868
1926
|
openapi:
|
|
1869
1927
|
type: string
|
|
1928
|
+
openapiJson:
|
|
1929
|
+
type: string
|
|
1870
1930
|
discovery:
|
|
1871
1931
|
type: string
|
|
1872
1932
|
|
package/docs/errors.md
CHANGED
|
@@ -25,7 +25,7 @@ GET-link `401` responses include a `WWW-Authenticate: Bearer …` header.
|
|
|
25
25
|
| `invalid_ttl` | 400 | `ttlSeconds` is zero, negative, not a number, or over the cap for the kind | ttlSeconds must be a positive number of seconds within the cap for the token kind: get_link ≤ 86400 (24h); rest and mcp ≤ 2592000 (30 days). | admin |
|
|
26
26
|
| `access_link_limit` | 409 | Too many active links | Revoke an existing link first | admin |
|
|
27
27
|
| `access_link_not_found` | 404 | Unknown link id on revoke | List links and use an existing id | admin |
|
|
28
|
-
| `link_create_not_supported` |
|
|
28
|
+
| `link_create_not_supported` | 403 | Attempted mint under a GET link | Admin mints via `POST /mcp/api/v1/links` only | GET |
|
|
29
29
|
| `rate_limited` | 429 | Per-IP or per-token limit | Wait `retryAfter` / `Retry-After` | GET, REST, MCP |
|
|
30
30
|
| `payload_too_large` | 413 | Body / proposal over ~1.5MB | Shrink content | GET, REST, MCP |
|
|
31
31
|
| `invalid_json` | 400 | Malformed JSON body | Fix Content-Type / body | REST, MCP |
|
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>`.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
Releases are published by the `Release` workflow
|
|
4
|
+
(`.github/workflows/release.yml`). Pushing a `v*` tag runs quality checks, the
|
|
5
|
+
production build and tests. It then publishes the package to npm and creates or
|
|
6
|
+
updates the GitHub release for that tag, with the CHANGELOG section as notes and
|
|
7
|
+
the `npm pack` tarball attached.
|
|
8
|
+
|
|
9
|
+
## Cut a release
|
|
10
|
+
|
|
11
|
+
1. Bump the version (no tag yet):
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm version 0.1.1 --no-git-tag-version
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. In `CHANGELOG.md`, rename `## [Unreleased]` to `## [0.1.1] - YYYY-MM-DD` (or
|
|
18
|
+
add that section) and summarise the changes. The workflow uses this section
|
|
19
|
+
as the release notes and fails if it is missing.
|
|
20
|
+
3. Commit, open a PR, and merge it to `main` once CI passes.
|
|
21
|
+
4. Tag the merge commit and push the tag:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
git checkout main && git pull
|
|
25
|
+
git tag -a v0.1.1 -m "v0.1.1"
|
|
26
|
+
git push origin v0.1.1
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The workflow fails early if the tag does not equal `v` + the `package.json`
|
|
30
|
+
version. If that version is already on npm, the publish step is skipped and only
|
|
31
|
+
the GitHub release is updated. Re-running the workflow is safe.
|
|
32
|
+
|
|
33
|
+
### Re-run for an existing tag
|
|
34
|
+
|
|
35
|
+
Use **Actions → Release → Run workflow** and enter the tag (for example
|
|
36
|
+
`v0.1.0`). The workflow checks out that tag and runs the same steps. Use this to
|
|
37
|
+
publish a version that was tagged before this workflow existed.
|
|
38
|
+
|
|
39
|
+
## npm authentication
|
|
40
|
+
|
|
41
|
+
The publish step supports two modes:
|
|
42
|
+
|
|
43
|
+
- **Trusted publishing (OIDC, preferred).** No secret is needed. The job has
|
|
44
|
+
`permissions: id-token: write` and uses Node 24 with the latest npm (trusted
|
|
45
|
+
publishing needs npm >= 11.5.1 and Node >= 22.14.0). Provenance is generated
|
|
46
|
+
automatically for a public package published from a public repository;
|
|
47
|
+
`--provenance` is also passed.
|
|
48
|
+
- **Token fallback.** If the repository secret `NPM_TOKEN` is set, it is used as
|
|
49
|
+
`NODE_AUTH_TOKEN` instead. Use this for the first publish, because a trusted
|
|
50
|
+
publisher can only be configured on a package that already exists on npm.
|
|
51
|
+
|
|
52
|
+
### First publish
|
|
53
|
+
|
|
54
|
+
1. On npmjs.com, create a granular access token with read and write access to
|
|
55
|
+
publish packages for the `@com55` scope. It needs "bypass 2FA" if your
|
|
56
|
+
account requires 2FA for writes, and a short expiry.
|
|
57
|
+
2. Add it to the GitHub repository as the Actions secret `NPM_TOKEN` (**Settings
|
|
58
|
+
→ Secrets and variables → Actions**).
|
|
59
|
+
3. Push the tag, or run the workflow manually with the existing tag.
|
|
60
|
+
|
|
61
|
+
### Switch to trusted publishing
|
|
62
|
+
|
|
63
|
+
After the package exists on npm:
|
|
64
|
+
|
|
65
|
+
1. Open npmjs.com → the package → **Settings → Trusted publishing**, and choose
|
|
66
|
+
**GitHub Actions**.
|
|
67
|
+
2. Enter:
|
|
68
|
+
- Organization or user: `com55`
|
|
69
|
+
- Repository: `node-red-ai-access`
|
|
70
|
+
- Workflow filename: `release.yml` (the filename only, with the extension)
|
|
71
|
+
- Environment: leave empty
|
|
72
|
+
- Allowed actions: enable direct `npm publish`. The workflow runs
|
|
73
|
+
`npm publish`, not `npm stage publish`.
|
|
74
|
+
3. Complete a publish through the workflow within 2 days, or the new
|
|
75
|
+
configuration expires and has to be recreated.
|
|
76
|
+
4. Delete the `NPM_TOKEN` secret and revoke the token on npmjs.com.
|
|
77
|
+
5. Optional: under the package's **Settings → Publishing access**, choose
|
|
78
|
+
"Require two-factor authentication and disallow tokens". Trusted publishing
|
|
79
|
+
keeps working.
|
|
80
|
+
|
|
81
|
+
`repository.url` in `package.json` must match this GitHub repository exactly, or
|
|
82
|
+
OIDC publishing is rejected.
|
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": {
|