@rodit/rodit-auth-be 9.11.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@rodit/rodit-auth-be",
3
+ "version": "9.11.14",
4
+ "description": "RODiT-based authentication system for Express.js applications",
5
+ "main": "index.js",
6
+ "exports": {
7
+ ".": "./index.js"
8
+ },
9
+ "scripts": {
10
+ },
11
+ "keywords": [
12
+ "authentication",
13
+ "express",
14
+ "middleware",
15
+ "jwt",
16
+ "rodit"
17
+ ],
18
+ "author": "Discernible IO",
19
+ "license": "UNLICENSED",
20
+ "private": false,
21
+ "files": [
22
+ "index.js",
23
+ "lib/**/*",
24
+ "services/**/*",
25
+ "CHANGELOG.md",
26
+ "README.md"
27
+ ],
28
+ "dependencies": {
29
+ "jose": "^5.6.3",
30
+ "tweetnacl": "^1.0.3",
31
+ "tweetnacl-util": "^0.15.1",
32
+ "ulid": "^3.0.2",
33
+ "undici": "^6.0.0"
34
+ },
35
+ "peerDependencies": {
36
+ "borsh": "^2.0.0",
37
+ "bs58": ">=5 <6",
38
+ "config": "^4.0.0",
39
+ "express": ">=4.18 <6",
40
+ "express-rate-limit": "^8.0.0",
41
+ "express-session": "^1.17.0 || ^1.18.0",
42
+ "node-vault": "^0.10.0",
43
+ "winston": "^3.0.0"
44
+ },
45
+ "devDependencies": {
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/discernible-io/rodit-auth-be.git"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "engines": {
55
+ "node": ">=18.0.0"
56
+ }
57
+ }
@@ -0,0 +1,588 @@
1
+ /**
2
+ * Configuration management
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ /*
7
+ * SDK Config Wrapper with Fallback Defaults
8
+ *
9
+ * This module wraps the 'config' package to provide safe accessors that
10
+ * gracefully fall back to baked-in defaults when config keys are missing.
11
+ *
12
+ * Exclusions: Vault keys (VAULT_*) and METHOD_PERMISSION_MAP are intentionally
13
+ * NOT included in fallback defaults.
14
+ */
15
+
16
+
17
+ // Attempt to load the 'config' package if present in the host app
18
+ let nodeConfig = null;
19
+ try {
20
+ // Using require directly so consumer apps can bring their own 'config'
21
+ // eslint-disable-next-line import/no-extraneous-dependencies
22
+ nodeConfig = require("config");
23
+ } catch (_) {
24
+ nodeConfig = null;
25
+ }
26
+
27
+ // Deep utilities (no external deps)
28
+ function deepGet(obj, keyPath) {
29
+ if (!obj || !keyPath) return undefined;
30
+ const parts = keyPath.split(".");
31
+ let cur = obj;
32
+ for (const p of parts) {
33
+ if (cur && Object.prototype.hasOwnProperty.call(cur, p)) {
34
+ cur = cur[p];
35
+ } else {
36
+ return undefined;
37
+ }
38
+ }
39
+ return cur;
40
+ }
41
+
42
+ function isPlainObject(val) {
43
+ return val && typeof val === "object" && !Array.isArray(val);
44
+ }
45
+
46
+ function deepMerge(target, source) {
47
+ const out = Array.isArray(target) ? [...target] : { ...(target || {}) };
48
+ if (isPlainObject(source)) {
49
+ for (const [k, v] of Object.entries(source)) {
50
+ if (isPlainObject(v)) {
51
+ out[k] = deepMerge(out[k] || {}, v);
52
+ } else if (Array.isArray(v)) {
53
+ out[k] = Array.isArray(out[k]) ? [...out[k], ...v] : [...v];
54
+ } else {
55
+ out[k] = v;
56
+ }
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ function candidateState(rawValue) {
63
+ if (rawValue === undefined || rawValue === null) return "missing";
64
+ if (typeof rawValue === "string" && rawValue.trim() === "") return "missing";
65
+ return "present";
66
+ }
67
+
68
+ function parseCandidateForType(rawValue, expectedType) {
69
+ const state = candidateState(rawValue);
70
+ if (state === "missing") {
71
+ return { state: "missing", value: undefined };
72
+ }
73
+
74
+ if (!expectedType) {
75
+ return { state: "valid", value: rawValue };
76
+ }
77
+
78
+ if (expectedType === "boolean") {
79
+ if (typeof rawValue === "boolean") {
80
+ return { state: "valid", value: rawValue };
81
+ }
82
+ if (typeof rawValue === "string") {
83
+ const lower = rawValue.trim().toLowerCase();
84
+ if (lower === "true") return { state: "valid", value: true };
85
+ if (lower === "false") return { state: "valid", value: false };
86
+ }
87
+ return {
88
+ state: "malformed",
89
+ reason: "boolean values must be string 'true' or 'false'"
90
+ };
91
+ }
92
+
93
+ if (expectedType === "number") {
94
+ if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
95
+ return { state: "valid", value: rawValue };
96
+ }
97
+ if (typeof rawValue === "string") {
98
+ const trimmed = rawValue.trim();
99
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
100
+ const parsed = Number(trimmed);
101
+ if (Number.isFinite(parsed)) {
102
+ return { state: "valid", value: parsed };
103
+ }
104
+ }
105
+ }
106
+ return { state: "malformed", reason: "number value is not parseable" };
107
+ }
108
+
109
+ if (expectedType === "string") {
110
+ if (typeof rawValue === "string") {
111
+ return { state: "valid", value: rawValue };
112
+ }
113
+ return { state: "malformed", reason: "string value expected" };
114
+ }
115
+
116
+ return { state: "valid", value: rawValue };
117
+ }
118
+
119
+ function inferExpectedType(pathStr, fallbackValue, defaultValue) {
120
+ const ruleType = VALIDATION_RULES?.[pathStr]?.type;
121
+ if (ruleType) return ruleType;
122
+ if (fallbackValue !== undefined && fallbackValue !== null) return typeof fallbackValue;
123
+ if (defaultValue !== undefined && defaultValue !== null) return typeof defaultValue;
124
+ return undefined;
125
+ }
126
+
127
+ function getResolved(pathStr, defaultValue) {
128
+ const envVarName = pathStr.toUpperCase().replace(/\./g, "_");
129
+ const hasEnvKey = Object.prototype.hasOwnProperty.call(process.env, envVarName);
130
+ const envRaw = hasEnvKey ? process.env[envVarName] : undefined;
131
+
132
+ let hostRaw;
133
+ let hasHostValue = false;
134
+ if (nodeConfig) {
135
+ try {
136
+ hostRaw = nodeConfig.get(pathStr);
137
+ hasHostValue = true;
138
+ } catch (_) {
139
+ hasHostValue = false;
140
+ }
141
+ }
142
+
143
+ const fallbackValue = deepGet(FALLBACK_DEFAULTS, pathStr);
144
+ const hasFallback = fallbackValue !== undefined;
145
+ const hasDefaultArg = defaultValue !== undefined;
146
+ const expectedType = inferExpectedType(pathStr, fallbackValue, defaultValue);
147
+
148
+ if (hasEnvKey) {
149
+ const envParsed = parseCandidateForType(envRaw, expectedType);
150
+ if (envParsed.state === "valid") {
151
+ return {
152
+ value: envParsed.value,
153
+ source: "environment",
154
+ reason: "environment value provided"
155
+ };
156
+ }
157
+ if (hasFallback) {
158
+ return {
159
+ value: fallbackValue,
160
+ source: "default",
161
+ reason: `default, environment value ${envParsed.state}`
162
+ };
163
+ }
164
+ if (hasDefaultArg) {
165
+ return {
166
+ value: defaultValue,
167
+ source: "default",
168
+ reason: `default, environment value ${envParsed.state}`
169
+ };
170
+ }
171
+ }
172
+
173
+ if (hasHostValue) {
174
+ const hostParsed = parseCandidateForType(hostRaw, expectedType);
175
+ if (hostParsed.state === "valid") {
176
+ return {
177
+ value: hostParsed.value,
178
+ source: "default.json",
179
+ reason: "default.json value provided"
180
+ };
181
+ }
182
+ if (hasFallback) {
183
+ return {
184
+ value: fallbackValue,
185
+ source: "default",
186
+ reason: `default, default.json value ${hostParsed.state}`
187
+ };
188
+ }
189
+ if (hasDefaultArg) {
190
+ return {
191
+ value: defaultValue,
192
+ source: "default",
193
+ reason: `default, default.json value ${hostParsed.state}`
194
+ };
195
+ }
196
+ }
197
+
198
+ if (hasFallback) {
199
+ return {
200
+ value: fallbackValue,
201
+ source: "default",
202
+ reason: "default, no environment or default.json value"
203
+ };
204
+ }
205
+
206
+ if (hasDefaultArg) {
207
+ return {
208
+ value: defaultValue,
209
+ source: "default",
210
+ reason: "default argument used"
211
+ };
212
+ }
213
+
214
+ const err = new Error(`Configuration property '${pathStr}' is not defined`);
215
+ err.code = "CONFIG_PROPERTY_MISSING";
216
+ throw err;
217
+ }
218
+
219
+ // Baked-in fallback defaults sourced from config/default.json (excluding Vault and METHOD_PERMISSION_MAP)
220
+ const FALLBACK_DEFAULTS = {
221
+ API_VERSION: "0.0.0",
222
+ // Credential source strategy.
223
+ // Options:
224
+ // - "env": read credentials from environment-backed sources
225
+ // - "file": read credentials from filesystem-backed sources
226
+ // - "vault": read credentials from vault-backed sources (when available)
227
+ RODIT_NEAR_CREDENTIALS_SOURCE: "env",
228
+ SECURITY_OPTIONS: {
229
+ LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY: "0.80",
230
+ THRESHOLD_VALIDATION_TYPE: "0.10",
231
+ DURATIONRAMP: "0.85",
232
+ // RODiT flow initiator behavior.
233
+ // Options:
234
+ // - "SERVER-INITIATED": server starts the flow
235
+ // - "CLIENT-INITIATED": client starts the flow
236
+ SERVERORCLIENT: "SERVER-INITIATED",
237
+ // Login error behavior.
238
+ // Options:
239
+ // - true: hide detailed login failure reasons from clients
240
+ // - false: return detailed login failure reasons to clients
241
+ SILENT_LOGIN_FAILURES: false,
242
+ // Session validation strictness.
243
+ // Options:
244
+ // - true: allow relaxed validation checks
245
+ // - false: enforce strict validation checks
246
+ RELAXED_SESSION_VALIDATION: true,
247
+ // Session middleware secret used for signing session data.
248
+ // Options:
249
+ // - any non-empty string (recommended: long, random secret on main)
250
+ SESSION_SECRET: "HMAC-session-secret-is-not-set",
251
+ // Webhook outbound TLS verification.
252
+ // Options:
253
+ // - true: skip TLS certificate verification (for controlled/self-signed setups)
254
+ // - false: enforce normal TLS certificate verification
255
+ WEBHOOK_TLS_SKIP_VERIFY: false,
256
+ // Inbound webhook signature verification bypass.
257
+ // Options:
258
+ // - true: bypass signature verification (test/debug only)
259
+ // - false: require signature verification
260
+ BYPASS_WEBHOOK_VERIFICATION: false,
261
+ LOGIN_MODE: "partner", // Options: "partner" (default), "promiscuous", "p2p"
262
+ // Server session lifetime (seconds) from login. Independent of passport jwt_duration.
263
+ // Still capped by peer/own not_after when bounded. Set 0 to use passport-derived rules.
264
+ SESSION_TTL_SECONDS: 5200,
265
+ // Default access-token lifetime when passport jwt_duration is missing or invalid.
266
+ FALLBACK_JWT_DURATION: 3600,
267
+ // Upper bound on JWT exp (seconds from iat) when peer RODiT not_after is unbounded
268
+ // (1970-01-01 / unix 0). Metadata jwt_duration may be shorter; this value is only a cap.
269
+ JWT_MAX_DURATION_SECONDS_RODIT_UNBOUNDED: 86400,
270
+ },
271
+ // Default to env-based credential store; host apps can override with RODIT_NEAR_CREDENTIALS_SOURCE env
272
+ credentials: {
273
+ filePath: "./.near-credentials/credentials-not-set.json"
274
+ },
275
+ API_DEFAULT_OPTIONS: {
276
+ ISO639: "es",
277
+ ISO3166: "ES",
278
+ ISO15924: "215",
279
+ TIMESTAMP_MAX_AGE: 300,
280
+ TIMEOPTIONS: {
281
+ tzname: "Europe/Madrid",
282
+ tzoffset: "+01:00",
283
+ datetimeformat: "2023-04-15T14:30:00-05:00",
284
+ },
285
+ },
286
+ NEAR_RPC_URL: "https://rpc.mainnet.fastnear.com",
287
+ NEAR_CONTRACT_ID: "discernible-io.near",
288
+ SERVICE_NAME: "service-name-not-set",
289
+ // Runtime environment.
290
+ // Options: "main", "development", "test"
291
+ NODE_ENV: "development",
292
+ // Logging verbosity.
293
+ // Options: "error", "warn", "info", "debug", "trace"
294
+ LOG_LEVEL: "info",
295
+ LOKI_TLS_SKIP_VERIFY: false,
296
+ // Default login endpoint path used by login_server flow.
297
+ LOGIN_RODIT_PATH: "/api/login",
298
+ SIGNPORTAL_API_URL: "https://signportal.api-not-set.example.com",
299
+ // Session storage configuration
300
+ // Options:
301
+ // - "memory": standalone in-memory SDK store
302
+ // - "express" / "express-session": express-session MemoryStore adapter
303
+ SESSION_STORAGE_TYPE: "memory",
304
+ // Session cleanup configuration
305
+ SESSION_CLEANUP_INTERVAL: 500000, // Milliseconds
306
+ SESSION_TOKEN_RETENTION_PERIOD: 5000000, // Seconds
307
+ NEAR_RPC_CACHE_TTL: 5000, // Milliseconds
308
+ // Session validation cache TTL (milliseconds) - trades security for performance
309
+ // Lower values = more secure but more storage lookups
310
+ // Higher values = faster but longer window after logout where token may still work
311
+ // Set to 0 to disable caching (always check session state)
312
+ SESSION_VALIDATION_CACHE_TTL: 5000, // 5 seconds default
313
+ WEBHOOK_TEST_ENABLED: false,
314
+ // Default empty permission map so consumers can opt-into permissions as needed
315
+ METHOD_PERMISSION_MAP: {},
316
+ };
317
+
318
+ function has(pathStr) {
319
+ try {
320
+ return getResolved(pathStr).value !== undefined;
321
+ } catch (_) {
322
+ return false;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Get configuration value with fallback support
328
+ * @param {string} pathStr - Configuration key path (e.g., 'API_DEFAULT_OPTIONS.LOG_DIR')
329
+ * @param {*} defaultValue - Optional default value if key is missing
330
+ * @returns {*} Configuration value
331
+ */
332
+ function get(pathStr, defaultValue) {
333
+ return getResolved(pathStr, defaultValue).value;
334
+ }
335
+
336
+ function getAllMerged() {
337
+ // Returns a merged view: node config (if any) overlaid onto fallbacks
338
+ let merged = { ...FALLBACK_DEFAULTS };
339
+ if (nodeConfig && typeof nodeConfig.util?.toObject === "function") {
340
+ try {
341
+ const asObject = nodeConfig.util.toObject();
342
+ merged = deepMerge(FALLBACK_DEFAULTS, asObject);
343
+ } catch (_) {}
344
+ }
345
+ return merged;
346
+ }
347
+
348
+ /**
349
+ * Validation rules for critical configuration
350
+ */
351
+ const VALIDATION_RULES = {
352
+ 'NEAR_RPC_URL': {
353
+ required: true,
354
+ type: 'string',
355
+ validate: (value, logger) => {
356
+ if (!value.startsWith('http://') && !value.startsWith('https://')) {
357
+ return 'NEAR_RPC_URL must be a valid HTTP/HTTPS URL';
358
+ }
359
+ // Warn if using public endpoint
360
+ if (value.includes('rpc.mainnet.near.org')) {
361
+ logger && logger.warn('Using public NEAR RPC endpoint; expect rate limiting', {
362
+ rpcUrl: value,
363
+ recommendation: 'Use a dedicated RPC provider for main deployments'
364
+ });
365
+ }
366
+ return null;
367
+ }
368
+ },
369
+ 'SECURITY_OPTIONS.LOGIN_MODE': {
370
+ required: true,
371
+ type: 'string',
372
+ validate: (value) => {
373
+ const validModes = ['partner', 'promiscuous', 'p2p'];
374
+ if (!validModes.includes(value)) {
375
+ return `LOGIN_MODE must be one of: ${validModes.join(', ')}`;
376
+ }
377
+ return null;
378
+ }
379
+ },
380
+ 'LOG_LEVEL': {
381
+ required: false,
382
+ type: 'string',
383
+ validate: (value) => {
384
+ const validLevels = ['error', 'warn', 'info', 'debug'];
385
+ if (value && !validLevels.includes(value)) {
386
+ return `LOG_LEVEL must be one of: ${validLevels.join(', ')}`;
387
+ }
388
+ return null;
389
+ }
390
+ },
391
+ 'SECURITY_OPTIONS.WEBHOOK_TLS_SKIP_VERIFY': {
392
+ required: false,
393
+ type: 'boolean',
394
+ validate: () => null
395
+ },
396
+ 'SECURITY_OPTIONS.BYPASS_WEBHOOK_VERIFICATION': {
397
+ required: false,
398
+ type: 'boolean',
399
+ validate: () => null
400
+ },
401
+ 'SECURITY_OPTIONS.SESSION_SECRET': {
402
+ required: false,
403
+ type: 'string',
404
+ validate: (value) => {
405
+ if (!value || value.length === 0) {
406
+ return 'SECURITY_OPTIONS.SESSION_SECRET cannot be empty when provided';
407
+ }
408
+ return null;
409
+ }
410
+ },
411
+ 'SECURITY_OPTIONS.FALLBACK_JWT_DURATION': {
412
+ required: false,
413
+ type: 'number',
414
+ validate: (value) => {
415
+ if (value != null && (value < 60 || value > 86400 * 7)) {
416
+ return 'SECURITY_OPTIONS.FALLBACK_JWT_DURATION should be between 60 and 604800 seconds (7 days)';
417
+ }
418
+ return null;
419
+ }
420
+ },
421
+ 'SECURITY_OPTIONS.SESSION_TTL_SECONDS': {
422
+ required: false,
423
+ type: 'number',
424
+ validate: (value) => {
425
+ if (value != null && (value < 60 || value > 86400 * 365)) {
426
+ return 'SECURITY_OPTIONS.SESSION_TTL_SECONDS should be between 60 and 31536000 seconds (365 days)';
427
+ }
428
+ return null;
429
+ }
430
+ },
431
+ 'NEAR_RPC_TIMEOUT': {
432
+ required: false,
433
+ type: 'number',
434
+ validate: (value) => {
435
+ if (value && (value < 1000 || value > 60000)) {
436
+ return 'NEAR_RPC_TIMEOUT should be between 1000-60000ms';
437
+ }
438
+ return null;
439
+ }
440
+ },
441
+ 'NEAR_CONTRACT_ID': {
442
+ required: true,
443
+ type: 'string',
444
+ validate: (value) => {
445
+ if (!value || value.length === 0) {
446
+ return 'NEAR_CONTRACT_ID cannot be empty';
447
+ }
448
+ return null;
449
+ }
450
+ }
451
+ };
452
+
453
+ /**
454
+ * Validate configuration against defined rules
455
+ * @param {Object} logger - Optional logger instance for warnings
456
+ * @returns {boolean} True if validation passes
457
+ * @throws {Error} If validation fails
458
+ */
459
+ function validate(logger) {
460
+ const errors = [];
461
+ const warnings = [];
462
+
463
+ if (logger) {
464
+ if (typeof logger.infoWithContext === "function") {
465
+ logger.infoWithContext("Validating configuration", {
466
+ component: "ConfigSDK",
467
+ operation: "config.validate"
468
+ });
469
+ } else {
470
+ logger.info("Validating configuration");
471
+ }
472
+ }
473
+
474
+ for (const [key, rules] of Object.entries(VALIDATION_RULES)) {
475
+ let value;
476
+ try {
477
+ value = get(key);
478
+ } catch (err) {
479
+ if (rules.required) {
480
+ errors.push(`Missing required config: ${key}`);
481
+ }
482
+ continue;
483
+ }
484
+
485
+ // Type check
486
+ if (rules.type && typeof value !== rules.type) {
487
+ errors.push(`${key} must be of type ${rules.type}, got ${typeof value}`);
488
+ continue;
489
+ }
490
+
491
+ // Custom validation
492
+ if (rules.validate) {
493
+ const validationError = rules.validate(value, logger);
494
+ if (validationError) {
495
+ errors.push(`${key}: ${validationError}`);
496
+ }
497
+ }
498
+
499
+ if (logger) {
500
+ if (typeof logger.debugWithContext === "function") {
501
+ logger.debugWithContext("Configuration key validated", {
502
+ component: "ConfigSDK",
503
+ operation: "config.validate",
504
+ key,
505
+ value
506
+ });
507
+ } else {
508
+ logger.debug(`${key}: ${value}`);
509
+ }
510
+ }
511
+ }
512
+
513
+ if (errors.length > 0) {
514
+ if (logger) {
515
+ if (typeof logger.errorWithContext === "function") {
516
+ logger.errorWithContext("Configuration validation failed", {
517
+ component: "ConfigSDK",
518
+ operation: "config.validate",
519
+ errors
520
+ });
521
+ } else {
522
+ logger.error("Configuration validation failed", { errors });
523
+ }
524
+ }
525
+ throw new Error(`Configuration validation failed:\n${errors.join('\n')}`);
526
+ }
527
+
528
+ if (warnings.length > 0) {
529
+ if (logger) {
530
+ if (typeof logger.warnWithContext === "function") {
531
+ logger.warnWithContext("Configuration warnings", {
532
+ component: "ConfigSDK",
533
+ operation: "config.validate",
534
+ warnings
535
+ });
536
+ } else {
537
+ logger.warn("Configuration warnings", { warnings });
538
+ }
539
+ }
540
+ }
541
+
542
+ if (logger) {
543
+ if (typeof logger.infoWithContext === "function") {
544
+ logger.infoWithContext("Configuration validation passed", {
545
+ component: "ConfigSDK",
546
+ operation: "config.validate"
547
+ });
548
+ } else {
549
+ logger.info("Configuration validation passed");
550
+ }
551
+ }
552
+ return true;
553
+ }
554
+
555
+ /**
556
+ * Default access-token duration (seconds) when RODiT metadata jwt_duration is absent or invalid.
557
+ *
558
+ * @returns {number}
559
+ */
560
+ function getDefaultJwtDurationSeconds() {
561
+ const parsed = parseInt(get("SECURITY_OPTIONS.FALLBACK_JWT_DURATION", "3600"), 10);
562
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 3600;
563
+ }
564
+
565
+ /**
566
+ * Server session TTL from login (seconds), or null when 0/disabled (passport-derived rules).
567
+ *
568
+ * @returns {number|null}
569
+ */
570
+ function getSessionTtlSeconds() {
571
+ const parsed = parseInt(get("SECURITY_OPTIONS.SESSION_TTL_SECONDS", "5200"), 10);
572
+ if (!Number.isFinite(parsed) || parsed <= 0) {
573
+ return null;
574
+ }
575
+ return Math.floor(parsed);
576
+ }
577
+
578
+ module.exports = {
579
+ has,
580
+ get,
581
+ getResolved,
582
+ getAllMerged,
583
+ getDefaultJwtDurationSeconds,
584
+ getSessionTtlSeconds,
585
+ validate,
586
+ FALLBACK_DEFAULTS,
587
+ VALIDATION_RULES,
588
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Runtime environment helpers (development / main / test).
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ function getNodeEnv() {
7
+ const value = process.env.NODE_ENV;
8
+ return value && String(value).trim() ? String(value).trim().toLowerCase() : 'development';
9
+ }
10
+
11
+ function isMainEnvironment() {
12
+ return getNodeEnv() === 'main';
13
+ }
14
+
15
+ function isDevelopmentEnvironment() {
16
+ return getNodeEnv() === 'development';
17
+ }
18
+
19
+ function isTestEnvironment() {
20
+ return getNodeEnv() === 'test';
21
+ }
22
+
23
+ /** Main deploy: strict security (hidden error details, required peer keys). */
24
+ function isStrictEnvironment() {
25
+ return isMainEnvironment();
26
+ }
27
+
28
+ module.exports = {
29
+ getNodeEnv,
30
+ isMainEnvironment,
31
+ isDevelopmentEnvironment,
32
+ isTestEnvironment,
33
+ isStrictEnvironment,
34
+ };
@@ -0,0 +1,29 @@
1
+ const DEFAULT_ERROR_STATUS = 500;
2
+
3
+ function buildErrorResponse({ requestId, code, message, details }) {
4
+ const payload = {
5
+ error: {
6
+ code,
7
+ message
8
+ },
9
+ requestId,
10
+ timestamp: new Date().toISOString()
11
+ };
12
+
13
+ if (details && Object.keys(details).length > 0) {
14
+ payload.error.details = details;
15
+ }
16
+
17
+ return payload;
18
+ }
19
+
20
+ function sendError(res, { statusCode = DEFAULT_ERROR_STATUS, requestId, code, message, details }) {
21
+ return res
22
+ .status(statusCode)
23
+ .json(buildErrorResponse({ requestId, code, message, details }));
24
+ }
25
+
26
+ module.exports = {
27
+ buildErrorResponse,
28
+ sendError
29
+ };