@com55/node-red-ai-access 0.0.0-stage → 0.1.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.
@@ -0,0 +1,707 @@
1
+ import { Express, Request, Response, NextFunction } from 'express';
2
+
3
+ /**
4
+ * In-memory access-link store. Nothing is persisted — a process restart
5
+ * clears every link, so previously issued tokens become invalid.
6
+ */
7
+ /** Surface a token is minted for. Missing/undefined on legacy records → get_link. */
8
+ type AccessLinkKind$1 = 'get_link' | 'rest' | 'mcp';
9
+ interface AccessLinkPublic {
10
+ id: string;
11
+ kind: AccessLinkKind$1;
12
+ label?: string;
13
+ createdAt: string;
14
+ expiresAt: string;
15
+ /** ISO time of last successful token validation; omitted until first use. */
16
+ lastUsedAt?: string;
17
+ createdBy?: string;
18
+ }
19
+ interface AccessLinkCreated extends AccessLinkPublic {
20
+ /** Raw token — available while the in-memory link is active. */
21
+ token: string;
22
+ }
23
+ interface AccessLinkRecord {
24
+ id: string;
25
+ tokenHash: string;
26
+ /** Raw token kept in memory so admins can re-copy the GET URL. */
27
+ token: string;
28
+ /**
29
+ * Token surface. Optional for in-memory migration of pre-kind records
30
+ * (treated as `get_link` by {@link resolveAccessLinkKind}).
31
+ */
32
+ kind?: AccessLinkKind$1;
33
+ label?: string;
34
+ createdAtMs: number;
35
+ expiresAtMs: number;
36
+ lastUsedAtMs?: number;
37
+ createdBy?: string;
38
+ revokedAtMs?: number;
39
+ }
40
+ interface CreateAccessLinkInput {
41
+ ttlSeconds?: number;
42
+ label?: string;
43
+ createdBy?: string;
44
+ /** Defaults to `get_link` when omitted. */
45
+ kind?: AccessLinkKind$1;
46
+ }
47
+ interface AccessLinkStoreOptions {
48
+ /** Defaults to 14400 (4h). */
49
+ defaultTtlSeconds?: number;
50
+ /** Defaults to 10. */
51
+ maxActiveLinks?: number;
52
+ /** Injectable clock for tests. */
53
+ now?: () => number;
54
+ /** Injectable id/token generators for tests. */
55
+ randomId?: () => string;
56
+ randomToken?: () => string;
57
+ }
58
+ /** Result of looking up a raw token in the in-memory store. */
59
+ type AccessLinkValidateResult = {
60
+ status: 'active';
61
+ link: AccessLinkPublic;
62
+ } | {
63
+ status: 'expired';
64
+ expiresAt: string;
65
+ } | {
66
+ status: 'invalid';
67
+ };
68
+ declare class AccessLinkStore {
69
+ private readonly links;
70
+ private defaultTtlSeconds;
71
+ private maxActiveLinks;
72
+ private readonly now;
73
+ private readonly randomId;
74
+ private readonly randomToken;
75
+ constructor(options?: AccessLinkStoreOptions);
76
+ /** Hot-update defaults from AI Access settings (no restart). */
77
+ configureDefaults(options: {
78
+ defaultTtlSeconds?: number;
79
+ maxActiveLinks?: number;
80
+ }): void;
81
+ /** Active = not revoked and not past expiresAt. */
82
+ private isActive;
83
+ list(): AccessLinkPublic[];
84
+ 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
+ revoke(linkId: string): AccessLinkPublic;
91
+ /**
92
+ * Look up a link by id even if revoked/expired (still in memory).
93
+ * Used to enrich older audit rows that omitted label/expiry.
94
+ */
95
+ getById(linkId: string): AccessLinkPublic | null;
96
+ /**
97
+ * Validate a raw bearer token with distinct expired vs unknown/revoked.
98
+ * After a process restart the store is empty, so expired tokens cannot be
99
+ * told apart from unknown ones (both → `invalid`).
100
+ */
101
+ validateResult(token: string): AccessLinkValidateResult;
102
+ /**
103
+ * Validate a raw bearer token. Returns the public link metadata when active;
104
+ * otherwise null (unknown / expired / revoked / pre-restart).
105
+ */
106
+ validate(token: string): AccessLinkPublic | null;
107
+ /** Test helper: number of records still held in memory (including revoked). */
108
+ size(): number;
109
+ /** Test helper: wipe everything (simulates a process restart). */
110
+ clear(): void;
111
+ /**
112
+ * Test helper: insert or mutate a raw record (e.g. legacy without `kind`).
113
+ * Not for production use.
114
+ */
115
+ upsertRecordForTest(record: AccessLinkRecord): void;
116
+ }
117
+
118
+ /**
119
+ * Flow adapter contracts for in-process RED.runtime and Admin API fallback.
120
+ */
121
+ /** Admin API / runtime deployment types used when applying an update. */
122
+ type DeploymentType = 'nodes' | 'flows' | 'full';
123
+ /** One node (or tab) object from the flat Admin API v2 flows array. */
124
+ type FlowNode = Record<string, unknown> & {
125
+ id: string;
126
+ type: string;
127
+ z?: string;
128
+ };
129
+ /**
130
+ * Nested tab shape used by GET /flow/:id and by updateFlow payloads.
131
+ * Child nodes live in `nodes` / `configs` (not in the flat array form).
132
+ */
133
+ interface FlowTab {
134
+ id: string;
135
+ type?: string;
136
+ label?: string;
137
+ disabled?: boolean;
138
+ info?: string;
139
+ env?: unknown[];
140
+ nodes?: FlowNode[];
141
+ configs?: FlowNode[];
142
+ [key: string]: unknown;
143
+ }
144
+ interface FlowsSnapshot {
145
+ rev: string;
146
+ flows: FlowNode[];
147
+ }
148
+ interface FlowSnapshot {
149
+ rev: string;
150
+ flow: FlowTab;
151
+ }
152
+ interface UpdateFlowParams {
153
+ flowId: string;
154
+ /** Replacement tab content (label/disabled/info/env + nodes/configs). */
155
+ flow: Partial<FlowTab>;
156
+ /** Revision from a prior getFlows/getFlow — must match the live rev. */
157
+ expectedRev: string;
158
+ /** Defaults to `nodes` (modified-nodes deploy). */
159
+ deploymentType?: DeploymentType;
160
+ }
161
+ interface UpdateFlowResult {
162
+ flowId: string;
163
+ rev: string;
164
+ }
165
+ interface FlowAdapter {
166
+ readonly kind: 'red' | 'http';
167
+ getFlows(): Promise<FlowsSnapshot>;
168
+ getFlow(flowId: string): Promise<FlowSnapshot>;
169
+ updateFlow(params: UpdateFlowParams): Promise<UpdateFlowResult>;
170
+ }
171
+
172
+ /**
173
+ * On-disk layout under `<userDir>/.ai-access`.
174
+ */
175
+ interface AiAccessPaths {
176
+ root: string;
177
+ proposals: string;
178
+ logs: string;
179
+ /** Persisted AI Access settings (`settings.json`). */
180
+ settings: string;
181
+ }
182
+
183
+ /**
184
+ * Append-only JSONL audit log under `.ai-access/logs/`.
185
+ */
186
+
187
+ type AuditActor = {
188
+ kind: 'access-link';
189
+ linkId: string;
190
+ label?: string;
191
+ } | {
192
+ kind: 'admin';
193
+ username?: string;
194
+ } | {
195
+ kind: 'system';
196
+ };
197
+ interface AuditEntry {
198
+ id: string;
199
+ at: string;
200
+ action: string;
201
+ actor: AuditActor;
202
+ details?: Record<string, unknown>;
203
+ }
204
+ declare class AuditLog {
205
+ private readonly paths;
206
+ private readonly filePath;
207
+ private readonly now;
208
+ private readonly maxAppendBytes;
209
+ private seq;
210
+ constructor(paths: AiAccessPaths, options?: {
211
+ now?: () => number;
212
+ maxAppendBytes?: number;
213
+ });
214
+ append(action: string, actor: AuditActor, details?: Record<string, unknown>): AuditEntry;
215
+ list(limit?: number): AuditEntry[];
216
+ }
217
+
218
+ /**
219
+ * Proposal JSON store under `.ai-access/proposals/`.
220
+ */
221
+
222
+ type ProposalStatus = 'pending' | 'approved' | 'rejected' | 'failed' | 'withdrawn' | 'expired' | 'superseded';
223
+ type ProposalType = 'replace_flow';
224
+ interface ProposalCreator {
225
+ kind: 'access-link';
226
+ linkId: string;
227
+ label?: string;
228
+ }
229
+ type ProposalDecisionBy = {
230
+ kind: 'admin';
231
+ username?: string;
232
+ } | {
233
+ kind: 'access-link';
234
+ linkId: string;
235
+ label?: string;
236
+ };
237
+ interface ReplaceFlowPayload {
238
+ flowId: string;
239
+ flow: Partial<FlowTab>;
240
+ }
241
+ /** Soft validation notes stored with a proposal (scaffold gaps, not hard errors). */
242
+ interface ProposalWarning {
243
+ code: string;
244
+ error: string;
245
+ nodeId?: string;
246
+ type?: string;
247
+ property?: string;
248
+ configType?: string;
249
+ candidates?: string[];
250
+ }
251
+ interface Proposal {
252
+ id: string;
253
+ type: ProposalType;
254
+ status: ProposalStatus;
255
+ expectedRev: string;
256
+ payload: ReplaceFlowPayload;
257
+ /**
258
+ * Flow tab snapshot at `expectedRev` when the proposal was submitted.
259
+ * Diffs compare `payload.flow` against this base (not the live flow).
260
+ * Optional on legacy proposals created before this field existed.
261
+ */
262
+ baseFlow?: Partial<FlowTab>;
263
+ /**
264
+ * Soft validation notes from submit (missing config, unwired nodes, …).
265
+ * Never blocks approve — AI may scaffold first; humans configure later.
266
+ */
267
+ warnings?: ProposalWarning[];
268
+ /**
269
+ * Short human title (≤80 chars). Set at submit (AI-supplied or auto-generated).
270
+ * Optional on legacy proposals; API responses always resolve a title.
271
+ */
272
+ title?: string;
273
+ createdBy: ProposalCreator;
274
+ createdAt: string;
275
+ updatedAt: string;
276
+ decidedBy?: ProposalDecisionBy;
277
+ reason?: string;
278
+ resultRev?: string;
279
+ }
280
+ interface CreateProposalInput {
281
+ type: ProposalType;
282
+ expectedRev: string;
283
+ payload: ReplaceFlowPayload;
284
+ /** Live flow tab at expectedRev (stored for stable diffs). */
285
+ baseFlow: Partial<FlowTab>;
286
+ createdBy: ProposalCreator;
287
+ /** Sanitized short title (≤80 chars). */
288
+ title: string;
289
+ /** Soft validation notes to persist with the proposal. */
290
+ warnings?: ProposalWarning[];
291
+ }
292
+ declare class ProposalStore {
293
+ private readonly paths;
294
+ private readonly inFlight;
295
+ private readonly now;
296
+ private readonly randomId;
297
+ private readonly maxProposalBytes;
298
+ constructor(paths: AiAccessPaths, options?: {
299
+ now?: () => number;
300
+ randomId?: () => string;
301
+ maxProposalBytes?: number;
302
+ });
303
+ private fileFor;
304
+ /** Rebuild a proposal record so only validated fields are written to disk. */
305
+ private toDiskRecord;
306
+ private writeAtomic;
307
+ create(input: CreateProposalInput): Proposal;
308
+ get(id: string): Proposal;
309
+ list(filter?: {
310
+ linkId?: string;
311
+ status?: ProposalStatus;
312
+ flowId?: string;
313
+ }): Proposal[];
314
+ /**
315
+ * Begin an approve attempt. Throws if another approve is in flight or status
316
+ * is not pending. Caller must call finishApprove / failApprove / releaseLock.
317
+ */
318
+ beginApprove(id: string): Proposal;
319
+ releaseLock(id: string): void;
320
+ markApproved(id: string, decidedBy: ProposalDecisionBy, resultRev: string): Proposal;
321
+ markRejected(id: string, decidedBy: ProposalDecisionBy | undefined, reason: string): Proposal;
322
+ markFailed(id: string, decidedBy: ProposalDecisionBy | undefined, reason: string): Proposal;
323
+ markWithdrawn(id: string, decidedBy: ProposalDecisionBy, reason: string): Proposal;
324
+ /**
325
+ * Auto-close a pending proposal that can no longer be approved
326
+ * (`expired` = live rev moved; `superseded` = another proposal was approved).
327
+ */
328
+ markClosed(id: string, status: 'expired' | 'superseded', reason: string, decidedBy?: ProposalDecisionBy): Proposal;
329
+ }
330
+
331
+ /**
332
+ * Persisted AI Access settings under `<userDir>/.ai-access/settings.json`.
333
+ *
334
+ * 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
337
+ * 3. Built-in defaults
338
+ *
339
+ * Changes via PUT /mcp/api/v1/settings update memory + disk immediately
340
+ * (no Node-RED restart).
341
+ */
342
+
343
+ interface PersistedAiAccessSettings {
344
+ /** When false, AI features are off even if adminAuth is enabled. Default true. */
345
+ enabled?: boolean;
346
+ allowWithoutAdminAuth?: boolean;
347
+ defaultLinkTtlSeconds?: number;
348
+ defaultDeploymentType?: DeploymentType;
349
+ maxActiveLinks?: number;
350
+ }
351
+ interface EffectiveAiAccessSettings {
352
+ enabled: boolean;
353
+ allowWithoutAdminAuth: boolean;
354
+ defaultLinkTtlSeconds: number;
355
+ defaultDeploymentType: DeploymentType;
356
+ maxActiveLinks: number;
357
+ }
358
+ type SettingsSource = 'default' | 'settings.js' | 'disk';
359
+ interface EffectiveAiAccessSettingsWithMeta extends EffectiveAiAccessSettings {
360
+ sources: Record<keyof EffectiveAiAccessSettings, SettingsSource>;
361
+ }
362
+ declare class SettingsStore {
363
+ private readonly paths;
364
+ private readonly filePath;
365
+ private disk;
366
+ private settingsJs;
367
+ private cached;
368
+ constructor(paths: AiAccessPaths, settingsJsAiAccess?: PersistedAiAccessSettings);
369
+ getEffective(): EffectiveAiAccessSettingsWithMeta;
370
+ getDisk(): PersistedAiAccessSettings;
371
+ /**
372
+ * Replace disk overrides with the given partial (omit a key to clear that
373
+ * disk override and fall back to settings.js / default).
374
+ */
375
+ update(partial: PersistedAiAccessSettings): EffectiveAiAccessSettingsWithMeta;
376
+ /** Merge into existing disk overrides (only provided keys change). */
377
+ patch(partial: PersistedAiAccessSettings): EffectiveAiAccessSettingsWithMeta;
378
+ private persist;
379
+ }
380
+
381
+ /**
382
+ * Installed palette node-type discovery + unknown-type validation.
383
+ *
384
+ * Read-only: lists types from Node-RED's node catalogue and parses HTML
385
+ * registerType defaults when available. Never installs packages.
386
+ */
387
+
388
+ interface NodeSetInfo {
389
+ id: string;
390
+ module: string;
391
+ version?: string;
392
+ types: string[];
393
+ enabled?: boolean;
394
+ name?: string;
395
+ }
396
+ interface NodeTypesAdapter {
397
+ listNodeSets(): Promise<NodeSetInfo[]>;
398
+ getNodeConfigHtml?(setId: string, lang?: string): Promise<string | null>;
399
+ }
400
+ interface CatalogueModule {
401
+ id: string;
402
+ description?: string;
403
+ updated_at?: string;
404
+ version?: string;
405
+ keywords?: string[];
406
+ url?: string;
407
+ downloads?: number;
408
+ }
409
+ interface CatalogueJson {
410
+ modules?: CatalogueModule[];
411
+ [key: string]: unknown;
412
+ }
413
+ type CatalogueFetcher = (url: string, etag?: string) => Promise<{
414
+ status: number;
415
+ etag?: string;
416
+ body?: CatalogueJson | null;
417
+ }>;
418
+
419
+ /**
420
+ * Approve / reject proposals through the FlowAdapter with audit logging.
421
+ */
422
+
423
+ declare class ProposalService {
424
+ private readonly proposals;
425
+ private readonly audit;
426
+ private readonly adapter;
427
+ private readonly getDeploymentType;
428
+ constructor(proposals: ProposalStore, audit: AuditLog, adapter: FlowAdapter, getDeploymentType: () => DeploymentType);
429
+ approve(id: string, decidedBy: ProposalDecisionBy, options?: {
430
+ deploymentType?: DeploymentType;
431
+ }): Promise<Proposal>;
432
+ reject(id: string, decidedBy: ProposalDecisionBy, reason: string): Proposal;
433
+ }
434
+
435
+ /**
436
+ * Minimal Node-RED runtime surface used by the AI Access plugin.
437
+ */
438
+
439
+ /**
440
+ * Plugin settings read from settings.js (`aiAccess: { ... }`).
441
+ */
442
+ interface AiAccessSettings {
443
+ /**
444
+ * Master switch. When false, AI features stay off even with adminAuth.
445
+ * Default: true. Editable from the editor sidebar AI Access tab.
446
+ */
447
+ enabled?: boolean;
448
+ /**
449
+ * When adminAuth is not configured, AI features stay off unless this is true
450
+ * (intended for local-only setups).
451
+ */
452
+ allowWithoutAdminAuth?: boolean;
453
+ /** Default access-link TTL in seconds (default: 14400 = 4h). */
454
+ defaultLinkTtlSeconds?: number;
455
+ /** Maximum concurrent active access links (default: 10). */
456
+ maxActiveLinks?: number;
457
+ /** Deployment type used when approving proposals (default: nodes). */
458
+ defaultDeploymentType?: 'nodes' | 'flows' | 'full';
459
+ /**
460
+ * Rate-limit window in milliseconds for `/mcp/api` and `/mcp/a`
461
+ * (default: 60000).
462
+ */
463
+ rateLimitWindowMs?: number;
464
+ /**
465
+ * Max requests per IP per window (default: 600 — generous for editor + AI
466
+ * polling).
467
+ */
468
+ rateLimitMaxPerIp?: number;
469
+ /**
470
+ * Max requests per access-link token per window (default: 300).
471
+ */
472
+ rateLimitMaxPerToken?: number;
473
+ /**
474
+ * Explicit public base URL (e.g. `https://nodered.example.com`) used for
475
+ * absolute URLs (access-link `url`, proposal `reviewUrl`, `accessLinksUrl`).
476
+ * Overrides `Host` / `X-Forwarded-*`. settings.js only.
477
+ */
478
+ publicBaseUrl?: string;
479
+ /**
480
+ * Browser origins allowed to call the AI-facing endpoints (`/mcp/a`,
481
+ * `/mcp/api`, `/mcp/rpc`) cross-origin, e.g. `['https://app.example.com']`.
482
+ * Default: none (no CORS headers). settings.js only.
483
+ */
484
+ corsAllowedOrigins?: string[];
485
+ }
486
+ type AiAccessPermission = 'read' | '*';
487
+ interface AiAccessCapabilities {
488
+ view: boolean;
489
+ manageLinks: boolean;
490
+ approveProposals: boolean;
491
+ }
492
+ interface AiAccessStatusCallerAdmin {
493
+ kind: 'admin';
494
+ permission?: AiAccessPermission;
495
+ }
496
+ interface AiAccessStatusCallerBaseUrls {
497
+ /** REST API base (`{httpAdminRoot}/mcp/api/v1`). */
498
+ api: string;
499
+ /** MCP Streamable HTTP endpoint (`{httpAdminRoot}/mcp/rpc`). */
500
+ rpc: string;
501
+ /** OpenAPI YAML URL. */
502
+ openapi: string;
503
+ /** OpenAPI JSON URL (same spec as `openapi`). */
504
+ openapiJson: string;
505
+ /** Unauthenticated JSON discovery index. */
506
+ discovery: string;
507
+ }
508
+ interface AiAccessStatusCallerAccessLink {
509
+ kind: 'access-link';
510
+ linkId: string;
511
+ /** Token surface this Bearer was minted for (`get_link` | `rest` | `mcp`). */
512
+ tokenKind: AccessLinkKind;
513
+ label?: string;
514
+ /** ISO-8601 expiry of this access link. */
515
+ expiresAt?: string;
516
+ /** Whole minutes until `expiresAt` (0 when already past). */
517
+ /** Whole minutes left, rounded up (never 0 while the token is valid). */
518
+ minutesRemaining?: number;
519
+ /** Whole seconds left, rounded up. */
520
+ secondsRemaining?: number;
521
+ /**
522
+ * Capabilities granted to this access link (not admin scopes).
523
+ * Example: `status:read`, `flows:read`, `proposals:create`, `proposals:read-own`.
524
+ */
525
+ permissions?: string[];
526
+ /**
527
+ * Absolute admin paths this token can use.
528
+ * Surfaces the token cannot call are omitted (not merely marked).
529
+ */
530
+ baseUrls?: Partial<AiAccessStatusCallerBaseUrls>;
531
+ }
532
+ type AiAccessStatusCaller = AiAccessStatusCallerAdmin | AiAccessStatusCallerAccessLink;
533
+ interface AiAccessStatus {
534
+ ok: boolean;
535
+ version: string;
536
+ adminAuthEnabled: boolean;
537
+ /** Effective master switch (settings.js / disk). */
538
+ aiAccessEnabled: boolean;
539
+ aiFeaturesEnabled: boolean;
540
+ allowWithoutAdminAuth: boolean;
541
+ /**
542
+ * Admin session permission (`read` / `*`), or `null` for access-link callers
543
+ * (AI tokens are not admin sessions — see `caller`).
544
+ */
545
+ permission: AiAccessPermission | null;
546
+ capabilities: AiAccessCapabilities;
547
+ storageDir?: string;
548
+ /** Effective default access-link TTL (seconds). */
549
+ defaultLinkTtlSeconds: number;
550
+ /** Effective max concurrent active access links. */
551
+ maxActiveLinks: number;
552
+ /** Deployment type used when approving proposals. */
553
+ defaultDeploymentType: 'nodes' | 'flows' | 'full';
554
+ /** Who is calling: admin session or access-link Bearer. */
555
+ caller?: AiAccessStatusCaller;
556
+ }
557
+ interface NodeRedPluginSettings {
558
+ userDir: string;
559
+ httpAdminRoot?: string;
560
+ adminAuth?: unknown;
561
+ aiAccess?: AiAccessSettings;
562
+ }
563
+ interface NodeRedAuthAPI {
564
+ needsPermission(permission: string): (req: Request, res: Response, next: NextFunction) => void;
565
+ }
566
+ interface NodeRedPluginAPI {
567
+ settings: NodeRedPluginSettings;
568
+ httpAdmin: Express;
569
+ auth: NodeRedAuthAPI;
570
+ plugins: {
571
+ registerPlugin: (id: string, definition?: Record<string, unknown>) => void;
572
+ };
573
+ log?: {
574
+ info?: (message: string) => void;
575
+ warn?: (message: string) => void;
576
+ error?: (message: string) => void;
577
+ };
578
+ }
579
+ type AccessLinkKind = 'get_link' | 'rest' | 'mcp';
580
+ interface AccessLinkAuthInfo {
581
+ id: string;
582
+ kind: AccessLinkKind;
583
+ label?: string;
584
+ createdAt: string;
585
+ expiresAt: string;
586
+ createdBy?: string;
587
+ }
588
+ interface AuthenticatedRequest extends Request {
589
+ user?: {
590
+ username?: string;
591
+ permissions?: string | string[];
592
+ };
593
+ authInfo?: {
594
+ scope?: string | string[];
595
+ };
596
+ /** Set by the Bearer access-link middleware when a valid token is presented. */
597
+ accessLink?: AccessLinkAuthInfo;
598
+ }
599
+
600
+ /**
601
+ * Construct shared plugin services (stores, adapter, audit).
602
+ */
603
+
604
+ interface PluginServices {
605
+ linkStore: AccessLinkStore;
606
+ proposalStore: ProposalStore;
607
+ auditLog: AuditLog;
608
+ flowAdapter: FlowAdapter;
609
+ nodeTypesAdapter: NodeTypesAdapter;
610
+ proposalService: ProposalService;
611
+ settingsStore: SettingsStore;
612
+ /** Optional catalogue fetcher override (unit tests). */
613
+ catalogueFetcher?: CatalogueFetcher;
614
+ catalogueUrl?: string;
615
+ }
616
+ interface CreatePluginServicesOptions {
617
+ flowAdapter?: FlowAdapter;
618
+ nodeTypesAdapter?: NodeTypesAdapter;
619
+ catalogueFetcher?: CatalogueFetcher;
620
+ catalogueUrl?: string;
621
+ linkStore?: AccessLinkStore;
622
+ settingsStore?: SettingsStore;
623
+ /** When false, do not bind the process-wide settings store (tests). Default true. */
624
+ bindSettings?: boolean;
625
+ }
626
+ declare function createPluginServices(RED: NodeRedPluginAPI, options?: CreatePluginServicesOptions): PluginServices;
627
+
628
+ /**
629
+ * Bearer access-link middleware for AI-facing routes.
630
+ */
631
+
632
+ declare const ACCESS_LINK_INVALID_MESSAGE = "Access link is invalid, revoked, or no longer available (it may have expired, or Node-RED may have restarted). Ask the user for a new access link.";
633
+ declare const ACCESS_LINK_MISSING_MESSAGE = "Access link Bearer token is missing. Ask the user for a new access link and send Authorization: Bearer <token>.";
634
+ /**
635
+ * Authenticate AI clients with an access-link Bearer token of the expected kind.
636
+ * Always rejects when AI features are disabled.
637
+ */
638
+ declare function requireAccessLink(RED: NodeRedPluginAPI, store: AccessLinkStore, expectedKind: AccessLinkKind$1): (req: AuthenticatedRequest, res: Response, next: NextFunction) => void;
639
+
640
+ /**
641
+ * On-disk layout for proposals and logs (under the Node-RED userDir).
642
+ */
643
+
644
+ /**
645
+ * Hidden AI Access data directory: `<userDir>/.ai-access`.
646
+ * Contains `proposals/` JSON files and append-only `logs/`.
647
+ */
648
+ declare function getStorageDir(settings: NodeRedPluginSettings): string;
649
+
650
+ /**
651
+ * Resolve AI Access settings from settings.js + optional disk store.
652
+ */
653
+
654
+ declare function bindSettingsStore(store: SettingsStore | null): void;
655
+ declare function isAdminAuthEnabled(settings: NodeRedPluginSettings): boolean;
656
+ /**
657
+ * Effective AI Access settings. When a {@link SettingsStore} is bound,
658
+ * disk overrides apply without restart; otherwise falls back to settings.js.
659
+ */
660
+ declare function resolveAiAccessSettings(settings: NodeRedPluginSettings): EffectiveAiAccessSettings;
661
+ /**
662
+ * AI features require `enabled` (default true) and either adminAuth or the
663
+ * local-only `allowWithoutAdminAuth` opt-in.
664
+ */
665
+ declare function areAiFeaturesEnabled(settings: NodeRedPluginSettings): boolean;
666
+
667
+ /** Max serialized proposal JSON size (pretty-printed). */
668
+ declare const MAX_PROPOSAL_JSON_BYTES = 1500000;
669
+
670
+ /**
671
+ * Editor sidebar AI Access tab — script + styles served from
672
+ * GET /mcp/editor/ai-access-sidebar.js (Cache-Control: no-store).
673
+ *
674
+ * Companion `ai-access.html` is a thin loader that injects this file with a
675
+ * cache-bust query so Node-RED's cached companion HTML no longer freezes UX.
676
+ */
677
+ /** Derived from package.json via build-time `__AI_ACCESS_VERSION__` (not hand-typed). */
678
+ declare const EDITOR_SIDEBAR_ASSET_VERSION: string;
679
+ /** Full JS payload for GET /mcp/editor/ai-access-sidebar.js (injects CSS, then runs tab). */
680
+ declare function renderEditorSidebarJs(): string;
681
+ /**
682
+ * Thin companion HTML loader. Version is always {@link EDITOR_SIDEBAR_ASSET_VERSION}
683
+ * (from package.json) — never hand-typed in a second place.
684
+ */
685
+ declare function renderEditorSidebarLoaderHtml(version?: string): string;
686
+
687
+ /**
688
+ * Package version injected at build time (tsup / vitest `define`).
689
+ * Used for status payloads and editor-sidebar asset cache-busting.
690
+ */
691
+ /** Resolved package version (`package.json` via build define). */
692
+ declare function packageVersion(): string;
693
+
694
+ /**
695
+ * Node-RED plugin entry for @com55/node-red-ai-access.
696
+ *
697
+ * Registers /mcp admin page, access links, proposals, flows, logs APIs,
698
+ * and the /mcp/rpc Streamable HTTP endpoint.
699
+ */
700
+
701
+ declare function registerRoutes(RED: NodeRedPluginAPI, serviceOptions?: CreatePluginServicesOptions): void;
702
+ /**
703
+ * Node-RED plugin factory. Loaded via `ai-access.cjs` → `dist/plugin.cjs`.
704
+ */
705
+ declare function registerAiAccessPlugin(RED: NodeRedPluginAPI): void;
706
+
707
+ export { ACCESS_LINK_INVALID_MESSAGE, ACCESS_LINK_MISSING_MESSAGE, type AccessLinkAuthInfo, AccessLinkStore, type AiAccessCapabilities, type AiAccessPermission, type AiAccessSettings, type AiAccessStatus, EDITOR_SIDEBAR_ASSET_VERSION, MAX_PROPOSAL_JSON_BYTES, type NodeRedPluginAPI, SettingsStore, areAiFeaturesEnabled, bindSettingsStore, createPluginServices, registerAiAccessPlugin as default, getStorageDir, isAdminAuthEnabled, packageVersion, registerRoutes, renderEditorSidebarJs, renderEditorSidebarLoaderHtml, requireAccessLink, resolveAiAccessSettings };