@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/CHANGELOG.md +54 -0
- package/README.md +3543 -0
- package/index.js +1884 -0
- package/lib/auth/authentication.js +1971 -0
- package/lib/auth/roditmanager.js +627 -0
- package/lib/auth/sessionmanager.js +1302 -0
- package/lib/auth/tokenservice.js +2418 -0
- package/lib/blockchain/blockchainservice.js +1715 -0
- package/lib/blockchain/statemanager.js +1614 -0
- package/lib/middleware/authenticationmw.js +2301 -0
- package/lib/middleware/environcredentialstoremw.js +176 -0
- package/lib/middleware/filecredentialstoremw.js +158 -0
- package/lib/middleware/loggingmw.js +82 -0
- package/lib/middleware/performanceexamplemw.js +58 -0
- package/lib/middleware/performancemw.js +172 -0
- package/lib/middleware/ratelimitmw.js +171 -0
- package/lib/middleware/validatepermissionsmw.js +439 -0
- package/lib/middleware/vaultcredentialstoremw.js +617 -0
- package/lib/middleware/versioningmw.js +142 -0
- package/lib/middleware/webhookhandlermw.js +1388 -0
- package/package.json +57 -0
- package/services/configsdk.js +588 -0
- package/services/env.js +34 -0
- package/services/error-response.js +29 -0
- package/services/logger.js +160 -0
- package/services/performanceservice.js +568 -0
- package/services/utils.js +1024 -0
- package/services/versionmanager.js +81 -0
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
|
+
};
|
package/services/env.js
ADDED
|
@@ -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
|
+
};
|