@bonsae/nrg-runtime 0.31.0 → 0.33.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.
@@ -27,18 +27,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  ));
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
 
30
- // src/core/server/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- CompletePortSchema: () => CompletePortSchema,
30
+ // src/sdk/lib/runtime.ts
31
+ var runtime_exports = {};
32
+ __export(runtime_exports, {
34
33
  ConfigNode: () => ConfigNode,
35
- ErrorPortSchema: () => ErrorPortSchema,
36
34
  IONode: () => IONode,
35
+ Kind: () => import_typebox3.Kind,
37
36
  Node: () => Node,
38
- NodeSourceSchema: () => NodeSourceSchema,
39
37
  NrgError: () => NrgError,
40
38
  SchemaType: () => SchemaType,
41
- StatusPortSchema: () => StatusPortSchema,
42
39
  defineConfigNode: () => defineConfigNode,
43
40
  defineIONode: () => defineIONode,
44
41
  defineModule: () => defineModule,
@@ -46,9 +43,14 @@ __export(index_exports, {
46
43
  registerType: () => registerType,
47
44
  registerTypes: () => registerTypes
48
45
  });
49
- module.exports = __toCommonJS(index_exports);
46
+ module.exports = __toCommonJS(runtime_exports);
50
47
 
51
- // src/core/shared/errors.ts
48
+ // src/sdk/lib/server/nodes/symbols.ts
49
+ var NRG_WIRE_HANDLERS = Symbol.for("nrg.wireHandlers");
50
+ var NRG_NODE = Symbol.for("nrg.node");
51
+ var NRG_CONFIG_NODE = Symbol.for("nrg.configNode");
52
+
53
+ // src/sdk/lib/shared/errors.ts
52
54
  var NrgError = class _NrgError extends Error {
53
55
  constructor(message) {
54
56
  super(message);
@@ -57,1426 +59,1525 @@ var NrgError = class _NrgError extends Error {
57
59
  }
58
60
  };
59
61
 
60
- // src/core/server/typed-input.ts
61
- var TypedInput = class {
62
- constructor(RED, node, input) {
63
- this.RED = RED;
64
- this.node = node;
65
- this.input = input;
66
- }
67
- resolvers = {
68
- // evaluateNodeProperty returns the node ID string for "node" type —
69
- // resolve it to the actual node instance via RED.nodes.getNode,
70
- // then surface the NRG wrapper if available.
71
- node: (raw) => {
72
- if (typeof raw === "string") {
73
- const node = this.RED.nodes.getNode(raw);
74
- return node?._node ?? node ?? raw;
75
- }
76
- return raw?._node ?? raw;
77
- }
78
- };
79
- get type() {
80
- return this.input.type;
81
- }
82
- get value() {
83
- return this.input.value;
84
- }
85
- resolve(msg) {
86
- return new Promise((resolve, reject) => {
87
- this.RED.util.evaluateNodeProperty(
88
- this.input.value,
89
- this.input.type,
90
- this.node,
91
- msg,
92
- (err, raw) => {
93
- if (err) return reject(err);
94
- const post = this.resolvers[this.input.type];
95
- resolve(post ? post(raw) : raw);
96
- }
62
+ // src/sdk/lib/server/module.ts
63
+ function defineModule(definition) {
64
+ for (const NodeClass of definition.nodes) {
65
+ if (!NodeClass[NRG_NODE]) {
66
+ const name = NodeClass?.name || String(NodeClass);
67
+ throw new NrgError(
68
+ `defineModule: "${name}" is not an nrg node class \u2014 extend IONode/ConfigNode or use defineIONode/defineConfigNode.`
97
69
  );
98
- });
70
+ }
99
71
  }
100
- };
72
+ return definition;
73
+ }
101
74
 
102
- // src/core/server/nodes/proxy.ts
103
- function setupConfigProxy(opts) {
104
- const { RED, node, config, schema } = opts;
105
- const SKIP_PROPS = /* @__PURE__ */ new Set(["id", "_id", "_users"]);
106
- const nodeRefProps = /* @__PURE__ */ new Set();
107
- const typedInputProps = /* @__PURE__ */ new Set();
108
- if (schema?.properties) {
109
- for (const [key, propSchema] of Object.entries(schema.properties)) {
110
- const s = propSchema;
111
- if (s?.["x-nrg-node-type"]) nodeRefProps.add(key);
112
- if (s?.["x-nrg-typed-input"]) typedInputProps.add(key);
113
- }
75
+ // src/sdk/lib/shared/validator.ts
76
+ var import_ajv = __toESM(require("ajv"), 1);
77
+ var import_ajv_formats = __toESM(require("ajv-formats"), 1);
78
+ var import_ajv_errors = __toESM(require("ajv-errors"), 1);
79
+ var Validator = class {
80
+ // Coercing instance (coerceTypes + useDefaults): mutates the validated object.
81
+ // Used for the config plane, where `this.config.<field>` must read as its typed
82
+ // value (the proxy returns whatever coercion left on the raw config).
83
+ ajv;
84
+ // Non-mutating instance (no coercion, no defaults): a pure predicate. Used for
85
+ // message/form validation so a validated msg/form is never silently rewritten.
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
+ constructor(options) {
93
+ const { customKeywords, customFormats, ...ajvOptions } = options || {};
94
+ this.ajv = this.buildAjv(ajvOptions, true, customKeywords, customFormats);
95
+ this.pureAjv = this.buildAjv(
96
+ ajvOptions,
97
+ false,
98
+ customKeywords,
99
+ customFormats
100
+ );
114
101
  }
115
- const cache = /* @__PURE__ */ new WeakMap();
116
- const createProxy = (obj) => {
117
- const cached = cache.get(obj);
118
- if (cached) return cached;
119
- if (Array.isArray(obj)) {
120
- const mapped = obj.map(
121
- (item) => item && typeof item === "object" ? createProxy(item) : item
122
- );
123
- cache.set(obj, mapped);
124
- return mapped;
125
- }
126
- const proxy = new Proxy(obj, {
127
- get(target, prop) {
128
- if (typeof prop === "symbol") return target[prop];
129
- if (SKIP_PROPS.has(prop)) return target[prop];
130
- const value = target[prop];
131
- if (nodeRefProps.has(prop)) {
132
- if (typeof value !== "string" || value.length === 0) return void 0;
133
- return RED.nodes.getNode(value)?._node ?? value;
134
- }
135
- if (typedInputProps.has(prop) && value && typeof value === "object" && "type" in value && "value" in value) {
136
- let ref = cache.get(value);
137
- if (!ref) {
138
- ref = new TypedInput(RED, node, value);
139
- cache.set(value, ref);
140
- }
141
- return ref;
142
- }
143
- if (value && typeof value === "object") {
144
- return createProxy(value);
145
- }
146
- return value;
102
+ buildAjv(ajvOptions, mutate, customKeywords, customFormats) {
103
+ const ajv = new import_ajv.default({
104
+ allErrors: true,
105
+ code: {
106
+ source: false
147
107
  },
148
- set(_target, prop) {
149
- throw new NrgError(
150
- `Cannot set property '${String(prop)}' on read-only node config`
151
- );
152
- }
108
+ // coerceTypes + useDefaults both WRITE into the validated object; gate them
109
+ // on `mutate` so the pure instance leaves data untouched.
110
+ coerceTypes: mutate,
111
+ removeAdditional: false,
112
+ strict: false,
113
+ strictSchema: false,
114
+ useDefaults: mutate,
115
+ validateFormats: true,
116
+ // NOTE: typebox handles validation via typescript
117
+ // NOTE: if true, types that are not serializable JSON, like Function, would not work
118
+ validateSchema: false,
119
+ verbose: true,
120
+ ...ajvOptions
153
121
  });
154
- cache.set(obj, proxy);
155
- return proxy;
156
- };
157
- return createProxy(config);
158
- }
159
-
160
- // src/core/server/utils.ts
161
- function getCredentialsFromSchema(schema) {
162
- const result = {};
163
- for (const [key, value] of Object.entries(schema.properties)) {
164
- const property = value;
165
- const isPassword = property.format === "password";
166
- if (!isPassword) {
167
- console.warn(
168
- `[nrg] credential "${key}" has no format:"password" \u2014 it is stored as a visible (cleartext-in-editor) credential. Add { format: "password" } to mask it.`
169
- );
170
- }
171
- result[key] = {
172
- // NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
173
- required: false,
174
- type: isPassword ? "password" : "text",
175
- value: property.default ?? void 0
176
- };
122
+ (0, import_ajv_formats.default)(ajv);
123
+ (0, import_ajv_errors.default)(ajv);
124
+ this.addCustomKeywords(ajv, customKeywords || []);
125
+ this.addCustomFormats(ajv, customFormats || {});
126
+ return ajv;
177
127
  }
178
- return result;
179
- }
180
-
181
- // src/core/server/nodes/symbols.ts
182
- var WIRE_HANDLERS = Symbol.for("nrg.wireHandlers");
183
-
184
- // src/core/server/nodes/node.ts
185
- var cachedSettingsMap = /* @__PURE__ */ new WeakMap();
186
- var Node = class _Node {
187
- static type;
188
- static category;
189
- static configSchema;
190
- static credentialsSchema;
191
- static settingsSchema;
192
- static validateSettings(RED) {
193
- if (!this.settingsSchema) return;
194
- RED.log.info("Validating settings");
195
- const prefix = this.type.replace(/-./g, (x) => x[1].toUpperCase());
196
- const properties = this.settingsSchema.properties;
197
- const settings = {};
198
- for (const key of Object.keys(properties)) {
199
- const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
200
- const value = RED.settings[settingKey];
201
- if (value !== void 0) {
202
- settings[key] = value;
203
- }
204
- }
205
- for (const [key, prop] of Object.entries(properties)) {
206
- if (settings[key] === void 0) {
207
- const defaultValue = prop.default ?? prop._default;
208
- if (defaultValue !== void 0) {
209
- settings[key] = defaultValue;
210
- }
128
+ /**
129
+ * Add custom keywords to the given ajv instance
130
+ */
131
+ addCustomKeywords(ajv, keywords) {
132
+ if (!keywords) return;
133
+ keywords.forEach((keyword) => {
134
+ ajv.addKeyword(keyword);
135
+ });
136
+ }
137
+ /**
138
+ * Add custom formats to the given ajv instance
139
+ */
140
+ addCustomFormats(ajv, formats) {
141
+ if (!formats) return;
142
+ Object.entries(formats).forEach(([name, validator]) => {
143
+ if (validator instanceof RegExp) {
144
+ ajv.addFormat(name, validator);
145
+ } else {
146
+ ajv.addFormat(name, { validate: validator });
211
147
  }
212
- }
213
- RED.validator.validate(settings, this.settingsSchema, {
214
- cacheKey: this.settingsSchema.$id || `${this.type}:settings`,
215
- throwOnError: true
216
148
  });
217
- cachedSettingsMap.set(this, settings);
218
- RED.log.info("Settings are valid");
219
149
  }
220
- static #buildSettings(NC) {
221
- if (!NC.settingsSchema) return;
222
- const settings = {};
223
- const prefix = NC.type.replace(/-./g, (x) => x[1].toUpperCase());
224
- for (const [key, prop] of Object.entries(NC.settingsSchema.properties)) {
225
- const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
226
- settings[settingKey] = {
227
- value: prop.default,
228
- exportable: prop.exportable ?? false
229
- };
150
+ /**
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.
157
+ *
158
+ * @param schema - JSON Schema to validate against
159
+ * @param mutate - `true` (default) uses the coercing instance; `false` uses the
160
+ * non-mutating (pure-predicate) instance. Each AJV keeps its own registry.
161
+ */
162
+ createValidator(schema, mutate = true) {
163
+ const ajv = mutate ? this.ajv : this.pureAjv;
164
+ if (schema.$id) {
165
+ const cached = ajv.getSchema(schema.$id);
166
+ if (cached) return cached;
230
167
  }
231
- return settings;
168
+ return ajv.compile(schema);
232
169
  }
233
170
  /**
234
- * Registers this node class with Node-RED. Handles instance creation,
235
- * event handler wiring, settings validation, and the user's registered() hook.
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.
236
181
  */
237
- static async register(RED) {
238
- const NodeClass = this;
239
- if (NodeClass.color && !/^#[0-9A-Fa-f]{6}$/.test(NodeClass.color)) {
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) {
240
187
  throw new NrgError(
241
- `Invalid color for ${NodeClass.type}: ${NodeClass.color} color must be in hex format`
242
- );
243
- }
244
- RED.nodes.registerType(
245
- NodeClass.type,
246
- function(config) {
247
- RED.nodes.createNode(this, config);
248
- const node = new NodeClass(RED, this, config, this.credentials);
249
- Object.defineProperty(this, "_node", {
250
- value: node,
251
- writable: false,
252
- configurable: false,
253
- enumerable: false
254
- });
255
- const createdPromise = Promise.resolve(node.created?.()).catch(
256
- (error) => {
257
- const message = error instanceof Error ? error.message : String(error);
258
- this.error("Error during created hook: " + message);
259
- throw error;
260
- }
261
- );
262
- createdPromise.catch(() => {
263
- this.status({ fill: "red", shape: "ring", text: "created() failed" });
264
- });
265
- node[WIRE_HANDLERS](this, createdPromise);
266
- },
267
- {
268
- credentials: NodeClass.credentialsSchema ? getCredentialsFromSchema(NodeClass.credentialsSchema) : {},
269
- settings: _Node.#buildSettings(this)
270
- }
271
- );
272
- NodeClass.validateSettings(RED);
273
- try {
274
- await Promise.resolve(NodeClass.registered?.(RED));
275
- } catch (error) {
276
- const message = error instanceof Error ? error.message : String(error);
277
- RED.log.error(
278
- `Error during registered hook for ${NodeClass.type}: ${message}`
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>".`
279
189
  );
280
190
  }
191
+ this.registeredSchemaIds.set(id, { schema, owner });
281
192
  }
282
- RED;
283
- node;
284
- context;
285
- config;
286
- timers = /* @__PURE__ */ new Set();
287
- intervals = /* @__PURE__ */ new Set();
288
- constructor(RED, node, config, credentials) {
289
- this.RED = RED;
290
- this.node = node;
291
- const constructor = this.constructor;
292
- if (constructor.configSchema) {
293
- this.log("Validating configs");
294
- const configResult = this.RED.validator.validate(
295
- config,
296
- constructor.configSchema,
297
- {
298
- cacheKey: constructor.configSchema.$id || `${constructor.type}:configs-schema`,
299
- throwOnError: false
300
- }
301
- );
302
- if (!configResult.valid && configResult.errors?.length) {
303
- this.warn(
304
- `Config validation errors: ${configResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
305
- );
193
+ /**
194
+ * Validate data against a schema and return a structured result
195
+ */
196
+ validate(data, schema, options) {
197
+ const validator = this.createValidator(schema, options?.mutate ?? true);
198
+ const valid = validator(data);
199
+ if (!valid) {
200
+ const errorMessage = this.formatErrors(validator.errors);
201
+ if (options?.throwOnError) {
202
+ throw new ValidationError(errorMessage, validator.errors || []);
306
203
  }
204
+ return {
205
+ valid: false,
206
+ errors: validator.errors || void 0,
207
+ errorMessage
208
+ };
307
209
  }
308
- this.config = setupConfigProxy({
309
- RED,
310
- node,
311
- config,
312
- schema: constructor.configSchema
313
- });
314
- if (constructor.credentialsSchema && credentials) {
315
- this.log("Validating credentials");
316
- const credResult = this.RED.validator.validate(
317
- credentials,
318
- constructor.credentialsSchema,
319
- {
320
- cacheKey: constructor.credentialsSchema.$id || `${constructor.type}:credentials-schema`,
321
- throwOnError: false
322
- }
323
- );
324
- if (!credResult.valid && credResult.errors?.length) {
325
- this.warn(
326
- `Credentials validation errors: ${credResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
327
- );
328
- }
329
- }
330
- }
331
- [WIRE_HANDLERS](nodeRedNode, createdPromise) {
332
- nodeRedNode.on(
333
- "close",
334
- async (removed, done) => {
335
- try {
336
- this.log("Calling closed");
337
- await this.#closed(removed);
338
- this.log("Node was closed");
339
- done();
340
- } catch (error) {
341
- if (error instanceof Error) {
342
- this.error("Error while closing node: " + error.message);
343
- done(error);
344
- } else {
345
- this.error("Unknown error occurred while closing node");
346
- done(new Error("Unknown error occurred while closing node"));
347
- }
348
- }
349
- }
350
- );
210
+ return {
211
+ valid: true,
212
+ data
213
+ };
351
214
  }
352
- async #closed(removed) {
353
- try {
354
- await Promise.resolve(this.closed?.(removed));
355
- } finally {
356
- this.log("clearing timers and intervals");
357
- this.timers.forEach((t) => clearTimeout(t));
358
- this.intervals.forEach((i) => clearInterval(i));
359
- this.timers.clear();
360
- this.intervals.clear();
361
- this.log("timers and intervals cleared");
215
+ /**
216
+ * Format errors into a human-readable string
217
+ */
218
+ formatErrors(errors, options) {
219
+ if (!errors || errors.length === 0) {
220
+ return "No errors";
362
221
  }
222
+ return this.ajv.errorsText(errors, {
223
+ separator: "; ",
224
+ dataVar: "data",
225
+ ...options
226
+ });
363
227
  }
364
- i18n(key, substitutions) {
365
- const nodeType = this.constructor.type;
366
- return this.RED._(`${nodeType}.${key}`, substitutions);
228
+ /**
229
+ * Get detailed error information
230
+ */
231
+ getDetailedErrors(errors) {
232
+ if (!errors || errors.length === 0) return [];
233
+ return errors.map((error) => ({
234
+ field: error.instancePath || "/",
235
+ message: error.message || "Validation failed",
236
+ keyword: error.keyword,
237
+ params: error.params,
238
+ schemaPath: error.schemaPath
239
+ }));
367
240
  }
368
- setTimeout(fn, ms) {
369
- const timer = setTimeout(() => {
370
- this.timers.delete(timer);
371
- fn();
372
- }, ms);
373
- this.timers.add(timer);
374
- return timer;
241
+ /**
242
+ * Add a schema to both validator instances for cross-schema `$ref` reference.
243
+ */
244
+ addSchema(schema, key) {
245
+ this.ajv.addSchema(schema, key);
246
+ this.pureAjv.addSchema(schema, key);
247
+ return this;
375
248
  }
376
- setInterval(fn, ms) {
377
- const interval = setInterval(fn, ms);
378
- this.intervals.add(interval);
379
- return interval;
249
+ /**
250
+ * Remove a schema from both validator instances.
251
+ */
252
+ removeSchema(key) {
253
+ this.ajv.removeSchema(key);
254
+ this.pureAjv.removeSchema(key);
255
+ return this;
380
256
  }
381
- clearTimeout(timer) {
382
- clearTimeout(timer);
383
- this.timers.delete(timer);
257
+ };
258
+ var ValidationError = class _ValidationError extends Error {
259
+ constructor(message, errors) {
260
+ super(message);
261
+ this.errors = errors;
262
+ this.name = "ValidationError";
263
+ Object.setPrototypeOf(this, _ValidationError.prototype);
384
264
  }
385
- clearInterval(interval) {
386
- clearInterval(interval);
387
- this.intervals.delete(interval);
265
+ };
266
+
267
+ // src/sdk/lib/server/validation.ts
268
+ function initValidator(RED) {
269
+ if (RED.validator) return;
270
+ const nrg = {
271
+ validator: new Validator({
272
+ customKeywords: [
273
+ {
274
+ keyword: "x-nrg-skip-validation",
275
+ schemaType: "boolean",
276
+ valid: true
277
+ },
278
+ {
279
+ keyword: "x-nrg-node-type",
280
+ type: "string",
281
+ validate: (schemaValue, dataValue) => {
282
+ if (!dataValue) return true;
283
+ const node = RED.nodes.getNode(dataValue);
284
+ return node?.type === schemaValue;
285
+ }
286
+ }
287
+ ],
288
+ customFormats: {
289
+ "node-id": /^[a-zA-Z0-9-_]+$/
290
+ }
291
+ })
292
+ };
293
+ Object.defineProperty(RED, "_nrg", {
294
+ value: nrg,
295
+ writable: false,
296
+ enumerable: false,
297
+ configurable: false
298
+ });
299
+ Object.defineProperty(RED, "validator", {
300
+ get: () => nrg.validator,
301
+ enumerable: false,
302
+ configurable: false
303
+ });
304
+ }
305
+
306
+ // 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";
311
+ function serveFile(filePath) {
312
+ return (_req, res, next) => {
313
+ if (!import_fs.default.existsSync(filePath)) return next();
314
+ res.setHeader("Content-Type", "application/javascript");
315
+ import_fs.default.createReadStream(filePath).pipe(res);
316
+ };
317
+ }
318
+ 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"));
322
+ 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
+ router.get(
324
+ `/nrg/assets/${CLIENT_ASSET}`,
325
+ serveFile(import_path.default.join(resourcesDir, CLIENT_ASSET))
326
+ );
327
+ router.get("/nrg/assets/vue.esm-browser.prod.js", serveFile(vueFile));
328
+ }
329
+
330
+ // src/sdk/lib/server/api/routes.ts
331
+ var _initialized = false;
332
+ function initRoutes(RED) {
333
+ if (_initialized) return;
334
+ _initialized = true;
335
+ initAssetsRoutes(RED.httpAdmin);
336
+ }
337
+
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: ""
388
+ }
388
389
  }
389
- on(event, callback) {
390
- this.node.on(event, callback);
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
+ );
391
426
  }
392
- log(msg) {
393
- this.node.log(msg);
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
+ );
394
432
  }
395
- warn(message) {
396
- this.node.warn(message);
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);
478
+ }
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;
397
502
  }
398
- error(message, msg) {
399
- this.node.error(message, msg);
503
+ if (schema.properties) {
504
+ for (const prop of Object.values(schema.properties)) {
505
+ markNonValidatable(prop);
506
+ }
400
507
  }
401
- get id() {
402
- return this.node.id;
508
+ if (schema.patternProperties) {
509
+ for (const prop of Object.values(schema.patternProperties)) {
510
+ markNonValidatable(prop);
511
+ }
403
512
  }
404
- get name() {
405
- return this.node.name;
513
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
514
+ markNonValidatable(schema.additionalProperties);
406
515
  }
407
- get z() {
408
- return this.node.z;
516
+ if (schema.items) {
517
+ markNonValidatable(schema.items);
409
518
  }
410
- get credentials() {
411
- return this.node.credentials;
519
+ if (schema.anyOf) {
520
+ schema.anyOf.forEach((s) => markNonValidatable(s));
412
521
  }
413
- get settings() {
414
- const constructor = this.constructor;
415
- return cachedSettingsMap.get(constructor) ?? {};
522
+ if (schema.allOf) {
523
+ schema.allOf.forEach((s) => markNonValidatable(s));
416
524
  }
417
- };
525
+ if (schema.oneOf) {
526
+ schema.oneOf.forEach((s) => markNonValidatable(s));
527
+ }
528
+ return schema;
529
+ }
530
+ function defineSchema(properties, options) {
531
+ const schema = SchemaType.Object(properties, options);
532
+ return markNonValidatable(schema);
533
+ }
418
534
 
419
- // src/core/server/nodes/io-node.ts
420
- var import_typebox = require("@sinclair/typebox");
535
+ // src/sdk/lib/shared/schemas/index.ts
536
+ var import_typebox3 = require("@sinclair/typebox");
421
537
 
422
- // src/core/server/nodes/context.ts
423
- var updateLocks = /* @__PURE__ */ new WeakMap();
424
- function setupContext(context, store) {
425
- const get = (key) => new Promise(
426
- (resolve, reject) => context.get(
427
- key,
428
- store,
429
- (error, value) => error ? reject(error) : resolve(value)
430
- )
431
- );
432
- const set = (key, value) => new Promise(
433
- (resolve, reject) => context.set(
434
- key,
435
- value,
436
- store,
437
- (error) => error ? reject(error) : resolve()
438
- )
439
- );
440
- const keys = () => new Promise(
441
- (resolve, reject) => context.keys(store, (error, k) => error ? reject(error) : resolve(k))
442
- );
443
- const nativeUpdate = context.update;
444
- const nativeIncrement = context.increment;
445
- const update = (key, fn) => {
446
- if (nativeUpdate) {
447
- return new Promise(
448
- (resolve, reject) => nativeUpdate(
449
- key,
450
- fn,
451
- store,
452
- (error, value) => error ? reject(error) : resolve(value)
453
- )
454
- );
455
- }
456
- let chains = updateLocks.get(context);
457
- if (!chains) updateLocks.set(context, chains = /* @__PURE__ */ new Map());
458
- const lockKey = JSON.stringify([store ?? null, key]);
459
- const task = async () => {
460
- const next = await fn(await get(key));
461
- await set(key, next);
462
- return next;
463
- };
464
- const previous = chains.get(lockKey) ?? Promise.resolve();
465
- const run = previous.then(task, task);
466
- const settled = run.then(
467
- () => void 0,
468
- () => void 0
469
- );
470
- chains.set(lockKey, settled);
471
- void settled.then(() => {
472
- if (chains.get(lockKey) === settled) chains.delete(lockKey);
473
- });
474
- return run;
475
- };
476
- const increment = (key, by = 1) => {
477
- if (nativeIncrement) {
478
- return new Promise(
479
- (resolve, reject) => nativeIncrement(
480
- key,
481
- by,
482
- store,
483
- (error, value) => error ? reject(error) : resolve(value)
484
- )
485
- );
486
- }
487
- return update(
488
- key,
489
- (current) => typeof current === "number" ? current + by : by
538
+ // src/sdk/lib/server/registration.ts
539
+ async function registerType(RED, NodeClass) {
540
+ RED.log.debug(`Registering Type: ${NodeClass.type}`);
541
+ if (!NodeClass[NRG_NODE]) {
542
+ throw new NrgError(
543
+ `${NodeClass.name} must extend IONode or ConfigNode classes`
490
544
  );
491
- };
492
- return { get, set, keys, update, increment };
493
- }
494
-
495
- // src/core/server/nodes/io-node.ts
496
- var import_node_async_hooks = require("node:async_hooks");
497
- function isSchemaLike(obj) {
498
- return obj != null && typeof obj === "object" && import_typebox.Kind in obj;
499
- }
500
- var RETURN_PROPERTY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
501
- var INPUT_KEY = "input";
502
- var IONode = class _IONode extends Node {
503
- static align;
504
- static color;
505
- static inputSchema;
506
- // outputsSchema accepts any schema shape: the raw sent value (per port) is
507
- // validated, and results are frequently non-objects.
508
- static outputsSchema;
509
- static validateInput = false;
510
- // A single boolean applies to every output port; a boolean[] sets the
511
- // per-port default by base-output index (missing entries default to false).
512
- static validateOutput = false;
513
- static get inputs() {
514
- return this.inputSchema ? 1 : 0;
515
545
  }
516
- static get outputs() {
517
- const s = this.outputsSchema;
518
- if (!s) return 0;
519
- if (Array.isArray(s)) return s.length;
520
- if (isSchemaLike(s)) return 1;
521
- const keys = Object.keys(s);
522
- for (const key of keys) {
523
- if (/^\d+$/.test(key)) {
524
- throw new NrgError(
525
- `outputsSchema record key "${key}" in ${this.type} looks numeric. Use descriptive string names (e.g. "success", "failure") to avoid JavaScript object key ordering issues.`
526
- );
527
- }
528
- if (key === "error" || key === "complete" || key === "status") {
529
- throw new NrgError(
530
- `outputsSchema record key "${key}" in ${this.type} is reserved for built-in ports. Use a different name (e.g. "failed" instead of "error").`
531
- );
532
- }
533
- }
534
- return keys.length;
546
+ if (!NodeClass.type) {
547
+ throw new NrgError("type must be provided when registering the node");
535
548
  }
536
- /**
537
- * The names of the base output ports when `outputsSchema` is a record of
538
- * named ports (`{ success, failure }`), in declaration order — otherwise
539
- * `undefined` (a single schema or a positional array). Resolved here, where
540
- * TypeBox's `Kind` symbol is intact, so the editor reads the names directly
541
- * instead of guessing them from a serialized (symbol-stripped) schema.
542
- */
543
- static get outputPortNames() {
544
- const s = this.outputsSchema;
545
- if (!s || Array.isArray(s) || isSchemaLike(s)) return void 0;
546
- return Object.keys(s);
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);
547
572
  }
548
- /**
549
- * Per-`input()`-invocation context, scoped via AsyncLocalStorage. One store
550
- * per class, shared by every instance: each `.run()` isolates one input()
551
- * call (and everything it `await`s, including detached `.then`/timer
552
- * continuations it schedules) into its own session, so concurrent inputs on a
553
- * node never clobber each other's context and a deferred send keeps the
554
- * context of the input that scheduled it. Real Node-RED never awaits an async
555
- * input handler before delivering the next message, so two inputs can be in
556
- * flight at once. `getStore()` is `undefined` only for a send made entirely
557
- * outside any `input()` (e.g. a timer set up in `created()`): it carries no
558
- * inherited context and delivers via `node.send`. Server-only — this is part
559
- * of the node runtime and is never imported in a browser.
560
- */
561
- static #invocation = new import_node_async_hooks.AsyncLocalStorage();
562
- context;
563
- constructor(RED, node, config, credentials) {
564
- super(RED, node, config, credentials);
565
- const context = node.context();
566
- const resolve = (scope, store) => {
567
- const target = scope === "global" ? context.global : scope === "flow" ? context.flow : context;
568
- return setupContext(target, store);
569
- };
570
- this.context = Object.assign(resolve, {
571
- node: setupContext(context),
572
- flow: setupContext(context.flow),
573
- global: setupContext(context.global)
574
- });
575
- const outputReturnProperties = this.config.outputReturnProperties;
576
- if (outputReturnProperties) {
577
- for (const [port, key] of Object.entries(outputReturnProperties)) {
578
- if (typeof key === "string" && key.trim() && !RETURN_PROPERTY_PATTERN.test(key.trim())) {
579
- throw new NrgError(
580
- `Invalid return property "${key}" for output port ${port} in ${this.constructor.type} \u2014 it must be a valid JavaScript identifier (letters, digits, _, $; not starting with a digit)`
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}" }).`
581
585
  );
582
586
  }
587
+ continue;
583
588
  }
589
+ RED.validator.reserveSchemaId(schema, owner);
584
590
  }
585
591
  }
586
- [WIRE_HANDLERS](nodeRedNode, createdPromise) {
587
- super[WIRE_HANDLERS](nodeRedNode, createdPromise);
588
- const NC = this.constructor;
589
- nodeRedNode.on(
590
- "input",
591
- async (msg, send, done) => {
592
- try {
593
- await createdPromise;
594
- } catch (initError) {
595
- done(
596
- initError instanceof Error ? initError : new Error(String(initError))
597
- );
598
- return;
599
- }
600
- const store = { inputMsg: msg, send };
601
- try {
602
- nodeRedNode.log("Calling input");
603
- const result = await this.#input(msg, store);
604
- if (this.config.completePort) {
605
- this.#sendToPort("complete", {
606
- ...msg,
607
- ...result !== void 0 ? { output: result } : {},
608
- complete: {
609
- source: this.#nodeSource()
610
- },
611
- [INPUT_KEY]: msg
612
- });
613
- }
614
- done();
615
- nodeRedNode.log("Input processed");
616
- } catch (error) {
617
- const errorMsg = error instanceof Error ? error.message : "Unknown error during input handling";
618
- if (this.config.errorPort && !store.errorEmitted) {
619
- const errorData = error && typeof error === "object" ? { ...error } : {};
620
- this.#sendToPort("error", {
621
- ...msg,
622
- error: {
623
- ...errorData,
624
- name: error?.name ?? "Error",
625
- message: errorMsg,
626
- source: this.#nodeSource()
627
- },
628
- [INPUT_KEY]: msg
629
- });
630
- }
631
- if (error instanceof Error) {
632
- nodeRedNode.error(
633
- "Error while processing input: " + error.message,
634
- msg
635
- );
636
- done(error);
637
- } else {
638
- nodeRedNode.error(
639
- "Unknown error occurred during input handling",
640
- msg
641
- );
642
- done(new Error(errorMsg));
643
- }
592
+ }
593
+ function registerTypes(nodes) {
594
+ const fn = Object.assign(
595
+ async function(RED) {
596
+ initValidator(RED);
597
+ initRoutes(RED);
598
+ try {
599
+ checkSchemaIds(RED, nodes);
600
+ RED.log.info("Registering node types in series");
601
+ for (const NodeClass of nodes) {
602
+ await registerType(RED, NodeClass);
644
603
  }
604
+ RED.log.info("All node types registered in series");
605
+ } catch (error) {
606
+ RED.log.error("Error registering node types:", error);
607
+ throw error;
645
608
  }
646
- );
647
- }
648
- input(msg) {
649
- return void 0;
609
+ },
610
+ { nodes }
611
+ );
612
+ return fn;
613
+ }
614
+
615
+ // src/sdk/lib/server/typed-input.ts
616
+ var TypedInput2 = class {
617
+ constructor(RED, node, input) {
618
+ this.RED = RED;
619
+ this.node = node;
620
+ this.input = input;
650
621
  }
651
- async #input(msg, store) {
652
- const NodeClass = this.constructor;
653
- const shouldValidateInput = this.config.validateInput ?? NodeClass.validateInput;
654
- if (shouldValidateInput && NodeClass.inputSchema) {
655
- this.log("Validating input");
656
- this.RED.validator.validate(msg, NodeClass.inputSchema, {
657
- cacheKey: NodeClass.inputSchema.$id || `${NodeClass.type}:input-schema`,
658
- throwOnError: true
659
- });
660
- this.log("Input is valid");
622
+ resolvers = {
623
+ // evaluateNodeProperty returns the node ID string for "node" type —
624
+ // resolve it to the actual node instance via RED.nodes.getNode,
625
+ // then surface the NRG wrapper if available.
626
+ node: (raw) => {
627
+ if (typeof raw === "string") {
628
+ const node = this.RED.nodes.getNode(raw);
629
+ return node?._node ?? node ?? raw;
630
+ }
631
+ return raw?._node ?? raw;
661
632
  }
662
- return await _IONode.#invocation.run(
663
- store,
664
- () => Promise.resolve(this.input(msg))
665
- );
633
+ };
634
+ get type() {
635
+ return this.input.type;
666
636
  }
667
- send(msg) {
668
- const multi = this.baseOutputs > 1 && Array.isArray(msg);
669
- const values = multi ? msg.slice(0, this.baseOutputs) : [msg];
670
- const out = values.map((m, port) => {
671
- if (m == null) return m;
672
- this.#validatePort(m, port);
673
- return this.#wrapOutgoing(m, this.#resolveContextMode(port), port);
674
- });
675
- this.#deliver(out);
637
+ get value() {
638
+ return this.input.value;
676
639
  }
677
- #deliver(out) {
678
- const send = _IONode.#invocation.getStore()?.send;
679
- if (send) {
680
- send(out);
681
- } else {
682
- this.node.send(out);
683
- }
640
+ resolve(msg) {
641
+ return new Promise((resolve, reject) => {
642
+ this.RED.util.evaluateNodeProperty(
643
+ this.input.value,
644
+ this.input.type,
645
+ this.node,
646
+ msg,
647
+ (err, raw) => {
648
+ if (err) return reject(err);
649
+ const post = this.resolvers[this.input.type];
650
+ resolve(post ? post(raw) : raw);
651
+ }
652
+ );
653
+ });
684
654
  }
685
- /**
686
- * Per-port output validation. A port validates when its flow-author flag
687
- * (`config.validateOutputs[port]`) — or the node's static `validateOutput`
688
- * fallback — is on and a schema exists for that port. The static fallback is
689
- * a single boolean (all ports) or a boolean[] (per-port, by base index).
690
- */
691
- #validatePort(value, port) {
692
- const NodeClass = this.constructor;
693
- const staticFlag = NodeClass.validateOutput;
694
- const staticForPort = Array.isArray(staticFlag) ? staticFlag[port] ?? false : staticFlag;
695
- const configured = this.config.validateOutputs?.[port];
696
- if (!(configured ?? staticForPort)) return;
697
- const schema = this.#outputSchemaForPort(port);
698
- if (!schema) return;
699
- this.log("Validating output");
700
- this.RED.validator.validate(value, schema, {
701
- cacheKey: schema.$id || `${NodeClass.type}:output-schema:${port}`,
702
- throwOnError: true
655
+ };
656
+
657
+ // src/sdk/lib/server/nodes/proxy.ts
658
+ function setupConfigProxy(opts) {
659
+ const { RED, node, config, schema } = opts;
660
+ const SKIP_PROPS = /* @__PURE__ */ new Set(["id", "_id", "_users"]);
661
+ const proxyCache = /* @__PURE__ */ new WeakMap();
662
+ const typedInputCache = /* @__PURE__ */ new WeakMap();
663
+ const createProxy = (obj, subSchema) => {
664
+ const cached = proxyCache.get(obj);
665
+ if (cached) return cached;
666
+ const schemaForProp = (target, prop) => Array.isArray(target) ? subSchema?.items : subSchema?.properties?.[prop];
667
+ const proxy = new Proxy(obj, {
668
+ get(target, prop) {
669
+ if (typeof prop === "symbol") return target[prop];
670
+ if (SKIP_PROPS.has(prop)) return target[prop];
671
+ const value = target[prop];
672
+ const fieldSchema = schemaForProp(target, prop);
673
+ if (fieldSchema?.["x-nrg-node-type"]) {
674
+ if (typeof value !== "string" || value.length === 0) return void 0;
675
+ const resolved = RED.nodes.getNode(value)?._node;
676
+ const ctor = resolved?.constructor;
677
+ if (ctor?.[NRG_NODE] && !resolved[NRG_CONFIG_NODE]) {
678
+ throw new NrgError(
679
+ `Config field "${String(prop)}" references "${value}", which is not an nrg config node.`
680
+ );
681
+ }
682
+ return resolved ?? value;
683
+ }
684
+ if (fieldSchema?.["x-nrg-typed-input"] && value && typeof value === "object" && "type" in value && "value" in value) {
685
+ let ref = typedInputCache.get(value);
686
+ if (!ref) {
687
+ ref = new TypedInput2(RED, node, value);
688
+ typedInputCache.set(value, ref);
689
+ }
690
+ return ref;
691
+ }
692
+ if (value && typeof value === "object") {
693
+ return createProxy(value, fieldSchema);
694
+ }
695
+ return value;
696
+ },
697
+ set(_target, prop) {
698
+ throw new NrgError(
699
+ `Cannot set property '${String(prop)}' on read-only node config`
700
+ );
701
+ },
702
+ deleteProperty(_target, prop) {
703
+ throw new NrgError(
704
+ `Cannot delete property '${String(prop)}' on read-only node config`
705
+ );
706
+ }
703
707
  });
704
- this.log("Output is valid");
705
- }
706
- /** Resolves the output schema for a base-output port: array → `[port]`,
707
- * record → the port-th value, single schema → itself. */
708
- #outputSchemaForPort(port) {
709
- const raw = this.constructor.outputsSchema;
710
- if (!raw) return void 0;
711
- if (Array.isArray(raw)) return raw[port];
712
- if (isSchemaLike(raw)) return raw;
713
- return Object.values(raw)[port];
714
- }
715
- /**
716
- * The return key for an output port — `"output"` unless a custom one is set
717
- * via `outputReturnProperties[port]` (author default and/or flow-author
718
- * override, only possible when the node declares `outputReturnProperties`).
719
- * `this.send(x)` always means "x is the value at this port's return key",
720
- * never "x is the whole outgoing message".
721
- */
722
- #returnPropertyKey(port) {
723
- const configured = this.config.outputReturnProperties?.[port];
724
- if (typeof configured === "string" && configured.trim()) {
725
- return configured.trim();
708
+ proxyCache.set(obj, proxy);
709
+ return proxy;
710
+ };
711
+ return createProxy(config, schema);
712
+ }
713
+
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
+ );
726
725
  }
727
- return "output";
728
- }
729
- /**
730
- * Resolves the context mode for a base-output port from the flow author's
731
- * per-port config (`config.outputContextModes[port]`, written by the editor
732
- * when the node declares `outputContextModes`), falling back to `"carry"`.
733
- */
734
- #resolveContextMode(port) {
735
- return this.config.outputContextModes?.[port] ?? "carry";
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
+ };
736
732
  }
733
+ return result;
734
+ }
735
+
736
+ // src/sdk/lib/server/nodes/node.ts
737
+ var cachedSettingsMap = /* @__PURE__ */ new WeakMap();
738
+ var Node = class _Node {
737
739
  /**
738
- * Merges a sent value into the incoming message at the returnProperty key so
739
- * upstream message properties propagate. A fresh base is built per call so
740
- * multi-port sends never share an object.
740
+ * Runtime NRG-node brand inherited by every subclass (IONode/ConfigNode and
741
+ * factory-built classes) and checked by `defineModule` at runtime. See
742
+ * symbols.ts for why it's a `Symbol.for()` runtime key, not a compile-time brand.
741
743
  */
742
- #wrapOutgoing(value, mode, port) {
743
- const key = this.#returnPropertyKey(port);
744
- const input = _IONode.#invocation.getStore()?.inputMsg ?? {};
745
- if (mode === "reset") {
746
- return { [key]: value };
744
+ static [NRG_NODE] = true;
745
+ static type;
746
+ static category;
747
+ static configSchema;
748
+ static credentialsSchema;
749
+ static settingsSchema;
750
+ static validateSettings(RED) {
751
+ if (!this.settingsSchema) return;
752
+ RED.log.info("Validating settings");
753
+ const prefix = this.type.replace(/-./g, (x) => x[1].toUpperCase());
754
+ const properties = this.settingsSchema.properties;
755
+ const settings = {};
756
+ for (const key of Object.keys(properties)) {
757
+ const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
758
+ const value = RED.settings[settingKey];
759
+ if (value !== void 0) {
760
+ settings[key] = value;
761
+ }
747
762
  }
748
- if (mode === "trace") {
749
- return { ...input, [key]: value, [INPUT_KEY]: input };
763
+ 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
+ }
769
+ }
750
770
  }
751
- return { ...input, [key]: value };
752
- }
753
- // --- Built-in port management ---
754
- get baseOutputs() {
755
- return this.constructor.outputs ?? 0;
771
+ RED.validator.validate(settings, this.settingsSchema, {
772
+ throwOnError: true
773
+ });
774
+ cachedSettingsMap.set(this, settings);
775
+ RED.log.info("Settings are valid");
756
776
  }
757
- get totalOutputs() {
758
- let count = this.baseOutputs;
759
- if (this.config.errorPort) count++;
760
- if (this.config.completePort) count++;
761
- if (this.config.statusPort) count++;
762
- return count;
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;
763
789
  }
764
790
  /**
765
- * Send a message to a specific output port by index or name.
766
- * Custom named ports are resolved from `outputsSchema` when it is a record.
767
- * Numeric indices refer to the base output ports (0-based).
768
- *
769
- * Built-in ports (`"error"`, `"complete"`, `"status"`) are managed by the
770
- * framework and cannot be sent to directly. Use `this.status()` for status,
771
- * throw an error or call `this.error()` for the error port, and the complete
772
- * port is sent automatically on successful input processing.
791
+ * Registers this node class with Node-RED. Handles instance creation,
792
+ * event handler wiring, settings validation, and the user's registered() hook.
773
793
  */
774
- sendToPort(port, msg) {
775
- if (port === "error" || port === "complete" || port === "status") {
794
+ static async register(RED) {
795
+ const NodeClass = this;
796
+ if (NodeClass.color && !/^#[0-9A-Fa-f]{6}$/.test(NodeClass.color)) {
776
797
  throw new NrgError(
777
- `sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
798
+ `Invalid color "${NodeClass.color}" for ${NodeClass.type}: must be a 6-digit hex color like "#a6bbcf" (shorthand "#abc" is not accepted).`
778
799
  );
779
800
  }
780
- const portIndex = typeof port === "number" ? port : this.#getNamedPortIndex(port);
781
- const mode = this.#resolveContextMode(portIndex ?? 0);
782
- this.#sendToPort(
783
- port,
784
- msg == null ? msg : this.#wrapOutgoing(msg, mode, portIndex ?? 0)
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
+ }
785
828
  );
786
- }
787
- #sendToPort(port, msg) {
788
- let portIndex;
789
- if (typeof port === "number") {
790
- portIndex = port;
791
- } else if (port === "error" || port === "complete" || port === "status") {
792
- portIndex = this.#getBuiltinPortIndex(port);
793
- if (portIndex === null) return;
794
- } else {
795
- portIndex = this.#getNamedPortIndex(port);
796
- if (portIndex === null) return;
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
+ );
797
837
  }
798
- const out = new Array(this.totalOutputs);
799
- out[portIndex] = msg;
800
- this.node.send(out);
801
838
  }
802
- #getNamedPortIndex(name) {
803
- const schema = this.constructor.outputsSchema;
804
- if (!schema || Array.isArray(schema) || isSchemaLike(schema)) return null;
805
- const idx = Object.keys(schema).indexOf(name);
806
- return idx === -1 ? null : idx;
807
- }
808
- #getBuiltinPortIndex(name) {
809
- if (name === "error") {
810
- return this.config.errorPort ? this.baseOutputs : null;
811
- }
812
- let idx = this.baseOutputs;
813
- if (this.config.errorPort) idx++;
814
- if (name === "complete") {
815
- return this.config.completePort ? idx : null;
839
+ RED;
840
+ node;
841
+ context;
842
+ config;
843
+ timers = /* @__PURE__ */ new Set();
844
+ intervals = /* @__PURE__ */ new Set();
845
+ constructor(RED, node, config, credentials) {
846
+ this.RED = RED;
847
+ this.node = node;
848
+ const constructor = this.constructor;
849
+ if (constructor.configSchema) {
850
+ this.log("Validating configs");
851
+ const configResult = this.RED.validator.validate(
852
+ config,
853
+ constructor.configSchema,
854
+ {
855
+ // Coercing (default): the proxy at (this).config wraps this same object,
856
+ // so `this.config.<field>` must read as its coerced/typed value.
857
+ throwOnError: false
858
+ }
859
+ );
860
+ if (!configResult.valid && configResult.errors?.length) {
861
+ this.warn(
862
+ `Config validation errors: ${configResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
863
+ );
864
+ }
865
+ }
866
+ this.config = setupConfigProxy({
867
+ RED,
868
+ node,
869
+ config,
870
+ schema: constructor.configSchema
871
+ });
872
+ if (constructor.credentialsSchema && credentials) {
873
+ this.log("Validating credentials");
874
+ const credResult = this.RED.validator.validate(
875
+ credentials,
876
+ constructor.credentialsSchema,
877
+ {
878
+ throwOnError: false
879
+ }
880
+ );
881
+ if (!credResult.valid && credResult.errors?.length) {
882
+ this.warn(
883
+ `Credentials validation errors: ${credResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
884
+ );
885
+ }
816
886
  }
817
- if (this.config.completePort) idx++;
818
- return this.config.statusPort ? idx : null;
819
- }
820
- #nodeSource() {
821
- return {
822
- id: this.id,
823
- type: this.constructor.type,
824
- name: this.name
825
- };
826
887
  }
827
- status(status) {
828
- this.node.status(status);
829
- this.#sendToPort("status", {
830
- status,
831
- source: this.#nodeSource()
832
- });
888
+ [NRG_WIRE_HANDLERS](nodeRedNode, createdPromise) {
889
+ nodeRedNode.on(
890
+ "close",
891
+ async (removed, done) => {
892
+ try {
893
+ this.log("Calling closed");
894
+ await this.#closed(removed);
895
+ this.log("Node was closed");
896
+ done();
897
+ } catch (error) {
898
+ if (error instanceof Error) {
899
+ this.error("Error while closing node: " + error.message);
900
+ done(error);
901
+ } else {
902
+ this.error("Unknown error occurred while closing node");
903
+ done(new Error("Unknown error occurred while closing node"));
904
+ }
905
+ }
906
+ }
907
+ );
833
908
  }
834
- error(message, msg) {
835
- super.error(message, msg);
836
- if (msg && this.config.errorPort) {
837
- this.#sendToPort("error", {
838
- ...msg,
839
- error: {
840
- // `name` keeps the error-port payload Catch-node compatible and
841
- // consistent with the auto-emit from a thrown error.
842
- name: "Error",
843
- message,
844
- source: this.#nodeSource()
845
- },
846
- [INPUT_KEY]: msg
847
- });
848
- const store = _IONode.#invocation.getStore();
849
- if (store) store.errorEmitted = true;
909
+ async #closed(removed) {
910
+ try {
911
+ await Promise.resolve(this.closed?.(removed));
912
+ } 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");
850
919
  }
851
920
  }
852
- updateWires(wires) {
853
- this.node.updateWires(wires);
921
+ i18n(key, substitutions) {
922
+ const nodeType = this.constructor.type;
923
+ return this.RED._(`${nodeType}.${key}`, substitutions);
854
924
  }
855
- receive(msg) {
856
- this.node.receive(msg);
925
+ setTimeout(fn, ms) {
926
+ const timer = setTimeout(() => {
927
+ this.timers.delete(timer);
928
+ fn();
929
+ }, ms);
930
+ this.timers.add(timer);
931
+ return timer;
857
932
  }
858
- get x() {
859
- return this.node.x;
933
+ setInterval(fn, ms) {
934
+ const interval = setInterval(fn, ms);
935
+ this.intervals.add(interval);
936
+ return interval;
860
937
  }
861
- get y() {
862
- return this.node.y;
938
+ clearTimeout(timer) {
939
+ clearTimeout(timer);
940
+ this.timers.delete(timer);
863
941
  }
864
- get g() {
865
- return this.node.g;
942
+ clearInterval(interval) {
943
+ clearInterval(interval);
944
+ this.intervals.delete(interval);
866
945
  }
867
- get wires() {
868
- return this.node.wires;
946
+ on(event, callback) {
947
+ this.node.on(event, callback);
869
948
  }
870
- get credentials() {
871
- return this.node.credentials;
949
+ log(msg) {
950
+ this.node.log(msg);
872
951
  }
873
- };
874
-
875
- // src/core/server/nodes/config-node.ts
876
- var ConfigNode = class extends Node {
877
- static category = "config";
878
- context;
879
- constructor(RED, node, config, credentials) {
880
- super(RED, node, config, credentials);
881
- const context = node.context();
882
- const resolve = (scope, store) => {
883
- const target = scope === "global" ? context.global : context;
884
- return setupContext(target, store);
885
- };
886
- this.context = Object.assign(resolve, {
887
- node: setupContext(context),
888
- global: setupContext(context.global)
889
- });
952
+ warn(message) {
953
+ this.node.warn(message);
890
954
  }
891
- get userIds() {
892
- return this.config._users;
955
+ error(message, msg) {
956
+ this.node.error(message, msg);
893
957
  }
894
- get users() {
895
- return this.userIds.map((id) => this.RED.nodes.getNode(id)?._node).filter((node) => node != null);
958
+ get id() {
959
+ return this.node.id;
896
960
  }
897
- getUser(index) {
898
- const id = this.userIds[index];
899
- if (!id) return void 0;
900
- return this.RED.nodes.getNode(id)?._node;
961
+ get name() {
962
+ return this.node.name;
963
+ }
964
+ get z() {
965
+ return this.node.z;
901
966
  }
902
967
  get credentials() {
903
968
  return this.node.credentials;
904
969
  }
970
+ get settings() {
971
+ const constructor = this.constructor;
972
+ return cachedSettingsMap.get(constructor) ?? {};
973
+ }
905
974
  };
906
975
 
907
- // src/core/server/nodes/factories.ts
908
- function defineIONode(def) {
909
- const NodeClass = class extends IONode {
910
- static type = def.type;
911
- static category = def.category ?? "function";
912
- static color = def.color ?? "#a6bbcf";
913
- static align = def.align;
914
- static configSchema = def.configSchema;
915
- static credentialsSchema = def.credentialsSchema;
916
- static settingsSchema = def.settingsSchema;
917
- static inputSchema = def.inputSchema;
918
- static outputsSchema = def.outputsSchema;
919
- static validateInput = def.validateInput ?? false;
920
- static validateOutput = def.validateOutput ?? false;
921
- static registered(RED) {
922
- return def.registered?.(RED);
923
- }
924
- async input(msg) {
925
- if (def.input) return def.input.call(this, msg);
926
- }
927
- async created() {
928
- if (def.created) return def.created.call(this);
929
- }
930
- async closed(removed) {
931
- if (def.closed) return def.closed.call(this, removed);
976
+ // src/sdk/lib/server/nodes/context.ts
977
+ var updateLocks = /* @__PURE__ */ new WeakMap();
978
+ function setupContext(context, store) {
979
+ const get = (key) => new Promise(
980
+ (resolve, reject) => context.get(
981
+ key,
982
+ store,
983
+ (error, value) => error ? reject(error) : resolve(value)
984
+ )
985
+ );
986
+ const set = (key, value) => new Promise(
987
+ (resolve, reject) => context.set(
988
+ key,
989
+ value,
990
+ store,
991
+ (error) => error ? reject(error) : resolve()
992
+ )
993
+ );
994
+ const keys = () => new Promise(
995
+ (resolve, reject) => context.keys(store, (error, k) => error ? reject(error) : resolve(k))
996
+ );
997
+ const nativeUpdate = context.update;
998
+ const nativeIncrement = context.increment;
999
+ const update = (key, fn) => {
1000
+ if (nativeUpdate) {
1001
+ return new Promise(
1002
+ (resolve, reject) => nativeUpdate(
1003
+ key,
1004
+ fn,
1005
+ store,
1006
+ (error, value) => error ? reject(error) : resolve(value)
1007
+ )
1008
+ );
932
1009
  }
1010
+ let chains = updateLocks.get(context);
1011
+ if (!chains) updateLocks.set(context, chains = /* @__PURE__ */ new Map());
1012
+ const lockKey = JSON.stringify([store ?? null, key]);
1013
+ const task = async () => {
1014
+ const next = await fn(await get(key));
1015
+ await set(key, next);
1016
+ return next;
1017
+ };
1018
+ const previous = chains.get(lockKey) ?? Promise.resolve();
1019
+ const run = previous.then(task, task);
1020
+ const settled = run.then(
1021
+ () => void 0,
1022
+ () => void 0
1023
+ );
1024
+ chains.set(lockKey, settled);
1025
+ void settled.then(() => {
1026
+ if (chains.get(lockKey) === settled) chains.delete(lockKey);
1027
+ });
1028
+ return run;
933
1029
  };
934
- Object.defineProperty(NodeClass, "name", {
935
- value: def.type.replace(
936
- /(^|-)(\w)/g,
937
- (_, __, c) => c.toUpperCase()
938
- ),
939
- configurable: true
940
- });
941
- return NodeClass;
942
- }
943
- function defineConfigNode(def) {
944
- const NodeClass = class extends ConfigNode {
945
- static type = def.type;
946
- static configSchema = def.configSchema;
947
- static credentialsSchema = def.credentialsSchema;
948
- static settingsSchema = def.settingsSchema;
949
- static registered(RED) {
950
- return def.registered?.(RED);
951
- }
952
- async created() {
953
- if (def.created) return def.created.call(this);
954
- }
955
- async closed(removed) {
956
- if (def.closed) return def.closed.call(this, removed);
1030
+ const increment = (key, by = 1) => {
1031
+ if (nativeIncrement) {
1032
+ return new Promise(
1033
+ (resolve, reject) => nativeIncrement(
1034
+ key,
1035
+ by,
1036
+ store,
1037
+ (error, value) => error ? reject(error) : resolve(value)
1038
+ )
1039
+ );
957
1040
  }
1041
+ return update(
1042
+ key,
1043
+ (current) => typeof current === "number" ? current + by : by
1044
+ );
958
1045
  };
959
- Object.defineProperty(NodeClass, "name", {
960
- value: def.type.replace(
961
- /(^|-)(\w)/g,
962
- (_, __, c) => c.toUpperCase()
963
- ),
964
- configurable: true
965
- });
966
- return NodeClass;
1046
+ return { get, set, keys, update, increment };
1047
+ }
1048
+
1049
+ // src/sdk/lib/server/nodes/io-node.ts
1050
+ 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;
967
1053
  }
968
-
969
- // src/core/shared/validator.ts
970
- var import_ajv = __toESM(require("ajv"), 1);
971
- var import_ajv_formats = __toESM(require("ajv-formats"), 1);
972
- var import_ajv_errors = __toESM(require("ajv-errors"), 1);
973
- var Validator = class {
974
- ajv;
975
- constructor(options) {
976
- const { customKeywords, customFormats, ...ajvOptions } = options || {};
977
- this.ajv = new import_ajv.default({
978
- allErrors: true,
979
- code: {
980
- source: false
981
- },
982
- coerceTypes: true,
983
- removeAdditional: false,
984
- strict: false,
985
- strictSchema: false,
986
- useDefaults: true,
987
- validateFormats: true,
988
- // NOTE: typebox handles validation via typescript
989
- // NOTE: if true, types that are not serializable JSON, like Function, would not work
990
- validateSchema: false,
991
- verbose: true,
992
- ...ajvOptions
993
- });
994
- (0, import_ajv_formats.default)(this.ajv);
995
- (0, import_ajv_errors.default)(this.ajv);
996
- this.addCustomKeywords(customKeywords || []);
997
- this.addCustomFormats(customFormats || {});
1054
+ var RETURN_PROPERTY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1055
+ var INPUT_KEY = "input";
1056
+ var IONode = class _IONode extends Node {
1057
+ static align;
1058
+ 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;
1067
+ static get inputs() {
1068
+ return this.inputSchema ? 1 : 0;
998
1069
  }
999
- /**
1000
- * Add custom keywords to the validator
1001
- */
1002
- addCustomKeywords(keywords) {
1003
- if (!keywords) return;
1004
- keywords.forEach((keyword) => {
1005
- this.ajv.addKeyword(keyword);
1006
- });
1070
+ 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;
1007
1089
  }
1008
1090
  /**
1009
- * Add custom formats to the validator
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.
1010
1096
  */
1011
- addCustomFormats(formats) {
1012
- if (!formats) return;
1013
- Object.entries(formats).forEach(([name, validator]) => {
1014
- if (validator instanceof RegExp) {
1015
- this.ajv.addFormat(name, validator);
1016
- } else {
1017
- this.ajv.addFormat(name, { validate: validator });
1018
- }
1019
- });
1097
+ static get outputPortNames() {
1098
+ const s = this.outputsSchema;
1099
+ if (!s || Array.isArray(s) || isSchemaLike(s)) return void 0;
1100
+ return Object.keys(s);
1020
1101
  }
1021
1102
  /**
1022
- * Create a validator function with caching
1023
- * @param schema - JSON Schema to validate against
1024
- * @param cacheKey - Optional cache key for reusing validators
1103
+ * Per-`input()`-invocation context, scoped via AsyncLocalStorage. One store
1104
+ * per class, shared by every instance: each `.run()` isolates one input()
1105
+ * call (and everything it `await`s, including detached `.then`/timer
1106
+ * continuations it schedules) into its own session, so concurrent inputs on a
1107
+ * node never clobber each other's context and a deferred send keeps the
1108
+ * context of the input that scheduled it. Real Node-RED never awaits an async
1109
+ * input handler before delivering the next message, so two inputs can be in
1110
+ * flight at once. `getStore()` is `undefined` only for a send made entirely
1111
+ * outside any `input()` (e.g. a timer set up in `created()`): it carries no
1112
+ * inherited context and delivers via `node.send`. Server-only — this is part
1113
+ * of the node runtime and is never imported in a browser.
1025
1114
  */
1026
- createValidator(schema, cacheKey) {
1027
- if (cacheKey && !schema.$id) {
1028
- schema.$id = cacheKey;
1029
- }
1030
- if (schema.$id) {
1031
- const cached = this.ajv.getSchema(schema.$id);
1032
- if (cached) return cached;
1115
+ static #invocation = new import_node_async_hooks.AsyncLocalStorage();
1116
+ context;
1117
+ constructor(RED, node, config, credentials) {
1118
+ super(RED, node, config, credentials);
1119
+ const context = node.context();
1120
+ const resolve = (scope, store) => {
1121
+ const target = scope === "global" ? context.global : scope === "flow" ? context.flow : context;
1122
+ return setupContext(target, store);
1123
+ };
1124
+ this.context = Object.assign(resolve, {
1125
+ node: setupContext(context),
1126
+ flow: setupContext(context.flow),
1127
+ global: setupContext(context.global)
1128
+ });
1129
+ const outputReturnProperties = this.config.outputReturnProperties;
1130
+ if (outputReturnProperties) {
1131
+ for (const [port, key] of Object.entries(outputReturnProperties)) {
1132
+ if (typeof key === "string" && key.trim() && !RETURN_PROPERTY_PATTERN.test(key.trim())) {
1133
+ throw new NrgError(
1134
+ `Invalid return property "${key}" for output port ${port} in ${this.constructor.type} \u2014 it must be a valid JavaScript identifier (letters, digits, _, $; not starting with a digit)`
1135
+ );
1136
+ }
1137
+ }
1033
1138
  }
1034
- const validator = this.ajv.compile(schema);
1035
- return validator;
1036
1139
  }
1037
- /**
1038
- * Validate data against a schema and return a structured result
1039
- */
1040
- validate(data, schema, options) {
1041
- const validator = this.createValidator(schema, options?.cacheKey);
1042
- const valid = validator(data);
1043
- if (!valid) {
1044
- const errorMessage = this.formatErrors(validator.errors);
1045
- if (options?.throwOnError) {
1046
- throw new ValidationError(errorMessage, validator.errors || []);
1140
+ [NRG_WIRE_HANDLERS](nodeRedNode, createdPromise) {
1141
+ super[NRG_WIRE_HANDLERS](nodeRedNode, createdPromise);
1142
+ const NC = this.constructor;
1143
+ nodeRedNode.on(
1144
+ "input",
1145
+ async (msg, send, done) => {
1146
+ try {
1147
+ await createdPromise;
1148
+ } catch (initError) {
1149
+ done(
1150
+ initError instanceof Error ? initError : new Error(String(initError))
1151
+ );
1152
+ return;
1153
+ }
1154
+ const store = { inputMsg: msg, send };
1155
+ try {
1156
+ nodeRedNode.log("Calling input");
1157
+ const result = await this.#input(msg, store);
1158
+ if (this.config.completePort) {
1159
+ this.#sendToPort("complete", {
1160
+ ...msg,
1161
+ ...result !== void 0 ? { output: result } : {},
1162
+ complete: {
1163
+ source: this.#nodeSource()
1164
+ },
1165
+ [INPUT_KEY]: msg
1166
+ });
1167
+ }
1168
+ done();
1169
+ nodeRedNode.log("Input processed");
1170
+ } catch (error) {
1171
+ const errorMsg = error instanceof Error ? error.message : "Unknown error during input handling";
1172
+ if (this.config.errorPort && !store.errorEmitted) {
1173
+ const errorData = error && typeof error === "object" ? { ...error } : {};
1174
+ this.#sendToPort("error", {
1175
+ ...msg,
1176
+ error: {
1177
+ ...errorData,
1178
+ name: error?.name ?? "Error",
1179
+ message: errorMsg,
1180
+ source: this.#nodeSource()
1181
+ },
1182
+ [INPUT_KEY]: msg
1183
+ });
1184
+ }
1185
+ if (error instanceof Error) {
1186
+ nodeRedNode.error(
1187
+ "Error while processing input: " + error.message,
1188
+ msg
1189
+ );
1190
+ done(error);
1191
+ } else {
1192
+ nodeRedNode.error(
1193
+ "Unknown error occurred during input handling",
1194
+ msg
1195
+ );
1196
+ done(new Error(errorMsg));
1197
+ }
1198
+ }
1047
1199
  }
1048
- return {
1049
- valid: false,
1050
- errors: validator.errors || void 0,
1051
- errorMessage
1052
- };
1200
+ );
1201
+ }
1202
+ input(msg) {
1203
+ return void 0;
1204
+ }
1205
+ 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");
1217
+ }
1218
+ return await _IONode.#invocation.run(
1219
+ store,
1220
+ () => Promise.resolve(this.input(msg))
1221
+ );
1222
+ }
1223
+ send(msg) {
1224
+ const multi = this.baseOutputs > 1 && Array.isArray(msg);
1225
+ const values = multi ? msg.slice(0, this.baseOutputs) : [msg];
1226
+ const out = values.map((m, port) => {
1227
+ if (m == null) return m;
1228
+ this.#validatePort(m, port);
1229
+ return this.#wrapOutgoing(m, this.#resolveContextMode(port), port);
1230
+ });
1231
+ this.#deliver(out);
1232
+ }
1233
+ #deliver(out) {
1234
+ const send = _IONode.#invocation.getStore()?.send;
1235
+ if (send) {
1236
+ send(out);
1237
+ } else {
1238
+ this.node.send(out);
1053
1239
  }
1054
- return {
1055
- valid: true,
1056
- data
1057
- };
1058
1240
  }
1059
1241
  /**
1060
- * Format errors into a human-readable string
1242
+ * 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).
1061
1246
  */
1062
- formatErrors(errors, options) {
1063
- if (!errors || errors.length === 0) {
1064
- return "No errors";
1065
- }
1066
- return this.ajv.errorsText(errors, {
1067
- separator: "; ",
1068
- dataVar: "data",
1069
- ...options
1247
+ #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);
1254
+ if (!schema) return;
1255
+ this.log("Validating output");
1256
+ this.RED.validator.validate(value, schema, {
1257
+ // Pure predicate: don't coerce or default the outgoing value.
1258
+ mutate: false,
1259
+ throwOnError: true
1070
1260
  });
1261
+ this.log("Output is valid");
1262
+ }
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];
1071
1271
  }
1072
1272
  /**
1073
- * Get detailed error information
1273
+ * 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`).
1276
+ * `this.send(x)` always means "x is the value at this port's return key",
1277
+ * never "x is the whole outgoing message".
1074
1278
  */
1075
- getDetailedErrors(errors) {
1076
- if (!errors || errors.length === 0) return [];
1077
- return errors.map((error) => ({
1078
- field: error.instancePath || "/",
1079
- message: error.message || "Validation failed",
1080
- keyword: error.keyword,
1081
- params: error.params,
1082
- schemaPath: error.schemaPath
1083
- }));
1279
+ #returnPropertyKey(port) {
1280
+ const configured = this.config.outputReturnProperties?.[port];
1281
+ if (typeof configured === "string" && configured.trim()) {
1282
+ return configured.trim();
1283
+ }
1284
+ return "output";
1084
1285
  }
1085
1286
  /**
1086
- * Add a schema to the validator for reference
1287
+ * 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"`.
1087
1290
  */
1088
- addSchema(schema, key) {
1089
- this.ajv.addSchema(schema, key);
1090
- return this;
1291
+ #resolveContextMode(port) {
1292
+ return this.config.outputContextModes?.[port] ?? "carry";
1091
1293
  }
1092
1294
  /**
1093
- * Remove a schema from the validator
1295
+ * Merges a sent value into the incoming message at the returnProperty key so
1296
+ * upstream message properties propagate. A fresh base is built per call so
1297
+ * multi-port sends never share an object.
1094
1298
  */
1095
- removeSchema(key) {
1096
- this.ajv.removeSchema(key);
1097
- return this;
1098
- }
1099
- };
1100
- var ValidationError = class _ValidationError extends Error {
1101
- constructor(message, errors) {
1102
- super(message);
1103
- this.errors = errors;
1104
- this.name = "ValidationError";
1105
- Object.setPrototypeOf(this, _ValidationError.prototype);
1106
- }
1107
- };
1108
-
1109
- // src/core/server/validation.ts
1110
- function initValidator(RED) {
1111
- if (RED.validator) return;
1112
- const nrg = {
1113
- validator: new Validator({
1114
- customKeywords: [
1115
- {
1116
- keyword: "x-nrg-skip-validation",
1117
- schemaType: "boolean",
1118
- valid: true
1119
- },
1120
- {
1121
- keyword: "x-nrg-node-type",
1122
- type: "string",
1123
- validate: (schemaValue, dataValue) => {
1124
- if (!dataValue) return true;
1125
- const node = RED.nodes.getNode(dataValue);
1126
- return node?.type === schemaValue;
1127
- }
1128
- }
1129
- ],
1130
- customFormats: {
1131
- "node-id": /^[a-zA-Z0-9-_]+$/
1132
- }
1133
- })
1134
- };
1135
- Object.defineProperty(RED, "_nrg", {
1136
- value: nrg,
1137
- writable: false,
1138
- enumerable: false,
1139
- configurable: false
1140
- });
1141
- Object.defineProperty(RED, "validator", {
1142
- get: () => nrg.validator,
1143
- enumerable: false,
1144
- configurable: false
1145
- });
1146
- }
1147
-
1148
- // src/core/server/api/assets.ts
1149
- var import_path = __toESM(require("path"), 1);
1150
- var import_fs = __toESM(require("fs"), 1);
1151
- var import_module = require("module");
1152
- function serveFile(filePath) {
1153
- return (_req, res, next) => {
1154
- if (!import_fs.default.existsSync(filePath)) return next();
1155
- res.setHeader("Content-Type", "application/javascript");
1156
- import_fs.default.createReadStream(filePath).pipe(res);
1157
- };
1158
- }
1159
- function initAssetsRoutes(router) {
1160
- const resourcesDir = import_path.default.resolve(__dirname, "./resources");
1161
- if (!import_fs.default.existsSync(resourcesDir)) return;
1162
- const _require = (0, import_module.createRequire)(import_path.default.join(__dirname, "package.json"));
1163
- const vueFile = process.env.NODE_ENV !== "production" ? _require.resolve("vue/dist/vue.esm-browser.js") : _require.resolve("vue/dist/vue.esm-browser.prod.js");
1164
- router.get(
1165
- "/nrg/assets/nrg-client.js",
1166
- serveFile(import_path.default.join(resourcesDir, "nrg-client.js"))
1167
- );
1168
- router.get("/nrg/assets/vue.esm-browser.prod.js", serveFile(vueFile));
1169
- router.get("/nrg/assets/vue.esm-browser.js", serveFile(vueFile));
1170
- }
1171
-
1172
- // src/core/server/api/routes.ts
1173
- var _initialized = false;
1174
- function initRoutes(RED) {
1175
- if (_initialized) return;
1176
- _initialized = true;
1177
- initAssetsRoutes(RED.httpAdmin);
1178
- }
1179
-
1180
- // src/core/server/registration.ts
1181
- async function registerType(RED, NodeClass) {
1182
- RED.log.debug(`Registering Type: ${NodeClass.type}`);
1183
- if (!(NodeClass.prototype instanceof Node)) {
1184
- throw new NrgError(
1185
- `${NodeClass.name} must extend IONode or ConfigNode classes`
1186
- );
1187
- }
1188
- if (!NodeClass.type) {
1189
- throw new NrgError("type must be provided when registering the node");
1190
- }
1191
- await NodeClass.register(RED);
1192
- RED.log.debug(`Type registered: ${NodeClass.type}`);
1193
- }
1194
- function registerTypes(nodes) {
1195
- const fn = Object.assign(
1196
- async function(RED) {
1197
- initValidator(RED);
1198
- initRoutes(RED);
1199
- try {
1200
- RED.log.info("Registering node types in series");
1201
- for (const NodeClass of nodes) {
1202
- await registerType(RED, NodeClass);
1203
- }
1204
- RED.log.info("All node types registered in series");
1205
- } catch (error) {
1206
- RED.log.error("Error registering node types:", error);
1207
- throw error;
1208
- }
1209
- },
1210
- { nodes }
1211
- );
1212
- return fn;
1213
- }
1214
-
1215
- // src/core/shared/schemas/type.ts
1216
- var import_typebox3 = require("@sinclair/typebox");
1217
-
1218
- // src/core/shared/constants.ts
1219
- var TYPED_INPUT_TYPES = [
1220
- "msg",
1221
- "flow",
1222
- "global",
1223
- "str",
1224
- "num",
1225
- "bool",
1226
- "json",
1227
- "bin",
1228
- "re",
1229
- "jsonata",
1230
- "date",
1231
- "env",
1232
- "node",
1233
- "cred"
1234
- ];
1235
-
1236
- // src/core/shared/schemas/base.ts
1237
- var import_typebox2 = require("@sinclair/typebox");
1238
- var NodeConfigSchema = import_typebox2.Type.Object({
1239
- id: import_typebox2.Type.String(),
1240
- type: import_typebox2.Type.String(),
1241
- name: import_typebox2.Type.String(),
1242
- z: import_typebox2.Type.Optional(import_typebox2.Type.String())
1243
- });
1244
- var ConfigNodeConfigSchema = import_typebox2.Type.Object({
1245
- ...NodeConfigSchema.properties,
1246
- _users: import_typebox2.Type.Array(import_typebox2.Type.String())
1247
- });
1248
- var IONodeConfigSchema = import_typebox2.Type.Object({
1249
- ...NodeConfigSchema.properties,
1250
- wires: import_typebox2.Type.Array(import_typebox2.Type.Array(import_typebox2.Type.String(), { default: [] }), {
1251
- default: [[]]
1252
- }),
1253
- x: import_typebox2.Type.Number(),
1254
- y: import_typebox2.Type.Number(),
1255
- g: import_typebox2.Type.Optional(import_typebox2.Type.String())
1256
- });
1257
- var TypedInputSchema = import_typebox2.Type.Object(
1258
- {
1259
- value: import_typebox2.Type.Union(
1260
- [
1261
- import_typebox2.Type.String(),
1262
- import_typebox2.Type.Number(),
1263
- import_typebox2.Type.Boolean(),
1264
- import_typebox2.Type.Null()
1265
- ],
1266
- {
1267
- description: "The actual value entered or selected.",
1268
- default: ""
1269
- }
1270
- ),
1271
- type: import_typebox2.Type.Union(
1272
- TYPED_INPUT_TYPES.map((type) => import_typebox2.Type.Literal(type)),
1273
- {
1274
- description: "The type of the value (string, number, message property, etc.)",
1275
- default: "str"
1276
- }
1277
- )
1278
- },
1279
- {
1280
- description: "Represents a Node-RED TypedInput value and its type.",
1281
- default: {
1282
- type: "str",
1283
- value: ""
1299
+ #wrapOutgoing(value, mode, port) {
1300
+ const key = this.#returnPropertyKey(port);
1301
+ const input = _IONode.#invocation.getStore()?.inputMsg ?? {};
1302
+ if (mode === "reset") {
1303
+ return { [key]: value };
1304
+ }
1305
+ if (mode === "trace") {
1306
+ return { ...input, [key]: value, [INPUT_KEY]: input };
1284
1307
  }
1308
+ return { ...input, [key]: value };
1285
1309
  }
1286
- );
1287
- var NodeSourceSchema = import_typebox2.Type.Object({
1288
- id: import_typebox2.Type.String(),
1289
- type: import_typebox2.Type.String(),
1290
- name: import_typebox2.Type.String()
1291
- });
1292
- var ErrorPortSchema = import_typebox2.Type.Object({
1293
- error: import_typebox2.Type.Object({
1294
- message: import_typebox2.Type.String(),
1295
- source: NodeSourceSchema
1296
- })
1297
- });
1298
- var CompletePortSchema = import_typebox2.Type.Object({
1299
- complete: import_typebox2.Type.Object({
1300
- source: NodeSourceSchema
1301
- })
1302
- });
1303
- var StatusPortSchema = import_typebox2.Type.Object({
1304
- status: import_typebox2.Type.Union([
1305
- import_typebox2.Type.Object({
1306
- fill: import_typebox2.Type.Optional(
1307
- import_typebox2.Type.Union([
1308
- import_typebox2.Type.Literal("red"),
1309
- import_typebox2.Type.Literal("green"),
1310
- import_typebox2.Type.Literal("yellow"),
1311
- import_typebox2.Type.Literal("blue"),
1312
- import_typebox2.Type.Literal("grey"),
1313
- import_typebox2.Type.Literal("gray")
1314
- ])
1315
- ),
1316
- shape: import_typebox2.Type.Optional(
1317
- import_typebox2.Type.Union([import_typebox2.Type.Literal("ring"), import_typebox2.Type.Literal("dot")])
1318
- ),
1319
- text: import_typebox2.Type.Optional(import_typebox2.Type.String())
1320
- }),
1321
- import_typebox2.Type.String()
1322
- ]),
1323
- source: NodeSourceSchema
1324
- });
1325
-
1326
- // src/core/shared/schemas/type.ts
1327
- var JSON_TYPES = /* @__PURE__ */ new Set([
1328
- "null",
1329
- "boolean",
1330
- "object",
1331
- "array",
1332
- "number",
1333
- "integer",
1334
- "string"
1335
- ]);
1336
- function isJSONType(value) {
1337
- return typeof value === "string" && JSON_TYPES.has(value);
1338
- }
1339
- function NodeRef(type, options) {
1340
- return {
1341
- ...import_typebox3.Type.String({
1342
- description: options?.description || `Reference to ${type}`
1343
- }),
1344
- // `...options` first so user options (description, x-nrg-form, …) apply, but
1345
- // the framework keys below win — a caller can't clobber `format`/
1346
- // `x-nrg-node-type`/`Kind` and silently break NodeRef resolution.
1347
- ...options,
1348
- format: "node-id",
1349
- "x-nrg-node-type": type,
1350
- [import_typebox3.Kind]: "NodeRef"
1351
- };
1352
- }
1353
- function TypedInput2(options) {
1354
- const opts = options ?? {};
1355
- const restrictedTypes = opts["x-nrg-form"]?.typedInputTypes;
1356
- if (restrictedTypes && restrictedTypes.length === 0) {
1357
- throw new Error(
1358
- "SchemaType.TypedInput: typedInputTypes must list at least one type."
1359
- );
1310
+ // --- Built-in port management ---
1311
+ get baseOutputs() {
1312
+ return this.constructor.outputs ?? 0;
1360
1313
  }
1361
- const explicitType = opts.default?.type;
1362
- if (restrictedTypes && explicitType !== void 0 && !restrictedTypes.includes(explicitType)) {
1363
- throw new Error(
1364
- `SchemaType.TypedInput: default type "${explicitType}" is not one of its typedInputTypes [${restrictedTypes.join(", ")}].`
1314
+ get totalOutputs() {
1315
+ let count = this.baseOutputs;
1316
+ if (this.config.errorPort) count++;
1317
+ if (this.config.completePort) count++;
1318
+ if (this.config.statusPort) count++;
1319
+ return count;
1320
+ }
1321
+ /**
1322
+ * 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.
1324
+ * Numeric indices refer to the base output ports (0-based).
1325
+ *
1326
+ * Built-in ports (`"error"`, `"complete"`, `"status"`) are managed by the
1327
+ * framework and cannot be sent to directly. Use `this.status()` for status,
1328
+ * throw an error or call `this.error()` for the error port, and the complete
1329
+ * port is sent automatically on successful input processing.
1330
+ */
1331
+ sendToPort(port, msg) {
1332
+ if (port === "error" || port === "complete" || port === "status") {
1333
+ throw new NrgError(
1334
+ `sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
1335
+ );
1336
+ }
1337
+ const portIndex = typeof port === "number" ? port : this.#getNamedPortIndex(port);
1338
+ if (typeof port === "string" && portIndex === null) {
1339
+ const keys = this.#namedPortKeys();
1340
+ 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.`
1342
+ );
1343
+ }
1344
+ const mode = this.#resolveContextMode(portIndex ?? 0);
1345
+ this.#sendToPort(
1346
+ port,
1347
+ msg == null ? msg : this.#wrapOutgoing(msg, mode, portIndex ?? 0)
1365
1348
  );
1366
1349
  }
1367
- const defaultType = explicitType ?? (restrictedTypes ? restrictedTypes.includes("str") ? "str" : restrictedTypes[0] : "str");
1368
- return {
1369
- ...TypedInputSchema,
1370
- // `...options` first; the framework keys below win (see NodeRef).
1371
- ...options,
1372
- default: {
1373
- ...TypedInputSchema.default,
1374
- ...opts.default,
1375
- type: defaultType
1376
- },
1377
- "x-nrg-typed-input": true,
1378
- [import_typebox3.Kind]: "TypedInput"
1379
- };
1380
- }
1381
- function NrgString(options) {
1382
- return import_typebox3.Type.String(options);
1383
- }
1384
- function OutputReturnProperties(options) {
1385
- return import_typebox3.Type.Record(
1386
- import_typebox3.Type.Number(),
1387
- import_typebox3.Type.String({ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }),
1388
- {
1389
- description: "Per-port return property, keyed by output port index. A missing entry falls back to `output`.",
1390
- default: {},
1391
- ...options
1350
+ #sendToPort(port, msg) {
1351
+ let portIndex;
1352
+ if (typeof port === "number") {
1353
+ portIndex = port;
1354
+ } else if (port === "error" || port === "complete" || port === "status") {
1355
+ portIndex = this.#getBuiltinPortIndex(port);
1356
+ if (portIndex === null) return;
1357
+ } else {
1358
+ portIndex = this.#getNamedPortIndex(port);
1359
+ if (portIndex === null) {
1360
+ throw new NrgError(`Unknown output port "${port}".`);
1361
+ }
1392
1362
  }
1393
- );
1394
- }
1395
- function OutputContextModes(options) {
1396
- return import_typebox3.Type.Record(
1397
- import_typebox3.Type.Number(),
1398
- import_typebox3.Type.Union([
1399
- import_typebox3.Type.Literal("carry"),
1400
- import_typebox3.Type.Literal("trace"),
1401
- import_typebox3.Type.Literal("reset")
1402
- ]),
1403
- {
1404
- description: "Per-port context mode, keyed by output port index. A missing entry falls back to `carry`.",
1405
- default: {},
1406
- ...options
1363
+ const out = new Array(this.totalOutputs);
1364
+ out[portIndex] = msg;
1365
+ this.node.send(out);
1366
+ }
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). */
1369
+ #namedPortKeys() {
1370
+ const schema = this.constructor.outputsSchema;
1371
+ if (!schema || Array.isArray(schema) || isSchemaLike(schema)) return null;
1372
+ return Object.keys(schema);
1373
+ }
1374
+ #getNamedPortIndex(name) {
1375
+ const keys = this.#namedPortKeys();
1376
+ if (!keys) return null;
1377
+ const idx = keys.indexOf(name);
1378
+ return idx === -1 ? null : idx;
1379
+ }
1380
+ #getBuiltinPortIndex(name) {
1381
+ if (name === "error") {
1382
+ return this.config.errorPort ? this.baseOutputs : null;
1407
1383
  }
1408
- );
1409
- }
1410
- function NrgUnsafe(options) {
1411
- return import_typebox3.Type.Unsafe(options);
1412
- }
1413
- var NrgBuilders = {
1414
- String: NrgString,
1415
- Unsafe: NrgUnsafe,
1416
- NodeRef,
1417
- TypedInput: TypedInput2,
1418
- OutputReturnProperties,
1419
- OutputContextModes
1420
- };
1421
- var SchemaType = Object.assign({}, import_typebox3.Type, NrgBuilders);
1422
- function markNonValidatable(schema) {
1423
- const type = schema.type;
1424
- const hasInvalidType = type !== void 0 && (Array.isArray(type) ? !type.every(isJSONType) : !isJSONType(type));
1425
- if (hasInvalidType) {
1426
- schema["x-nrg-skip-validation"] = true;
1427
- if (schema.default !== void 0) {
1428
- schema._default = schema.default;
1429
- delete schema.default;
1384
+ let idx = this.baseOutputs;
1385
+ if (this.config.errorPort) idx++;
1386
+ if (name === "complete") {
1387
+ return this.config.completePort ? idx : null;
1430
1388
  }
1431
- delete schema.type;
1389
+ if (this.config.completePort) idx++;
1390
+ return this.config.statusPort ? idx : null;
1432
1391
  }
1433
- if (schema.properties) {
1434
- for (const prop of Object.values(schema.properties)) {
1435
- markNonValidatable(prop);
1436
- }
1392
+ #nodeSource() {
1393
+ return {
1394
+ id: this.id,
1395
+ type: this.constructor.type,
1396
+ name: this.name
1397
+ };
1437
1398
  }
1438
- if (schema.patternProperties) {
1439
- for (const prop of Object.values(schema.patternProperties)) {
1440
- markNonValidatable(prop);
1399
+ status(status) {
1400
+ this.node.status(status);
1401
+ this.#sendToPort("status", {
1402
+ status,
1403
+ source: this.#nodeSource()
1404
+ });
1405
+ }
1406
+ error(message, msg) {
1407
+ super.error(message, msg);
1408
+ if (msg && this.config.errorPort) {
1409
+ this.#sendToPort("error", {
1410
+ ...msg,
1411
+ error: {
1412
+ // `name` keeps the error-port payload Catch-node compatible and
1413
+ // consistent with the auto-emit from a thrown error.
1414
+ name: "Error",
1415
+ message,
1416
+ source: this.#nodeSource()
1417
+ },
1418
+ [INPUT_KEY]: msg
1419
+ });
1420
+ const store = _IONode.#invocation.getStore();
1421
+ if (store) store.errorEmitted = true;
1441
1422
  }
1442
1423
  }
1443
- if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
1444
- markNonValidatable(schema.additionalProperties);
1424
+ updateWires(wires) {
1425
+ this.node.updateWires(wires);
1445
1426
  }
1446
- if (schema.items) {
1447
- markNonValidatable(schema.items);
1427
+ receive(msg) {
1428
+ this.node.receive(msg);
1448
1429
  }
1449
- if (schema.anyOf) {
1450
- schema.anyOf.forEach((s) => markNonValidatable(s));
1430
+ get x() {
1431
+ return this.node.x;
1451
1432
  }
1452
- if (schema.allOf) {
1453
- schema.allOf.forEach((s) => markNonValidatable(s));
1433
+ get y() {
1434
+ return this.node.y;
1454
1435
  }
1455
- if (schema.oneOf) {
1456
- schema.oneOf.forEach((s) => markNonValidatable(s));
1436
+ get g() {
1437
+ return this.node.g;
1438
+ }
1439
+ get wires() {
1440
+ return this.node.wires;
1441
+ }
1442
+ get credentials() {
1443
+ return this.node.credentials;
1444
+ }
1445
+ };
1446
+
1447
+ // src/sdk/lib/server/nodes/config-node.ts
1448
+ var ConfigNode = class extends Node {
1449
+ static category = "config";
1450
+ // Runtime counterpart: a real property so runtime code (the config proxy's
1451
+ // NodeRef resolution) can verify a referenced node is a config node — the only
1452
+ // guard a JS author gets. See NRG_CONFIG_NODE in ./symbols.
1453
+ [NRG_CONFIG_NODE] = true;
1454
+ context;
1455
+ constructor(RED, node, config, credentials) {
1456
+ super(RED, node, config, credentials);
1457
+ const context = node.context();
1458
+ const resolve = (scope, store) => {
1459
+ const target = scope === "global" ? context.global : context;
1460
+ return setupContext(target, store);
1461
+ };
1462
+ this.context = Object.assign(resolve, {
1463
+ node: setupContext(context),
1464
+ global: setupContext(context.global)
1465
+ });
1466
+ }
1467
+ get userIds() {
1468
+ return this.config._users;
1469
+ }
1470
+ get users() {
1471
+ return this.userIds.map((id) => this.RED.nodes.getNode(id)?._node).filter((node) => node != null);
1472
+ }
1473
+ getUser(index) {
1474
+ const id = this.userIds[index];
1475
+ if (!id) return void 0;
1476
+ return this.RED.nodes.getNode(id)?._node;
1477
+ }
1478
+ get credentials() {
1479
+ return this.node.credentials;
1480
+ }
1481
+ };
1482
+
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);
1496
+ }
1457
1497
  }
1458
- return schema;
1459
1498
  }
1460
- function defineSchema(properties, options) {
1461
- const schema = SchemaType.Object(properties, options);
1462
- return markNonValidatable(schema);
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);
1520
+ }
1521
+ async closed(removed) {
1522
+ if (def.closed) return def.closed.call(this, removed);
1523
+ }
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
+ );
1541
+ return NodeClass;
1463
1542
  }
1464
-
1465
- // src/core/server/index.ts
1466
- function defineModule(definition) {
1467
- return definition;
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);
1551
+ }
1552
+ async created() {
1553
+ if (def.created) return def.created.call(this);
1554
+ }
1555
+ async closed(removed) {
1556
+ if (def.closed) return def.closed.call(this, removed);
1557
+ }
1558
+ };
1559
+ Object.defineProperty(NodeClass, "name", {
1560
+ value: def.type.replace(
1561
+ /(^|-)(\w)/g,
1562
+ (_, __, c) => c.toUpperCase()
1563
+ ),
1564
+ configurable: true
1565
+ });
1566
+ normalizeSchemas([
1567
+ NodeClass.configSchema,
1568
+ NodeClass.credentialsSchema,
1569
+ NodeClass.settingsSchema
1570
+ ]);
1571
+ return NodeClass;
1468
1572
  }
1469
1573
  // Annotate the CommonJS export names for ESM import in node:
1470
1574
  0 && (module.exports = {
1471
- CompletePortSchema,
1472
1575
  ConfigNode,
1473
- ErrorPortSchema,
1474
1576
  IONode,
1577
+ Kind,
1475
1578
  Node,
1476
- NodeSourceSchema,
1477
1579
  NrgError,
1478
1580
  SchemaType,
1479
- StatusPortSchema,
1480
1581
  defineConfigNode,
1481
1582
  defineIONode,
1482
1583
  defineModule,