@bonsae/nrg-runtime 0.33.0 → 0.35.0

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/index.cjs CHANGED
@@ -36,8 +36,6 @@ __export(runtime_exports, {
36
36
  Node: () => Node,
37
37
  NrgError: () => NrgError,
38
38
  SchemaType: () => SchemaType,
39
- defineConfigNode: () => defineConfigNode,
40
- defineIONode: () => defineIONode,
41
39
  defineModule: () => defineModule,
42
40
  defineSchema: () => defineSchema,
43
41
  registerType: () => registerType,
@@ -46,9 +44,11 @@ __export(runtime_exports, {
46
44
  module.exports = __toCommonJS(runtime_exports);
47
45
 
48
46
  // src/sdk/lib/server/nodes/symbols.ts
49
- var NRG_WIRE_HANDLERS = Symbol.for("nrg.wireHandlers");
47
+ var NRG_SETUP_CLOSE_HANDLER = Symbol.for("nrg.setupCloseHandler");
48
+ var NRG_SETUP_INPUT_HANDLER = Symbol.for("nrg.setupInputHandler");
50
49
  var NRG_NODE = Symbol.for("nrg.node");
51
50
  var NRG_CONFIG_NODE = Symbol.for("nrg.configNode");
51
+ var NRG_PORTS = Symbol.for("nrg.ports");
52
52
 
53
53
  // src/sdk/lib/shared/errors.ts
54
54
  var NrgError = class _NrgError extends Error {
@@ -65,7 +65,7 @@ function defineModule(definition) {
65
65
  if (!NodeClass[NRG_NODE]) {
66
66
  const name = NodeClass?.name || String(NodeClass);
67
67
  throw new NrgError(
68
- `defineModule: "${name}" is not an nrg node class \u2014 extend IONode/ConfigNode or use defineIONode/defineConfigNode.`
68
+ `defineModule: "${name}" is not an nrg node class \u2014 extend IONode/ConfigNode.`
69
69
  );
70
70
  }
71
71
  }
@@ -84,11 +84,6 @@ var Validator = class {
84
84
  // Non-mutating instance (no coercion, no defaults): a pure predicate. Used for
85
85
  // message/form validation so a validated msg/form is never silently rewritten.
86
86
  pureAjv;
87
- // `$id` → the schema object (and owner label) that first claimed it, recorded
88
- // by reserveSchemaId() at node registration. AJV caches compiled validators by
89
- // `$id`, so two *different* schemas sharing one `$id` is a silent bug — this
90
- // registry makes the collision fail loudly.
91
- registeredSchemaIds = /* @__PURE__ */ new Map();
92
87
  constructor(options) {
93
88
  const { customKeywords, customFormats, ...ajvOptions } = options || {};
94
89
  this.ajv = this.buildAjv(ajvOptions, true, customKeywords, customFormats);
@@ -129,7 +124,6 @@ var Validator = class {
129
124
  * Add custom keywords to the given ajv instance
130
125
  */
131
126
  addCustomKeywords(ajv, keywords) {
132
- if (!keywords) return;
133
127
  keywords.forEach((keyword) => {
134
128
  ajv.addKeyword(keyword);
135
129
  });
@@ -138,7 +132,6 @@ var Validator = class {
138
132
  * Add custom formats to the given ajv instance
139
133
  */
140
134
  addCustomFormats(ajv, formats) {
141
- if (!formats) return;
142
135
  Object.entries(formats).forEach(([name, validator]) => {
143
136
  if (validator instanceof RegExp) {
144
137
  ajv.addFormat(name, validator);
@@ -147,13 +140,22 @@ var Validator = class {
147
140
  }
148
141
  });
149
142
  }
143
+ // Object-keyed compile cache for `$id`-less schemas. AJV caches by `$id`; a
144
+ // schema without one (an ad-hoc `SchemaType.Object`, or a flow-author override
145
+ // a node memoizes and revalidates every message) would otherwise recompile —
146
+ // and accumulate — on every `compile()`. Keyed by the schema OBJECT so a reused
147
+ // instance compiles once; a WeakMap so the entry is collected with the schema.
148
+ // One per AJV (coercing vs pure) since each keeps its own compiled forms.
149
+ pureInlineCache = /* @__PURE__ */ new WeakMap();
150
+ mutateInlineCache = /* @__PURE__ */ new WeakMap();
150
151
  /**
151
- * Compile (and cache) a validator for a schema, using AJV's own registry as the
152
- * cache keyed by the schema's `$id`. `defineSchema` requires a unique `$id`,
153
- * so `getSchema($id)` returns the previously compiled validator and `compile`
154
- * registers it on first use. We never write `$id` (no mutation of the caller's
155
- * schema). A schema without `$id` (rare an ad-hoc `SchemaType.Object`) simply
156
- * compiles fresh each call.
152
+ * Compile (and cache) a validator for a schema. Schemas WITH an `$id` use AJV's
153
+ * own registry (`defineSchema` gives every schema a unique `$id`, so `getSchema`
154
+ * returns the previously compiled validator). Schemas WITHOUT one — a flow-author
155
+ * override, or an ad-hoc `SchemaType.Object` are cached by object reference
156
+ * here, so a caller that reuses the same schema instance compiles it only once
157
+ * (no unbounded recompile/leak). We never write `$id` (no mutation of the
158
+ * caller's schema).
157
159
  *
158
160
  * @param schema - JSON Schema to validate against
159
161
  * @param mutate - `true` (default) uses the coercing instance; `false` uses the
@@ -162,33 +164,16 @@ var Validator = class {
162
164
  createValidator(schema, mutate = true) {
163
165
  const ajv = mutate ? this.ajv : this.pureAjv;
164
166
  if (schema.$id) {
165
- const cached = ajv.getSchema(schema.$id);
166
- if (cached) return cached;
167
+ const cached2 = ajv.getSchema(schema.$id);
168
+ if (cached2) return cached2;
169
+ return ajv.compile(schema);
167
170
  }
168
- return ajv.compile(schema);
169
- }
170
- /**
171
- * Reserve a schema's `$id` at registration so a collision fails loudly.
172
- * {@link createValidator} caches compiled validators by `$id`, so if two
173
- * *different* schemas share one `$id` the second would silently validate
174
- * against the first. Reserving the same schema object again (a re-deploy, or
175
- * one schema reused across roles) is idempotent. A schema without `$id` is a
176
- * no-op here. Convention: `"<node-type>:<role>"`.
177
- *
178
- * @param schema - the schema being registered (its `$id` is the key)
179
- * @param owner - human-readable origin (e.g. `"my-node.config"`) for the error
180
- * @throws NrgError when `$id` is already held by a different schema object.
181
- */
182
- reserveSchemaId(schema, owner) {
183
- const id = schema.$id;
184
- if (!id) return;
185
- const existing = this.registeredSchemaIds.get(id);
186
- if (existing && existing.schema !== schema) {
187
- throw new NrgError(
188
- `Duplicate schema $id "${id}": declared by both ${existing.owner} and ${owner}. $id is the AJV compile-cache key and must be unique \u2014 a collision makes one schema validate against another. Convention: "<node-type>:<role>".`
189
- );
190
- }
191
- this.registeredSchemaIds.set(id, { schema, owner });
171
+ const cache = mutate ? this.mutateInlineCache : this.pureInlineCache;
172
+ const cached = cache.get(schema);
173
+ if (cached) return cached;
174
+ const compiled = ajv.compile(schema);
175
+ cache.set(schema, compiled);
176
+ return compiled;
192
177
  }
193
178
  /**
194
179
  * Validate data against a schema and return a structured result
@@ -304,25 +289,25 @@ function initValidator(RED) {
304
289
  }
305
290
 
306
291
  // src/sdk/lib/server/api/assets.ts
307
- var import_path = __toESM(require("path"), 1);
308
- var import_fs = __toESM(require("fs"), 1);
309
- var import_module = require("module");
310
- var CLIENT_ASSET = true ? "nrg.229bba2d.js" : "nrg.js";
292
+ var import_node_path = __toESM(require("node:path"), 1);
293
+ var import_node_fs = __toESM(require("node:fs"), 1);
294
+ var import_node_module = require("node:module");
295
+ var CLIENT_ASSET = true ? "nrg.718765b9.js" : "nrg.js";
311
296
  function serveFile(filePath) {
312
297
  return (_req, res, next) => {
313
- if (!import_fs.default.existsSync(filePath)) return next();
298
+ if (!import_node_fs.default.existsSync(filePath)) return next();
314
299
  res.setHeader("Content-Type", "application/javascript");
315
- import_fs.default.createReadStream(filePath).pipe(res);
300
+ import_node_fs.default.createReadStream(filePath).pipe(res);
316
301
  };
317
302
  }
318
303
  function initAssetsRoutes(router) {
319
- const resourcesDir = import_path.default.resolve(__dirname, "./resources");
320
- if (!import_fs.default.existsSync(resourcesDir)) return;
321
- const _require = (0, import_module.createRequire)(import_path.default.join(__dirname, "package.json"));
304
+ const resourcesDir = import_node_path.default.resolve(__dirname, "./resources");
305
+ if (!import_node_fs.default.existsSync(resourcesDir)) return;
306
+ const _require = (0, import_node_module.createRequire)(import_node_path.default.join(__dirname, "package.json"));
322
307
  const vueFile = process.env.NODE_ENV !== "production" ? _require.resolve("vue/dist/vue.esm-browser.js") : _require.resolve("vue/dist/vue.esm-browser.prod.js");
323
308
  router.get(
324
309
  `/nrg/assets/${CLIENT_ASSET}`,
325
- serveFile(import_path.default.join(resourcesDir, CLIENT_ASSET))
310
+ serveFile(import_node_path.default.join(resourcesDir, CLIENT_ASSET))
326
311
  );
327
312
  router.get("/nrg/assets/vue.esm-browser.prod.js", serveFile(vueFile));
328
313
  }
@@ -335,206 +320,41 @@ function initRoutes(RED) {
335
320
  initAssetsRoutes(RED.httpAdmin);
336
321
  }
337
322
 
338
- // src/sdk/lib/shared/schemas/factories.ts
339
- var import_typebox2 = require("@sinclair/typebox");
340
-
341
- // src/sdk/lib/shared/constants.ts
342
- var TYPED_INPUT_TYPES = [
343
- "msg",
344
- "flow",
345
- "global",
346
- "str",
347
- "num",
348
- "bool",
349
- "json",
350
- "bin",
351
- "re",
352
- "jsonata",
353
- "date",
354
- "env",
355
- "node",
356
- "cred"
357
- ];
358
-
359
- // src/sdk/lib/shared/schemas/base.ts
360
- var import_typebox = require("@sinclair/typebox");
361
- var TypedInputSchema = import_typebox.Type.Object(
362
- {
363
- value: import_typebox.Type.Union(
364
- [
365
- import_typebox.Type.String(),
366
- import_typebox.Type.Number(),
367
- import_typebox.Type.Boolean(),
368
- import_typebox.Type.Null()
369
- ],
370
- {
371
- description: "The actual value entered or selected.",
372
- default: ""
373
- }
374
- ),
375
- type: import_typebox.Type.Union(
376
- TYPED_INPUT_TYPES.map((type) => import_typebox.Type.Literal(type)),
377
- {
378
- description: "The type of the value (string, number, message property, etc.)",
379
- default: "str"
380
- }
381
- )
382
- },
383
- {
384
- description: "Represents a Node-RED TypedInput value and its type.",
385
- default: {
386
- type: "str",
387
- value: ""
323
+ // src/sdk/lib/shared/schemas/utils.ts
324
+ function getCredentialsFromSchema(schema) {
325
+ if (!schema?.properties) return void 0;
326
+ const result = {};
327
+ for (const [key, value] of Object.entries(schema.properties)) {
328
+ const property = value;
329
+ const isPassword = property.format === "password";
330
+ if (!isPassword) {
331
+ console.warn(
332
+ `[nrg] credential "${key}" has no format:"password" \u2014 it is stored as a visible (cleartext-in-editor) credential. Add { format: "password" } to mask it.`
333
+ );
388
334
  }
335
+ result[key] = {
336
+ // NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
337
+ required: false,
338
+ type: isPassword ? "password" : "text",
339
+ value: property.default ?? void 0
340
+ };
389
341
  }
390
- );
391
-
392
- // src/sdk/lib/shared/schemas/factories.ts
393
- var JSON_TYPES = /* @__PURE__ */ new Set([
394
- "null",
395
- "boolean",
396
- "object",
397
- "array",
398
- "number",
399
- "integer",
400
- "string"
401
- ]);
402
- function isJSONType(value) {
403
- return typeof value === "string" && JSON_TYPES.has(value);
404
- }
405
- function NodeRef(type, options) {
406
- return {
407
- ...import_typebox2.Type.String({
408
- description: options?.description || `Reference to ${type}`
409
- }),
410
- // `...options` first so user options (description, x-nrg-form, …) apply, but
411
- // the framework keys below win — a caller can't clobber `format`/
412
- // `x-nrg-node-type`/`Kind` and silently break NodeRef resolution.
413
- ...options,
414
- format: "node-id",
415
- "x-nrg-node-type": type,
416
- [import_typebox2.Kind]: "NodeRef"
417
- };
418
- }
419
- function TypedInput(options) {
420
- const opts = options ?? {};
421
- const restrictedTypes = opts["x-nrg-form"]?.typedInputTypes;
422
- if (restrictedTypes && restrictedTypes.length === 0) {
423
- throw new Error(
424
- "SchemaType.TypedInput: typedInputTypes must list at least one type."
425
- );
426
- }
427
- const explicitType = opts.default?.type;
428
- if (restrictedTypes && explicitType !== void 0 && !restrictedTypes.includes(explicitType)) {
429
- throw new Error(
430
- `SchemaType.TypedInput: default type "${explicitType}" is not one of its typedInputTypes [${restrictedTypes.join(", ")}].`
431
- );
432
- }
433
- const defaultType = explicitType ?? (restrictedTypes ? restrictedTypes.includes("str") ? "str" : restrictedTypes[0] : "str");
434
- return {
435
- ...TypedInputSchema,
436
- // `...options` first; the framework keys below win (see NodeRef).
437
- ...options,
438
- default: {
439
- ...TypedInputSchema.default,
440
- ...opts.default,
441
- type: defaultType
442
- },
443
- "x-nrg-typed-input": true,
444
- [import_typebox2.Kind]: "TypedInput"
445
- };
446
- }
447
- function NrgString(options) {
448
- return import_typebox2.Type.String(options);
449
- }
450
- function OutputReturnProperties(options) {
451
- return import_typebox2.Type.Record(
452
- import_typebox2.Type.Number(),
453
- import_typebox2.Type.String({ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }),
454
- {
455
- description: "Per-port return property, keyed by output port index. A missing entry falls back to `output`.",
456
- default: {},
457
- ...options
458
- }
459
- );
460
- }
461
- function OutputContextModes(options) {
462
- return import_typebox2.Type.Record(
463
- import_typebox2.Type.Number(),
464
- import_typebox2.Type.Union([
465
- import_typebox2.Type.Literal("carry"),
466
- import_typebox2.Type.Literal("trace"),
467
- import_typebox2.Type.Literal("reset")
468
- ]),
469
- {
470
- description: "Per-port context mode, keyed by output port index. A missing entry falls back to `carry`.",
471
- default: {},
472
- ...options
473
- }
474
- );
475
- }
476
- function NrgUnsafe(options) {
477
- return import_typebox2.Type.Unsafe(options);
342
+ return result;
478
343
  }
479
- var NRG_SCHEMA_TYPES_FACTORIES = {
480
- String: NrgString,
481
- Unsafe: NrgUnsafe,
482
- NodeRef,
483
- TypedInput,
484
- OutputReturnProperties,
485
- OutputContextModes
486
- };
487
- var SchemaType = Object.assign(
488
- {},
489
- import_typebox2.Type,
490
- NRG_SCHEMA_TYPES_FACTORIES
491
- );
492
- function markNonValidatable(schema) {
493
- const type = schema.type;
494
- const hasInvalidType = type !== void 0 && (Array.isArray(type) ? !type.every(isJSONType) : !isJSONType(type));
495
- if (hasInvalidType) {
496
- schema["x-nrg-skip-validation"] = true;
497
- if (schema.default !== void 0) {
498
- schema._default = schema.default;
499
- delete schema.default;
500
- }
501
- delete schema.type;
502
- }
503
- if (schema.properties) {
504
- for (const prop of Object.values(schema.properties)) {
505
- markNonValidatable(prop);
506
- }
507
- }
508
- if (schema.patternProperties) {
509
- for (const prop of Object.values(schema.patternProperties)) {
510
- markNonValidatable(prop);
511
- }
512
- }
513
- if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
514
- markNonValidatable(schema.additionalProperties);
515
- }
516
- if (schema.items) {
517
- markNonValidatable(schema.items);
518
- }
519
- if (schema.anyOf) {
520
- schema.anyOf.forEach((s) => markNonValidatable(s));
521
- }
522
- if (schema.allOf) {
523
- schema.allOf.forEach((s) => markNonValidatable(s));
524
- }
525
- if (schema.oneOf) {
526
- schema.oneOf.forEach((s) => markNonValidatable(s));
344
+ function getSettingsFromSchema(schema, nodeType) {
345
+ const settings = {};
346
+ if (!schema?.properties) return settings;
347
+ const prefix = nodeType.replace(/-./g, (x) => x[1].toUpperCase());
348
+ for (const [key, prop] of Object.entries(schema.properties)) {
349
+ const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
350
+ settings[settingKey] = {
351
+ value: prop.default,
352
+ exportable: prop.exportable ?? false
353
+ };
527
354
  }
528
- return schema;
529
- }
530
- function defineSchema(properties, options) {
531
- const schema = SchemaType.Object(properties, options);
532
- return markNonValidatable(schema);
355
+ return settings;
533
356
  }
534
357
 
535
- // src/sdk/lib/shared/schemas/index.ts
536
- var import_typebox3 = require("@sinclair/typebox");
537
-
538
358
  // src/sdk/lib/server/registration.ts
539
359
  async function registerType(RED, NodeClass) {
540
360
  RED.log.debug(`Registering Type: ${NodeClass.type}`);
@@ -546,49 +366,52 @@ async function registerType(RED, NodeClass) {
546
366
  if (!NodeClass.type) {
547
367
  throw new NrgError("type must be provided when registering the node");
548
368
  }
549
- await NodeClass.register(RED);
550
- RED.log.debug(`Type registered: ${NodeClass.type}`);
551
- }
552
- function collectNodeSchemas(NodeClass) {
553
- const found = [];
554
- const add = (role, schema) => {
555
- if (schema && typeof schema === "object") {
556
- found.push({ role, schema });
557
- }
558
- };
559
- add("config", NodeClass.configSchema);
560
- add("credentials", NodeClass.credentialsSchema);
561
- add("settings", NodeClass.settingsSchema);
562
- add("input", NodeClass.inputSchema);
563
- const outputs = NodeClass.outputsSchema;
564
- if (Array.isArray(outputs)) {
565
- outputs.forEach((schema, i) => add(`output[${i}]`, schema));
566
- } else if (outputs && typeof outputs === "object" && !(import_typebox3.Kind in outputs)) {
567
- for (const [port, schema] of Object.entries(outputs)) {
568
- add(`output.${port}`, schema);
569
- }
570
- } else {
571
- add("output", outputs);
369
+ if (NodeClass.color && !/^#[0-9A-Fa-f]{6}$/.test(NodeClass.color)) {
370
+ throw new NrgError(
371
+ `Invalid color "${NodeClass.color}" for ${NodeClass.type}: must be a 6-digit hex color like "#a6bbcf" (shorthand "#abc" is not accepted).`
372
+ );
572
373
  }
573
- return found;
574
- }
575
- function checkSchemaIds(RED, nodes) {
576
- for (const NodeClass of nodes) {
577
- for (const { role, schema } of collectNodeSchemas(NodeClass)) {
578
- const owner = `${NodeClass.type}.${role}`;
579
- const id = schema.$id;
580
- if (typeof id !== "string" || id.length === 0) {
581
- const props = schema.properties;
582
- if (props && Object.keys(props).length > 0) {
583
- RED.log.warn(
584
- `[nrg] ${owner} schema has properties but no $id \u2014 it recompiles on every validation and cannot be $ref'd. Build it with defineSchema(props, { $id: "${NodeClass.type}:${role}" }).`
585
- );
374
+ RED.nodes.registerType(
375
+ NodeClass.type,
376
+ function(config) {
377
+ RED.nodes.createNode(this, config);
378
+ const node = new NodeClass(RED, this, config, this.credentials);
379
+ Object.defineProperty(this, "_node", {
380
+ value: node,
381
+ writable: false,
382
+ configurable: false,
383
+ enumerable: false
384
+ });
385
+ const createdPromise = Promise.resolve(node.created?.()).catch(
386
+ (error) => {
387
+ const message = error instanceof Error ? error.message : String(error);
388
+ this.error("Error during created hook: " + message);
389
+ throw error;
586
390
  }
587
- continue;
391
+ );
392
+ createdPromise.catch(() => {
393
+ this.status({ fill: "red", shape: "ring", text: "created() failed" });
394
+ });
395
+ node[NRG_SETUP_CLOSE_HANDLER]();
396
+ if (NRG_SETUP_INPUT_HANDLER in node && typeof node[NRG_SETUP_INPUT_HANDLER] === "function") {
397
+ node[NRG_SETUP_INPUT_HANDLER](createdPromise);
588
398
  }
589
- RED.validator.reserveSchemaId(schema, owner);
399
+ },
400
+ {
401
+ credentials: NodeClass.credentialsSchema ? getCredentialsFromSchema(NodeClass.credentialsSchema) : void 0,
402
+ settings: NodeClass.settingsSchema ? getSettingsFromSchema(NodeClass.settingsSchema, NodeClass.type) : void 0
590
403
  }
404
+ );
405
+ NodeClass.validateSettings(RED);
406
+ try {
407
+ await Promise.resolve(NodeClass.registered?.(RED));
408
+ } catch (error) {
409
+ const message = error instanceof Error ? error.message : String(error);
410
+ RED.log.error(
411
+ `Error during registered hook for ${NodeClass.type}: ${message}`
412
+ );
591
413
  }
414
+ RED.log.debug(`Type registered: ${NodeClass.type}`);
592
415
  }
593
416
  function registerTypes(nodes) {
594
417
  const fn = Object.assign(
@@ -596,7 +419,6 @@ function registerTypes(nodes) {
596
419
  initValidator(RED);
597
420
  initRoutes(RED);
598
421
  try {
599
- checkSchemaIds(RED, nodes);
600
422
  RED.log.info("Registering node types in series");
601
423
  for (const NodeClass of nodes) {
602
424
  await registerType(RED, NodeClass);
@@ -613,7 +435,7 @@ function registerTypes(nodes) {
613
435
  }
614
436
 
615
437
  // src/sdk/lib/server/typed-input.ts
616
- var TypedInput2 = class {
438
+ var TypedInput = class {
617
439
  constructor(RED, node, input) {
618
440
  this.RED = RED;
619
441
  this.node = node;
@@ -684,7 +506,7 @@ function setupConfigProxy(opts) {
684
506
  if (fieldSchema?.["x-nrg-typed-input"] && value && typeof value === "object" && "type" in value && "value" in value) {
685
507
  let ref = typedInputCache.get(value);
686
508
  if (!ref) {
687
- ref = new TypedInput2(RED, node, value);
509
+ ref = new TypedInput(RED, node, value);
688
510
  typedInputCache.set(value, ref);
689
511
  }
690
512
  return ref;
@@ -711,31 +533,8 @@ function setupConfigProxy(opts) {
711
533
  return createProxy(config, schema);
712
534
  }
713
535
 
714
- // src/sdk/lib/shared/schemas/utils.ts
715
- function getCredentialsFromSchema(schema) {
716
- if (!schema?.properties) return void 0;
717
- const result = {};
718
- for (const [key, value] of Object.entries(schema.properties)) {
719
- const property = value;
720
- const isPassword = property.format === "password";
721
- if (!isPassword) {
722
- console.warn(
723
- `[nrg] credential "${key}" has no format:"password" \u2014 it is stored as a visible (cleartext-in-editor) credential. Add { format: "password" } to mask it.`
724
- );
725
- }
726
- result[key] = {
727
- // NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
728
- required: false,
729
- type: isPassword ? "password" : "text",
730
- value: property.default ?? void 0
731
- };
732
- }
733
- return result;
734
- }
735
-
736
536
  // src/sdk/lib/server/nodes/node.ts
737
- var cachedSettingsMap = /* @__PURE__ */ new WeakMap();
738
- var Node = class _Node {
537
+ var Node = class {
739
538
  /**
740
539
  * Runtime NRG-node brand — inherited by every subclass (IONode/ConfigNode and
741
540
  * factory-built classes) and checked by `defineModule` at runtime. See
@@ -747,6 +546,11 @@ var Node = class _Node {
747
546
  static configSchema;
748
547
  static credentialsSchema;
749
548
  static settingsSchema;
549
+ // Per-type settings, resolved from `RED.settings` and validated once by
550
+ // `validateSettings` at registration; the instance `settings` getter returns
551
+ // it. It's *assigned* (never mutated in place), so every node type gets its
552
+ // own value with no cross-subclass sharing — a plain static, no cache needed.
553
+ static resolvedSettings;
750
554
  static validateSettings(RED) {
751
555
  if (!this.settingsSchema) return;
752
556
  RED.log.info("Validating settings");
@@ -761,93 +565,28 @@ var Node = class _Node {
761
565
  }
762
566
  }
763
567
  for (const [key, prop] of Object.entries(properties)) {
764
- if (settings[key] === void 0) {
765
- const defaultValue = prop.default ?? prop._default;
766
- if (defaultValue !== void 0) {
767
- settings[key] = defaultValue;
768
- }
568
+ if (settings[key] === void 0 && prop._default !== void 0) {
569
+ settings[key] = prop._default;
769
570
  }
770
571
  }
771
572
  RED.validator.validate(settings, this.settingsSchema, {
772
573
  throwOnError: true
773
574
  });
774
- cachedSettingsMap.set(this, settings);
575
+ this.resolvedSettings = settings;
775
576
  RED.log.info("Settings are valid");
776
577
  }
777
- static #buildSettings(NC) {
778
- if (!NC.settingsSchema) return;
779
- const settings = {};
780
- const prefix = NC.type.replace(/-./g, (x) => x[1].toUpperCase());
781
- for (const [key, prop] of Object.entries(NC.settingsSchema.properties)) {
782
- const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
783
- settings[settingKey] = {
784
- value: prop.default,
785
- exportable: prop.exportable ?? false
786
- };
787
- }
788
- return settings;
789
- }
790
- /**
791
- * Registers this node class with Node-RED. Handles instance creation,
792
- * event handler wiring, settings validation, and the user's registered() hook.
793
- */
794
- static async register(RED) {
795
- const NodeClass = this;
796
- if (NodeClass.color && !/^#[0-9A-Fa-f]{6}$/.test(NodeClass.color)) {
797
- throw new NrgError(
798
- `Invalid color "${NodeClass.color}" for ${NodeClass.type}: must be a 6-digit hex color like "#a6bbcf" (shorthand "#abc" is not accepted).`
799
- );
800
- }
801
- RED.nodes.registerType(
802
- NodeClass.type,
803
- function(config) {
804
- RED.nodes.createNode(this, config);
805
- const node = new NodeClass(RED, this, config, this.credentials);
806
- Object.defineProperty(this, "_node", {
807
- value: node,
808
- writable: false,
809
- configurable: false,
810
- enumerable: false
811
- });
812
- const createdPromise = Promise.resolve(node.created?.()).catch(
813
- (error) => {
814
- const message = error instanceof Error ? error.message : String(error);
815
- this.error("Error during created hook: " + message);
816
- throw error;
817
- }
818
- );
819
- createdPromise.catch(() => {
820
- this.status({ fill: "red", shape: "ring", text: "created() failed" });
821
- });
822
- node[NRG_WIRE_HANDLERS](this, createdPromise);
823
- },
824
- {
825
- credentials: NodeClass.credentialsSchema ? getCredentialsFromSchema(NodeClass.credentialsSchema) : {},
826
- settings: _Node.#buildSettings(this)
827
- }
828
- );
829
- NodeClass.validateSettings(RED);
830
- try {
831
- await Promise.resolve(NodeClass.registered?.(RED));
832
- } catch (error) {
833
- const message = error instanceof Error ? error.message : String(error);
834
- RED.log.error(
835
- `Error during registered hook for ${NodeClass.type}: ${message}`
836
- );
837
- }
838
- }
839
578
  RED;
840
579
  node;
841
580
  context;
842
581
  config;
843
- timers = /* @__PURE__ */ new Set();
844
- intervals = /* @__PURE__ */ new Set();
582
+ #timers = /* @__PURE__ */ new Set();
583
+ #intervals = /* @__PURE__ */ new Set();
845
584
  constructor(RED, node, config, credentials) {
846
585
  this.RED = RED;
847
586
  this.node = node;
848
587
  const constructor = this.constructor;
849
588
  if (constructor.configSchema) {
850
- this.log("Validating configs");
589
+ this.node.log("Validating configs");
851
590
  const configResult = this.RED.validator.validate(
852
591
  config,
853
592
  constructor.configSchema,
@@ -858,7 +597,7 @@ var Node = class _Node {
858
597
  }
859
598
  );
860
599
  if (!configResult.valid && configResult.errors?.length) {
861
- this.warn(
600
+ this.node.warn(
862
601
  `Config validation errors: ${configResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
863
602
  );
864
603
  }
@@ -870,7 +609,7 @@ var Node = class _Node {
870
609
  schema: constructor.configSchema
871
610
  });
872
611
  if (constructor.credentialsSchema && credentials) {
873
- this.log("Validating credentials");
612
+ this.node.log("Validating credentials");
874
613
  const credResult = this.RED.validator.validate(
875
614
  credentials,
876
615
  constructor.credentialsSchema,
@@ -879,27 +618,30 @@ var Node = class _Node {
879
618
  }
880
619
  );
881
620
  if (!credResult.valid && credResult.errors?.length) {
882
- this.warn(
621
+ this.node.warn(
883
622
  `Credentials validation errors: ${credResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
884
623
  );
885
624
  }
886
625
  }
887
626
  }
888
- [NRG_WIRE_HANDLERS](nodeRedNode, createdPromise) {
889
- nodeRedNode.on(
627
+ // Wires the `close` handler, common to every node kind. IONode adds the `input`
628
+ // handler separately (NRG_SETUP_INPUT_HANDLER), so this setup carries none of the
629
+ // input-only `createdPromise`.
630
+ [NRG_SETUP_CLOSE_HANDLER]() {
631
+ this.node.on(
890
632
  "close",
891
633
  async (removed, done) => {
892
634
  try {
893
- this.log("Calling closed");
635
+ this.node.log("Calling closed");
894
636
  await this.#closed(removed);
895
- this.log("Node was closed");
637
+ this.node.log("Node was closed");
896
638
  done();
897
639
  } catch (error) {
898
640
  if (error instanceof Error) {
899
- this.error("Error while closing node: " + error.message);
641
+ this.node.error("Error while closing node: " + error.message);
900
642
  done(error);
901
643
  } else {
902
- this.error("Unknown error occurred while closing node");
644
+ this.node.error("Unknown error occurred while closing node");
903
645
  done(new Error("Unknown error occurred while closing node"));
904
646
  }
905
647
  }
@@ -910,12 +652,12 @@ var Node = class _Node {
910
652
  try {
911
653
  await Promise.resolve(this.closed?.(removed));
912
654
  } finally {
913
- this.log("clearing timers and intervals");
914
- this.timers.forEach((t) => clearTimeout(t));
915
- this.intervals.forEach((i) => clearInterval(i));
916
- this.timers.clear();
917
- this.intervals.clear();
918
- this.log("timers and intervals cleared");
655
+ this.node.log("clearing timers and intervals");
656
+ this.#timers.forEach((t) => clearTimeout(t));
657
+ this.#intervals.forEach((i) => clearInterval(i));
658
+ this.#timers.clear();
659
+ this.#intervals.clear();
660
+ this.node.log("timers and intervals cleared");
919
661
  }
920
662
  }
921
663
  i18n(key, substitutions) {
@@ -924,24 +666,24 @@ var Node = class _Node {
924
666
  }
925
667
  setTimeout(fn, ms) {
926
668
  const timer = setTimeout(() => {
927
- this.timers.delete(timer);
669
+ this.#timers.delete(timer);
928
670
  fn();
929
671
  }, ms);
930
- this.timers.add(timer);
672
+ this.#timers.add(timer);
931
673
  return timer;
932
674
  }
933
675
  setInterval(fn, ms) {
934
676
  const interval = setInterval(fn, ms);
935
- this.intervals.add(interval);
677
+ this.#intervals.add(interval);
936
678
  return interval;
937
679
  }
938
680
  clearTimeout(timer) {
939
681
  clearTimeout(timer);
940
- this.timers.delete(timer);
682
+ this.#timers.delete(timer);
941
683
  }
942
684
  clearInterval(interval) {
943
685
  clearInterval(interval);
944
- this.intervals.delete(interval);
686
+ this.#intervals.delete(interval);
945
687
  }
946
688
  on(event, callback) {
947
689
  this.node.on(event, callback);
@@ -969,7 +711,7 @@ var Node = class _Node {
969
711
  }
970
712
  get settings() {
971
713
  const constructor = this.constructor;
972
- return cachedSettingsMap.get(constructor) ?? {};
714
+ return constructor.resolvedSettings ?? {};
973
715
  }
974
716
  };
975
717
 
@@ -1048,56 +790,32 @@ function setupContext(context, store) {
1048
790
 
1049
791
  // src/sdk/lib/server/nodes/io-node.ts
1050
792
  var import_node_async_hooks = require("node:async_hooks");
1051
- function isSchemaLike(obj) {
1052
- return obj != null && typeof obj === "object" && import_typebox3.Kind in obj;
1053
- }
1054
793
  var RETURN_PROPERTY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1055
794
  var INPUT_KEY = "input";
1056
795
  var IONode = class _IONode extends Node {
1057
796
  static align;
797
+ /**
798
+ * Node palette color. The template-literal type only enforces a leading `#`
799
+ * (an exact 6-hex-digit template type is infeasible — it explodes to `TS2590`),
800
+ * so shorthand (`#abc`) and invalid hex type-check but are rejected at runtime.
801
+ * The real gate is the `/^#[0-9A-Fa-f]{6}$/` check in `Node.register`.
802
+ */
1058
803
  static color;
1059
- static inputSchema;
1060
- // outputsSchema accepts any schema shape: the raw sent value (per port) is
1061
- // validated, and results are frequently non-objects.
1062
- static outputsSchema;
1063
- static validateInput = false;
1064
- // A single boolean applies to every output port; a boolean[] sets the
1065
- // per-port default by base-output index (missing entries default to false).
1066
- static validateOutput = false;
804
+ // Port topology is TS-types-only: the build extracts a node's `Input`/`Output`
805
+ // generics and stamps them under `NRG_PORTS`. There is no schema fallback.
1067
806
  static get inputs() {
1068
- return this.inputSchema ? 1 : 0;
807
+ return this[NRG_PORTS]?.inputs ?? 0;
1069
808
  }
1070
809
  static get outputs() {
1071
- const s = this.outputsSchema;
1072
- if (!s) return 0;
1073
- if (Array.isArray(s)) return s.length;
1074
- if (isSchemaLike(s)) return 1;
1075
- const keys = Object.keys(s);
1076
- for (const key of keys) {
1077
- if (/^\d+$/.test(key)) {
1078
- throw new NrgError(
1079
- `outputsSchema record key "${key}" in ${this.type} looks numeric. Use descriptive string names (e.g. "success", "failure") to avoid JavaScript object key ordering issues.`
1080
- );
1081
- }
1082
- if (key === "error" || key === "complete" || key === "status") {
1083
- throw new NrgError(
1084
- `outputsSchema record key "${key}" in ${this.type} is reserved for built-in ports. Use a different name (e.g. "failed" instead of "error").`
1085
- );
1086
- }
1087
- }
1088
- return keys.length;
810
+ return this[NRG_PORTS]?.outputs ?? 0;
1089
811
  }
1090
812
  /**
1091
- * The names of the base output ports when `outputsSchema` is a record of
1092
- * named ports (`{ success, failure }`), in declaration order — otherwise
1093
- * `undefined` (a single schema or a positional array). Resolved here, where
1094
- * TypeBox's `Kind` symbol is intact, so the editor reads the names directly
1095
- * instead of guessing them from a serialized (symbol-stripped) schema.
813
+ * The names of the base output ports for a named-port node (`Port<T>` record
814
+ * in the `Output` generic), in declaration order — otherwise `undefined` (a
815
+ * single or positional output). Read straight off the injected topology.
1096
816
  */
1097
817
  static get outputPortNames() {
1098
- const s = this.outputsSchema;
1099
- if (!s || Array.isArray(s) || isSchemaLike(s)) return void 0;
1100
- return Object.keys(s);
818
+ return this[NRG_PORTS]?.outputNames;
1101
819
  }
1102
820
  /**
1103
821
  * Per-`input()`-invocation context, scoped via AsyncLocalStorage. One store
@@ -1113,7 +831,6 @@ var IONode = class _IONode extends Node {
1113
831
  * of the node runtime and is never imported in a browser.
1114
832
  */
1115
833
  static #invocation = new import_node_async_hooks.AsyncLocalStorage();
1116
- context;
1117
834
  constructor(RED, node, config, credentials) {
1118
835
  super(RED, node, config, credentials);
1119
836
  const context = node.context();
@@ -1137,10 +854,10 @@ var IONode = class _IONode extends Node {
1137
854
  }
1138
855
  }
1139
856
  }
1140
- [NRG_WIRE_HANDLERS](nodeRedNode, createdPromise) {
1141
- super[NRG_WIRE_HANDLERS](nodeRedNode, createdPromise);
1142
- const NC = this.constructor;
1143
- nodeRedNode.on(
857
+ // Wires the `input` handler (IONode only). The base Node wires `close`
858
+ // separately (NRG_SETUP_CLOSE_HANDLER); the registrar invokes both.
859
+ [NRG_SETUP_INPUT_HANDLER](createdPromise) {
860
+ this.node.on(
1144
861
  "input",
1145
862
  async (msg, send, done) => {
1146
863
  try {
@@ -1153,7 +870,7 @@ var IONode = class _IONode extends Node {
1153
870
  }
1154
871
  const store = { inputMsg: msg, send };
1155
872
  try {
1156
- nodeRedNode.log("Calling input");
873
+ this.node.log("Calling input");
1157
874
  const result = await this.#input(msg, store);
1158
875
  if (this.config.completePort) {
1159
876
  this.#sendToPort("complete", {
@@ -1166,7 +883,7 @@ var IONode = class _IONode extends Node {
1166
883
  });
1167
884
  }
1168
885
  done();
1169
- nodeRedNode.log("Input processed");
886
+ this.node.log("Input processed");
1170
887
  } catch (error) {
1171
888
  const errorMsg = error instanceof Error ? error.message : "Unknown error during input handling";
1172
889
  if (this.config.errorPort && !store.errorEmitted) {
@@ -1183,13 +900,13 @@ var IONode = class _IONode extends Node {
1183
900
  });
1184
901
  }
1185
902
  if (error instanceof Error) {
1186
- nodeRedNode.error(
903
+ this.node.error(
1187
904
  "Error while processing input: " + error.message,
1188
905
  msg
1189
906
  );
1190
907
  done(error);
1191
908
  } else {
1192
- nodeRedNode.error(
909
+ this.node.error(
1193
910
  "Unknown error occurred during input handling",
1194
911
  msg
1195
912
  );
@@ -1203,17 +920,19 @@ var IONode = class _IONode extends Node {
1203
920
  return void 0;
1204
921
  }
1205
922
  async #input(msg, store) {
1206
- const NodeClass = this.constructor;
1207
- const shouldValidateInput = this.config.validateInput ?? NodeClass.validateInput;
1208
- if (shouldValidateInput && NodeClass.inputSchema) {
1209
- this.log("Validating input");
1210
- this.RED.validator.validate(msg, NodeClass.inputSchema, {
1211
- // Pure predicate: never coerce or inject defaults into the live msg that
1212
- // continues downstream.
1213
- mutate: false,
1214
- throwOnError: true
1215
- });
1216
- this.log("Input is valid");
923
+ const shouldValidateInput = this.config.validateInput ?? false;
924
+ if (shouldValidateInput) {
925
+ const inputSchema = this.#effectiveInputSchema();
926
+ if (inputSchema) {
927
+ this.node.log("Validating input");
928
+ this.RED.validator.validate(msg, inputSchema, {
929
+ // Pure predicate: never coerce or inject defaults into the live msg
930
+ // that continues downstream.
931
+ mutate: false,
932
+ throwOnError: true
933
+ });
934
+ this.node.log("Input is valid");
935
+ }
1217
936
  }
1218
937
  return await _IONode.#invocation.run(
1219
938
  store,
@@ -1221,8 +940,8 @@ var IONode = class _IONode extends Node {
1221
940
  );
1222
941
  }
1223
942
  send(msg) {
1224
- const multi = this.baseOutputs > 1 && Array.isArray(msg);
1225
- const values = multi ? msg.slice(0, this.baseOutputs) : [msg];
943
+ const multi = this.#baseOutputs > 1 && Array.isArray(msg);
944
+ const values = multi ? msg.slice(0, this.#baseOutputs) : [msg];
1226
945
  const out = values.map((m, port) => {
1227
946
  if (m == null) return m;
1228
947
  this.#validatePort(m, port);
@@ -1240,39 +959,67 @@ var IONode = class _IONode extends Node {
1240
959
  }
1241
960
  /**
1242
961
  * Per-port output validation. A port validates when its flow-author flag
1243
- * (`config.validateOutputs[port]`) or the node's static `validateOutput`
1244
- * fallback is on and a schema exists for that port. The static fallback is
1245
- * a single boolean (all ports) or a boolean[] (per-port, by base index).
962
+ * (`config.validateOutputs[port]`, whose default the node author seeds in the
963
+ * config schema) is on and a schema exists for that port.
1246
964
  */
1247
965
  #validatePort(value, port) {
1248
- const NodeClass = this.constructor;
1249
- const staticFlag = NodeClass.validateOutput;
1250
- const staticForPort = Array.isArray(staticFlag) ? staticFlag[port] ?? false : staticFlag;
1251
- const configured = this.config.validateOutputs?.[port];
1252
- if (!(configured ?? staticForPort)) return;
1253
- const schema = this.#outputSchemaForPort(port);
966
+ if (!this.config.validateOutputs?.[port]) return;
967
+ const schema = this.#effectiveOutputSchema(port);
1254
968
  if (!schema) return;
1255
- this.log("Validating output");
969
+ this.node.log("Validating output");
1256
970
  this.RED.validator.validate(value, schema, {
1257
971
  // Pure predicate: don't coerce or default the outgoing value.
1258
972
  mutate: false,
1259
973
  throwOnError: true
1260
974
  });
1261
- this.log("Output is valid");
975
+ this.node.log("Output is valid");
976
+ }
977
+ // Per-instance cache of resolved data-validation schemas, keyed by the raw JSON
978
+ // string (a node's config is immutable for its lifetime). `null` = the string
979
+ // is unusable (bad JSON, or valid JSON that does not compile) → the boundary is
980
+ // not validated. Memoizing keeps the SAME parsed object across messages, so
981
+ // the validator's object-keyed compile cache hits: no per-message JSON.parse,
982
+ // no per-message recompile/leak, and the warning fires exactly once.
983
+ #overrideSchemas = /* @__PURE__ */ new Map();
984
+ /** Resolve a data-validation schema (a JSON-Schema string from config — the
985
+ * author default, overridable by the flow author in the editor) to a schema
986
+ * object, memoized per instance. `undefined` → no schema (blank / not a
987
+ * string), so the boundary is not validated. A string that is invalid JSON, or
988
+ * valid JSON that does NOT compile (a typo'd keyword), also yields `undefined`
989
+ * — warned once here rather than throwing on every message inside `validate()`. */
990
+ #resolveOverride(raw) {
991
+ if (typeof raw !== "string" || !raw.trim()) return void 0;
992
+ const cached = this.#overrideSchemas.get(raw);
993
+ if (cached !== void 0) return cached ?? void 0;
994
+ let resolved = null;
995
+ try {
996
+ const parsed = JSON.parse(raw);
997
+ this.RED.validator.createValidator(parsed, false);
998
+ resolved = parsed;
999
+ } catch (e) {
1000
+ const reason = e instanceof Error ? e.message : "parse/compile error";
1001
+ this.node.warn(
1002
+ `Ignoring an invalid data-validation schema (${reason}); skipping validation.`
1003
+ );
1004
+ }
1005
+ this.#overrideSchemas.set(raw, resolved);
1006
+ return resolved ?? void 0;
1007
+ }
1008
+ /** The schema used for INPUT validation: the `config.inputSchema` field (the
1009
+ * node author's default, overridable by the flow author). Data-validation
1010
+ * schemas are config-driven only — there is no static schema. */
1011
+ #effectiveInputSchema() {
1012
+ return this.#resolveOverride(this.config.inputSchema);
1262
1013
  }
1263
- /** Resolves the output schema for a base-output port: array `[port]`,
1264
- * record the port-th value, single schema → itself. */
1265
- #outputSchemaForPort(port) {
1266
- const raw = this.constructor.outputsSchema;
1267
- if (!raw) return void 0;
1268
- if (Array.isArray(raw)) return raw[port];
1269
- if (isSchemaLike(raw)) return raw;
1270
- return Object.values(raw)[port];
1014
+ /** The schema used for OUTPUT validation on a port: the `config.outputSchemas`
1015
+ * field for that port (author default, flow-author-overridable). */
1016
+ #effectiveOutputSchema(port) {
1017
+ return this.#resolveOverride(this.config.outputSchemas?.[port]);
1271
1018
  }
1272
1019
  /**
1273
1020
  * The return key for an output port — `"output"` unless a custom one is set
1274
- * via `outputReturnProperties[port]` (author default and/or flow-author
1275
- * override, only possible when the node declares `outputReturnProperties`).
1021
+ * via `outputReturnProperties[port]` (the node author's declared default and/or
1022
+ * the flow author's per-port override, which the editor always allows).
1276
1023
  * `this.send(x)` always means "x is the value at this port's return key",
1277
1024
  * never "x is the whole outgoing message".
1278
1025
  */
@@ -1285,8 +1032,8 @@ var IONode = class _IONode extends Node {
1285
1032
  }
1286
1033
  /**
1287
1034
  * Resolves the context mode for a base-output port from the flow author's
1288
- * per-port config (`config.outputContextModes[port]`, written by the editor
1289
- * when the node declares `outputContextModes`), falling back to `"carry"`.
1035
+ * per-port config (`config.outputContextModes[port]`, which the editor lets the
1036
+ * flow author set on any port), falling back to `"carry"`.
1290
1037
  */
1291
1038
  #resolveContextMode(port) {
1292
1039
  return this.config.outputContextModes?.[port] ?? "carry";
@@ -1308,19 +1055,28 @@ var IONode = class _IONode extends Node {
1308
1055
  return { ...input, [key]: value };
1309
1056
  }
1310
1057
  // --- Built-in port management ---
1311
- get baseOutputs() {
1058
+ // Private, override-proof accessors the framework reads for port routing. A
1059
+ // consumer field named `baseOutputs`/`totalOutputs` shadows the PUBLIC getters
1060
+ // below (self-inflicted), but never these `#` ones the framework relies on.
1061
+ get #baseOutputs() {
1312
1062
  return this.constructor.outputs ?? 0;
1313
1063
  }
1314
- get totalOutputs() {
1315
- let count = this.baseOutputs;
1064
+ get #totalOutputs() {
1065
+ let count = this.#baseOutputs;
1316
1066
  if (this.config.errorPort) count++;
1317
1067
  if (this.config.completePort) count++;
1318
1068
  if (this.config.statusPort) count++;
1319
1069
  return count;
1320
1070
  }
1071
+ get baseOutputs() {
1072
+ return this.#baseOutputs;
1073
+ }
1074
+ get totalOutputs() {
1075
+ return this.#totalOutputs;
1076
+ }
1321
1077
  /**
1322
1078
  * Send a message to a specific output port by index or name.
1323
- * Custom named ports are resolved from `outputsSchema` when it is a record.
1079
+ * Named ports are resolved from the node's named `Port<T>` Output generic.
1324
1080
  * Numeric indices refer to the base output ports (0-based).
1325
1081
  *
1326
1082
  * Built-in ports (`"error"`, `"complete"`, `"status"`) are managed by the
@@ -1338,13 +1094,18 @@ var IONode = class _IONode extends Node {
1338
1094
  if (typeof port === "string" && portIndex === null) {
1339
1095
  const keys = this.#namedPortKeys();
1340
1096
  throw new NrgError(
1341
- keys && keys.length ? `sendToPort("${port}") \u2014 unknown output port. Valid named ports: ${keys.map((n) => `"${n}"`).join(", ")}.` : `sendToPort("${port}") \u2014 this node has no named output ports. Make outputsSchema a record of named schemas, or send to a numeric port index.`
1097
+ keys && keys.length ? `sendToPort("${port}") \u2014 unknown output port. Valid named ports: ${keys.map((n) => `"${n}"`).join(", ")}.` : `sendToPort("${port}") \u2014 this node has no named output ports. Declare named ports with a Port<T> record in the node's Output generic, or send to a numeric port index.`
1342
1098
  );
1343
1099
  }
1344
- const mode = this.#resolveContextMode(portIndex ?? 0);
1100
+ const idx = portIndex ?? 0;
1101
+ if (msg == null) {
1102
+ this.#sendToPort(port, msg);
1103
+ return;
1104
+ }
1105
+ this.#validatePort(msg, idx);
1345
1106
  this.#sendToPort(
1346
1107
  port,
1347
- msg == null ? msg : this.#wrapOutgoing(msg, mode, portIndex ?? 0)
1108
+ this.#wrapOutgoing(msg, this.#resolveContextMode(idx), idx)
1348
1109
  );
1349
1110
  }
1350
1111
  #sendToPort(port, msg) {
@@ -1360,16 +1121,16 @@ var IONode = class _IONode extends Node {
1360
1121
  throw new NrgError(`Unknown output port "${port}".`);
1361
1122
  }
1362
1123
  }
1363
- const out = new Array(this.totalOutputs);
1124
+ const out = new Array(this.#totalOutputs);
1364
1125
  out[portIndex] = msg;
1365
1126
  this.node.send(out);
1366
1127
  }
1367
- /** The declared named output ports (record `outputsSchema`), or null when the
1368
- * node has none (no schema, a tuple schema, or a single schema). */
1128
+ /** The declared named output ports (from the injected `Output`-generic
1129
+ * topology's `Port<T>` record), or null when the node has none (a
1130
+ * positional/single output, or no declaration). */
1369
1131
  #namedPortKeys() {
1370
- const schema = this.constructor.outputsSchema;
1371
- if (!schema || Array.isArray(schema) || isSchemaLike(schema)) return null;
1372
- return Object.keys(schema);
1132
+ const NC = this.constructor;
1133
+ return NC[NRG_PORTS]?.outputNames ?? null;
1373
1134
  }
1374
1135
  #getNamedPortIndex(name) {
1375
1136
  const keys = this.#namedPortKeys();
@@ -1379,9 +1140,9 @@ var IONode = class _IONode extends Node {
1379
1140
  }
1380
1141
  #getBuiltinPortIndex(name) {
1381
1142
  if (name === "error") {
1382
- return this.config.errorPort ? this.baseOutputs : null;
1143
+ return this.config.errorPort ? this.#baseOutputs : null;
1383
1144
  }
1384
- let idx = this.baseOutputs;
1145
+ let idx = this.#baseOutputs;
1385
1146
  if (this.config.errorPort) idx++;
1386
1147
  if (name === "complete") {
1387
1148
  return this.config.completePort ? idx : null;
@@ -1391,9 +1152,9 @@ var IONode = class _IONode extends Node {
1391
1152
  }
1392
1153
  #nodeSource() {
1393
1154
  return {
1394
- id: this.id,
1155
+ id: this.node.id,
1395
1156
  type: this.constructor.type,
1396
- name: this.name
1157
+ name: this.node.name
1397
1158
  };
1398
1159
  }
1399
1160
  status(status) {
@@ -1451,7 +1212,6 @@ var ConfigNode = class extends Node {
1451
1212
  // NodeRef resolution) can verify a referenced node is a config node — the only
1452
1213
  // guard a JS author gets. See NRG_CONFIG_NODE in ./symbols.
1453
1214
  [NRG_CONFIG_NODE] = true;
1454
- context;
1455
1215
  constructor(RED, node, config, credentials) {
1456
1216
  super(RED, node, config, credentials);
1457
1217
  const context = node.context();
@@ -1480,96 +1240,224 @@ var ConfigNode = class extends Node {
1480
1240
  }
1481
1241
  };
1482
1242
 
1483
- // src/sdk/lib/server/nodes/factories.ts
1484
- function normalizeSchemas(schemas, outputsSchema) {
1485
- for (const schema of schemas) {
1486
- if (schema) markNonValidatable(schema);
1487
- }
1488
- if (!outputsSchema) return;
1489
- if (Array.isArray(outputsSchema)) {
1490
- for (const schema of outputsSchema) markNonValidatable(schema);
1491
- } else if (import_typebox3.Kind in outputsSchema) {
1492
- markNonValidatable(outputsSchema);
1493
- } else {
1494
- for (const schema of Object.values(outputsSchema)) {
1495
- markNonValidatable(schema);
1243
+ // src/sdk/lib/shared/schemas/factories.ts
1244
+ var import_typebox2 = require("@sinclair/typebox");
1245
+
1246
+ // src/sdk/lib/shared/constants.ts
1247
+ var TYPED_INPUT_TYPES = [
1248
+ "msg",
1249
+ "flow",
1250
+ "global",
1251
+ "str",
1252
+ "num",
1253
+ "bool",
1254
+ "json",
1255
+ "bin",
1256
+ "re",
1257
+ "jsonata",
1258
+ "date",
1259
+ "env",
1260
+ "node",
1261
+ "cred"
1262
+ ];
1263
+
1264
+ // src/sdk/lib/shared/schemas/base.ts
1265
+ var import_typebox = require("@sinclair/typebox");
1266
+ var TypedInputSchema = import_typebox.Type.Object(
1267
+ {
1268
+ value: import_typebox.Type.Union(
1269
+ [
1270
+ import_typebox.Type.String(),
1271
+ import_typebox.Type.Number(),
1272
+ import_typebox.Type.Boolean(),
1273
+ import_typebox.Type.Null()
1274
+ ],
1275
+ {
1276
+ description: "The actual value entered or selected.",
1277
+ default: ""
1278
+ }
1279
+ ),
1280
+ type: import_typebox.Type.Union(
1281
+ TYPED_INPUT_TYPES.map((type) => import_typebox.Type.Literal(type)),
1282
+ {
1283
+ description: "The type of the value (string, number, message property, etc.)",
1284
+ default: "str"
1285
+ }
1286
+ )
1287
+ },
1288
+ {
1289
+ description: "Represents a Node-RED TypedInput value and its type.",
1290
+ default: {
1291
+ type: "str",
1292
+ value: ""
1496
1293
  }
1497
1294
  }
1295
+ );
1296
+
1297
+ // src/sdk/lib/shared/schemas/factories.ts
1298
+ var JSON_TYPES = /* @__PURE__ */ new Set([
1299
+ "null",
1300
+ "boolean",
1301
+ "object",
1302
+ "array",
1303
+ "number",
1304
+ "integer",
1305
+ "string"
1306
+ ]);
1307
+ function isJSONType(value) {
1308
+ return typeof value === "string" && JSON_TYPES.has(value);
1498
1309
  }
1499
- function defineIONode(def) {
1500
- const NodeClass = class extends IONode {
1501
- static type = def.type;
1502
- static category = def.category ?? "function";
1503
- static color = def.color ?? "#a6bbcf";
1504
- static align = def.align;
1505
- static configSchema = def.configSchema;
1506
- static credentialsSchema = def.credentialsSchema;
1507
- static settingsSchema = def.settingsSchema;
1508
- static inputSchema = def.inputSchema;
1509
- static outputsSchema = def.outputsSchema;
1510
- static validateInput = def.validateInput ?? false;
1511
- static validateOutput = def.validateOutput ?? false;
1512
- static registered(RED) {
1513
- return def.registered?.(RED);
1514
- }
1515
- async input(msg) {
1516
- if (def.input) return def.input.call(this, msg);
1517
- }
1518
- async created() {
1519
- if (def.created) return def.created.call(this);
1310
+ function NodeRef(type, options) {
1311
+ return {
1312
+ ...import_typebox2.Type.String({
1313
+ description: options?.description || `Reference to ${type}`
1314
+ }),
1315
+ // `...options` first so user options (description, x-nrg-form, …) apply, but
1316
+ // the framework keys below win — a caller can't clobber `format`/
1317
+ // `x-nrg-node-type`/`Kind` and silently break NodeRef resolution.
1318
+ ...options,
1319
+ format: "node-id",
1320
+ "x-nrg-node-type": type,
1321
+ [import_typebox2.Kind]: "NodeRef"
1322
+ };
1323
+ }
1324
+ function TypedInput2(options) {
1325
+ const opts = options ?? {};
1326
+ const restrictedTypes = opts["x-nrg-form"]?.typedInputTypes;
1327
+ if (restrictedTypes && restrictedTypes.length === 0) {
1328
+ throw new Error(
1329
+ "SchemaType.TypedInput: typedInputTypes must list at least one type."
1330
+ );
1331
+ }
1332
+ const explicitType = opts.default?.type;
1333
+ if (restrictedTypes && explicitType !== void 0 && !restrictedTypes.includes(explicitType)) {
1334
+ throw new Error(
1335
+ `SchemaType.TypedInput: default type "${explicitType}" is not one of its typedInputTypes [${restrictedTypes.join(", ")}].`
1336
+ );
1337
+ }
1338
+ const defaultType = explicitType ?? (restrictedTypes ? restrictedTypes.includes("str") ? "str" : restrictedTypes[0] : "str");
1339
+ return {
1340
+ ...TypedInputSchema,
1341
+ // `...options` first; the framework keys below win (see NodeRef).
1342
+ ...options,
1343
+ default: {
1344
+ ...TypedInputSchema.default,
1345
+ ...opts.default,
1346
+ type: defaultType
1347
+ },
1348
+ "x-nrg-typed-input": true,
1349
+ [import_typebox2.Kind]: "TypedInput"
1350
+ };
1351
+ }
1352
+ function NrgString(options) {
1353
+ return import_typebox2.Type.String(options);
1354
+ }
1355
+ function OutputReturnProperties(options) {
1356
+ return import_typebox2.Type.Record(
1357
+ import_typebox2.Type.Number(),
1358
+ import_typebox2.Type.String({ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }),
1359
+ {
1360
+ description: "Per-port return property, keyed by output port index. A missing entry falls back to `output`.",
1361
+ default: {},
1362
+ ...options
1520
1363
  }
1521
- async closed(removed) {
1522
- if (def.closed) return def.closed.call(this, removed);
1364
+ );
1365
+ }
1366
+ function OutputContextModes(options) {
1367
+ return import_typebox2.Type.Record(
1368
+ import_typebox2.Type.Number(),
1369
+ import_typebox2.Type.Union([
1370
+ import_typebox2.Type.Literal("carry"),
1371
+ import_typebox2.Type.Literal("trace"),
1372
+ import_typebox2.Type.Literal("reset")
1373
+ ]),
1374
+ {
1375
+ description: "Per-port context mode, keyed by output port index. A missing entry falls back to `carry`.",
1376
+ default: {},
1377
+ ...options
1523
1378
  }
1524
- };
1525
- Object.defineProperty(NodeClass, "name", {
1526
- value: def.type.replace(
1527
- /(^|-)(\w)/g,
1528
- (_, __, c) => c.toUpperCase()
1529
- ),
1530
- configurable: true
1531
- });
1532
- normalizeSchemas(
1533
- [
1534
- NodeClass.configSchema,
1535
- NodeClass.credentialsSchema,
1536
- NodeClass.settingsSchema,
1537
- NodeClass.inputSchema
1538
- ],
1539
- NodeClass.outputsSchema
1540
1379
  );
1541
- return NodeClass;
1542
1380
  }
1543
- function defineConfigNode(def) {
1544
- const NodeClass = class extends ConfigNode {
1545
- static type = def.type;
1546
- static configSchema = def.configSchema;
1547
- static credentialsSchema = def.credentialsSchema;
1548
- static settingsSchema = def.settingsSchema;
1549
- static registered(RED) {
1550
- return def.registered?.(RED);
1381
+ function OutputSchemas(options) {
1382
+ return import_typebox2.Type.Record(import_typebox2.Type.Number(), import_typebox2.Type.String(), {
1383
+ description: "Per-port output data-validation schema (JSON Schema string), keyed by output port index.",
1384
+ default: {},
1385
+ ...options
1386
+ });
1387
+ }
1388
+ function InputSchema(options) {
1389
+ return import_typebox2.Type.String({
1390
+ description: "Input data-validation schema (JSON Schema string).",
1391
+ default: "",
1392
+ ...options
1393
+ });
1394
+ }
1395
+ function NrgUnsafe(options) {
1396
+ return import_typebox2.Type.Unsafe(options);
1397
+ }
1398
+ var NRG_SCHEMA_TYPES_FACTORIES = {
1399
+ String: NrgString,
1400
+ Unsafe: NrgUnsafe,
1401
+ NodeRef,
1402
+ TypedInput: TypedInput2,
1403
+ OutputReturnProperties,
1404
+ OutputContextModes,
1405
+ OutputSchemas,
1406
+ InputSchema
1407
+ };
1408
+ var SchemaType = Object.assign(
1409
+ {},
1410
+ import_typebox2.Type,
1411
+ NRG_SCHEMA_TYPES_FACTORIES
1412
+ );
1413
+ function markNonValidatable(schema) {
1414
+ const type = schema.type;
1415
+ const hasInvalidType = type !== void 0 && (Array.isArray(type) ? !type.every(isJSONType) : !isJSONType(type));
1416
+ if (hasInvalidType) {
1417
+ schema["x-nrg-skip-validation"] = true;
1418
+ if (schema.default !== void 0) {
1419
+ schema._default = schema.default;
1420
+ delete schema.default;
1551
1421
  }
1552
- async created() {
1553
- if (def.created) return def.created.call(this);
1422
+ delete schema.type;
1423
+ }
1424
+ if (schema.properties) {
1425
+ for (const prop of Object.values(schema.properties)) {
1426
+ markNonValidatable(prop);
1554
1427
  }
1555
- async closed(removed) {
1556
- if (def.closed) return def.closed.call(this, removed);
1428
+ }
1429
+ if (schema.patternProperties) {
1430
+ for (const prop of Object.values(schema.patternProperties)) {
1431
+ markNonValidatable(prop);
1557
1432
  }
1558
- };
1559
- Object.defineProperty(NodeClass, "name", {
1560
- value: def.type.replace(
1561
- /(^|-)(\w)/g,
1562
- (_, __, c) => c.toUpperCase()
1563
- ),
1564
- configurable: true
1433
+ }
1434
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
1435
+ markNonValidatable(schema.additionalProperties);
1436
+ }
1437
+ if (schema.items) {
1438
+ markNonValidatable(schema.items);
1439
+ }
1440
+ if (schema.anyOf) {
1441
+ schema.anyOf.forEach((s) => markNonValidatable(s));
1442
+ }
1443
+ if (schema.allOf) {
1444
+ schema.allOf.forEach((s) => markNonValidatable(s));
1445
+ }
1446
+ if (schema.oneOf) {
1447
+ schema.oneOf.forEach((s) => markNonValidatable(s));
1448
+ }
1449
+ return schema;
1450
+ }
1451
+ function defineSchema(properties, options) {
1452
+ const schema = SchemaType.Object(properties, {
1453
+ ...options,
1454
+ $id: options?.$id ?? crypto.randomUUID()
1565
1455
  });
1566
- normalizeSchemas([
1567
- NodeClass.configSchema,
1568
- NodeClass.credentialsSchema,
1569
- NodeClass.settingsSchema
1570
- ]);
1571
- return NodeClass;
1456
+ return markNonValidatable(schema);
1572
1457
  }
1458
+
1459
+ // src/sdk/lib/shared/schemas/index.ts
1460
+ var import_typebox3 = require("@sinclair/typebox");
1573
1461
  // Annotate the CommonJS export names for ESM import in node:
1574
1462
  0 && (module.exports = {
1575
1463
  ConfigNode,
@@ -1578,8 +1466,6 @@ function defineConfigNode(def) {
1578
1466
  Node,
1579
1467
  NrgError,
1580
1468
  SchemaType,
1581
- defineConfigNode,
1582
- defineIONode,
1583
1469
  defineModule,
1584
1470
  defineSchema,
1585
1471
  registerType,