@blamejs/core 0.6.13 → 0.6.21
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 +8 -0
- package/NOTICE +16 -0
- package/README.md +27 -18
- package/index.js +12 -0
- package/lib/archive.js +8 -7
- package/lib/audit.js +4 -0
- package/lib/auth/password.js +449 -4
- package/lib/bundler.js +8 -8
- package/lib/cache.js +105 -20
- package/lib/cli.js +598 -4
- package/lib/config-drift.js +309 -0
- package/lib/crypto-field.js +37 -0
- package/lib/crypto.js +8 -0
- package/lib/db-query.js +21 -2
- package/lib/db.js +32 -2
- package/lib/dual-control.js +475 -0
- package/lib/file-type.js +265 -0
- package/lib/framework-schema.js +38 -6
- package/lib/http-client-cookie-jar.js +117 -17
- package/lib/http-client.js +81 -3
- package/lib/internal-sha1-hibp.js +34 -0
- package/lib/mail.js +5 -4
- package/lib/middleware/csp-nonce.js +7 -4
- package/lib/middleware/index.js +2 -0
- package/lib/middleware/network-allowlist.js +199 -0
- package/lib/network-dns.js +564 -0
- package/lib/network-heartbeat.js +290 -0
- package/lib/network-nts.js +552 -0
- package/lib/network-proxy.js +246 -0
- package/lib/network-tls.js +326 -0
- package/lib/network.js +233 -0
- package/lib/ntp-check.js +50 -4
- package/lib/object-store/azure-blob.js +16 -42
- package/lib/pagination.js +136 -76
- package/lib/parsers/index.js +16 -2
- package/lib/parsers/safe-ini.js +273 -0
- package/lib/permissions.js +223 -9
- package/lib/pqc-agent.js +4 -4
- package/lib/retention.js +439 -0
- package/lib/security-assert.js +368 -0
- package/lib/session.js +138 -8
- package/lib/ssrf-guard.js +9 -0
- package/lib/vault/index.js +3 -3
- package/lib/vendor/MANIFEST.json +12 -0
- package/lib/vendor/common-passwords-top-10000.txt +10000 -0
- package/package.json +3 -2
- package/sbom.cyclonedx.json +61 -0
package/lib/network.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var ntpCheck = require("./ntp-check");
|
|
4
|
+
var nts = require("./network-nts");
|
|
5
|
+
var dns = require("./network-dns");
|
|
6
|
+
var proxy = require("./network-proxy");
|
|
7
|
+
var trust = require("./network-tls");
|
|
8
|
+
var heartbeat = require("./network-heartbeat");
|
|
9
|
+
|
|
10
|
+
var validateOpts = require("./validate-opts");
|
|
11
|
+
var lazyRequire = require("./lazy-require");
|
|
12
|
+
var { defineClass } = require("./framework-error");
|
|
13
|
+
|
|
14
|
+
var NetworkError = defineClass("NetworkError", { alwaysPermanent: true });
|
|
15
|
+
|
|
16
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
17
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
18
|
+
|
|
19
|
+
var SOCKET_DEFAULTS = {
|
|
20
|
+
noDelay: true,
|
|
21
|
+
keepAlive: true,
|
|
22
|
+
keepAliveInitialDelayMs: 0,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function _setSocketNoDelay(value) {
|
|
26
|
+
if (typeof value !== "boolean") {
|
|
27
|
+
throw new NetworkError("socket/bad-no-delay", "socket.setDefaultNoDelay: expected boolean, got " + typeof value);
|
|
28
|
+
}
|
|
29
|
+
SOCKET_DEFAULTS.noDelay = value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _setSocketKeepAlive(opts) {
|
|
33
|
+
opts = opts || {};
|
|
34
|
+
validateOpts(opts, ["enable", "initialDelayMs"], "socket.setDefaultKeepAlive");
|
|
35
|
+
if (opts.enable !== undefined) {
|
|
36
|
+
if (typeof opts.enable !== "boolean") {
|
|
37
|
+
throw new NetworkError("socket/bad-keepalive", "socket.setDefaultKeepAlive: enable must be boolean");
|
|
38
|
+
}
|
|
39
|
+
SOCKET_DEFAULTS.keepAlive = opts.enable;
|
|
40
|
+
}
|
|
41
|
+
if (opts.initialDelayMs !== undefined) {
|
|
42
|
+
if (typeof opts.initialDelayMs !== "number" || !isFinite(opts.initialDelayMs) || opts.initialDelayMs < 0) {
|
|
43
|
+
throw new NetworkError("socket/bad-keepalive-delay", "socket.setDefaultKeepAlive: initialDelayMs must be non-negative finite number");
|
|
44
|
+
}
|
|
45
|
+
SOCKET_DEFAULTS.keepAliveInitialDelayMs = opts.initialDelayMs;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// SO_LINGER is intentionally NOT exposed: Node's net.Socket has no
|
|
50
|
+
// public setLinger() method (the option is tracked at
|
|
51
|
+
// nodejs/node#27293 / rejected for the public surface). Reaching into
|
|
52
|
+
// socket._handle to call setLinger is an unstable internal API. The
|
|
53
|
+
// closest operator-exposed semantic is socket.destroy() (RST = "abort
|
|
54
|
+
// on close") vs socket.end() (graceful FIN). Operators needing true
|
|
55
|
+
// SO_LINGER should use a native binding outside the framework. This
|
|
56
|
+
// stub stays in the export with throw-on-call so a future Node release
|
|
57
|
+
// that exposes setLinger can be wired without an API addition.
|
|
58
|
+
function _setSocketLinger(_opts) {
|
|
59
|
+
throw new NetworkError("socket/linger-not-supported",
|
|
60
|
+
"socket.setDefaultLinger: SO_LINGER is not exposed by Node's public net.Socket API " +
|
|
61
|
+
"(see nodejs/node#27293). Use socket.destroy() (abort) vs socket.end() (graceful) " +
|
|
62
|
+
"to control close semantics, or a native binding if true SO_LINGER is required.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _socketDefaults() {
|
|
66
|
+
return {
|
|
67
|
+
noDelay: SOCKET_DEFAULTS.noDelay,
|
|
68
|
+
keepAlive: SOCKET_DEFAULTS.keepAlive,
|
|
69
|
+
keepAliveInitialDelayMs: SOCKET_DEFAULTS.keepAliveInitialDelayMs,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function applyToSocket(socket) {
|
|
74
|
+
if (!socket) return socket;
|
|
75
|
+
try {
|
|
76
|
+
if (typeof socket.setNoDelay === "function") socket.setNoDelay(SOCKET_DEFAULTS.noDelay);
|
|
77
|
+
if (typeof socket.setKeepAlive === "function") {
|
|
78
|
+
socket.setKeepAlive(SOCKET_DEFAULTS.keepAlive, SOCKET_DEFAULTS.keepAliveInitialDelayMs || 0);
|
|
79
|
+
}
|
|
80
|
+
} catch (_e) {}
|
|
81
|
+
return socket;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var ntpFacade = {
|
|
85
|
+
querySingle: ntpCheck.querySingle,
|
|
86
|
+
checkDrift: ntpCheck.checkDrift,
|
|
87
|
+
bootCheck: ntpCheck.bootCheck,
|
|
88
|
+
setThresholds: ntpCheck.setThresholds,
|
|
89
|
+
getThresholds: ntpCheck.getThresholds,
|
|
90
|
+
setServers: function (list) {
|
|
91
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
92
|
+
throw new NetworkError("ntp/bad-servers", "ntp.setServers: expected non-empty array");
|
|
93
|
+
}
|
|
94
|
+
ntpFacade._defaultServers = list.slice();
|
|
95
|
+
_emitObs("network.ntp.servers.set", { count: list.length });
|
|
96
|
+
},
|
|
97
|
+
getServers: function () {
|
|
98
|
+
return (ntpFacade._defaultServers || ntpCheck.DEFAULT_SERVERS).slice();
|
|
99
|
+
},
|
|
100
|
+
_defaultServers: null,
|
|
101
|
+
nts: nts,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function bootFromEnv(opts) {
|
|
105
|
+
opts = opts || {};
|
|
106
|
+
validateOpts(opts, ["env", "audit"], "network.bootFromEnv");
|
|
107
|
+
var env = opts.env || process.env;
|
|
108
|
+
var applied = { ntp: {}, dns: {}, proxy: false, tls: {}, heartbeat: 0, socket: {} };
|
|
109
|
+
|
|
110
|
+
if (env.BLAMEJS_NTP_SERVERS) {
|
|
111
|
+
var list = String(env.BLAMEJS_NTP_SERVERS).split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
112
|
+
if (list.length > 0) { ntpFacade.setServers(list); applied.ntp.servers = list.length; }
|
|
113
|
+
}
|
|
114
|
+
var ntpTimeout = env.BLAMEJS_NTP_TIMEOUT_MS;
|
|
115
|
+
if (ntpTimeout) {
|
|
116
|
+
var t = parseInt(ntpTimeout, 10);
|
|
117
|
+
if (isFinite(t) && t > 0) { ntpFacade._defaultTimeoutMs = t; applied.ntp.timeoutMs = t; }
|
|
118
|
+
}
|
|
119
|
+
var ntpWarn = env.BLAMEJS_NTP_DRIFT_WARN_MS;
|
|
120
|
+
var ntpFatal = env.BLAMEJS_NTP_DRIFT_FATAL_MS;
|
|
121
|
+
if (ntpWarn || ntpFatal) {
|
|
122
|
+
var thr = {};
|
|
123
|
+
if (ntpWarn) { thr.warnMs = parseInt(ntpWarn, 10); applied.ntp.warnMs = thr.warnMs; }
|
|
124
|
+
if (ntpFatal) { thr.fatalMs = parseInt(ntpFatal, 10); applied.ntp.fatalMs = thr.fatalMs; }
|
|
125
|
+
ntpCheck.setThresholds(thr);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
var dnsServers = env.BLAMEJS_DNS_SERVERS;
|
|
129
|
+
if (dnsServers) {
|
|
130
|
+
var dl = String(dnsServers).split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
131
|
+
if (dl.length > 0) { dns.setServers(dl); applied.dns.servers = dl.length; }
|
|
132
|
+
}
|
|
133
|
+
if (env.BLAMEJS_DNS_RESULT_ORDER) { dns.setResultOrder(env.BLAMEJS_DNS_RESULT_ORDER); applied.dns.resultOrder = env.BLAMEJS_DNS_RESULT_ORDER; }
|
|
134
|
+
if (env.BLAMEJS_DNS_FAMILY) { dns.setFamily(parseInt(env.BLAMEJS_DNS_FAMILY, 10)); applied.dns.family = parseInt(env.BLAMEJS_DNS_FAMILY, 10); }
|
|
135
|
+
if (env.BLAMEJS_DNS_LOOKUP_TIMEOUT_MS) { dns.setLookupTimeoutMs(parseInt(env.BLAMEJS_DNS_LOOKUP_TIMEOUT_MS, 10)); applied.dns.lookupTimeoutMs = parseInt(env.BLAMEJS_DNS_LOOKUP_TIMEOUT_MS, 10); }
|
|
136
|
+
if (env.BLAMEJS_DNS_CACHE_TTL_MS) { dns.setCacheTtlMs(parseInt(env.BLAMEJS_DNS_CACHE_TTL_MS, 10)); applied.dns.cacheTtlMs = parseInt(env.BLAMEJS_DNS_CACHE_TTL_MS, 10); }
|
|
137
|
+
if (env.BLAMEJS_DOH_URL) { dns.useDnsOverHttps({ url: env.BLAMEJS_DOH_URL }); applied.dns.doh = env.BLAMEJS_DOH_URL; }
|
|
138
|
+
else if (env.BLAMEJS_DOH_PROVIDER) { dns.useDnsOverHttps({ provider: env.BLAMEJS_DOH_PROVIDER }); applied.dns.dohProvider = env.BLAMEJS_DOH_PROVIDER; }
|
|
139
|
+
if (env.BLAMEJS_DOT_HOST) { dns.useDnsOverTls({ host: env.BLAMEJS_DOT_HOST, port: env.BLAMEJS_DOT_PORT ? parseInt(env.BLAMEJS_DOT_PORT, 10) : 853 }); applied.dns.dot = env.BLAMEJS_DOT_HOST; }
|
|
140
|
+
|
|
141
|
+
if (env.HTTP_PROXY || env.http_proxy || env.HTTPS_PROXY || env.https_proxy ||
|
|
142
|
+
env.NO_PROXY || env.no_proxy || env.ALL_PROXY || env.all_proxy) {
|
|
143
|
+
applied.proxy = proxy.fromEnv(env);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (env.BLAMEJS_EXTRA_CA_CERTS) {
|
|
147
|
+
trust.addCa(env.BLAMEJS_EXTRA_CA_CERTS, { label: "BLAMEJS_EXTRA_CA_CERTS" });
|
|
148
|
+
applied.tls.fileLoaded = env.BLAMEJS_EXTRA_CA_CERTS;
|
|
149
|
+
}
|
|
150
|
+
if (env.BLAMEJS_EXTRA_CA_CERTS_DIR) {
|
|
151
|
+
trust.addCaBundle(env.BLAMEJS_EXTRA_CA_CERTS_DIR, { label: "BLAMEJS_EXTRA_CA_CERTS_DIR" });
|
|
152
|
+
applied.tls.dirLoaded = env.BLAMEJS_EXTRA_CA_CERTS_DIR;
|
|
153
|
+
}
|
|
154
|
+
if (env.BLAMEJS_USE_SYSTEM_TRUST === "1" || env.BLAMEJS_USE_SYSTEM_TRUST === "true") {
|
|
155
|
+
trust.useSystemTrust(true);
|
|
156
|
+
applied.tls.systemTrust = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (env.BLAMEJS_SOCKET_NO_DELAY) _setSocketNoDelay(env.BLAMEJS_SOCKET_NO_DELAY === "1" || env.BLAMEJS_SOCKET_NO_DELAY === "true");
|
|
160
|
+
if (env.BLAMEJS_SOCKET_KEEPALIVE) _setSocketKeepAlive({ enable: env.BLAMEJS_SOCKET_KEEPALIVE === "1" || env.BLAMEJS_SOCKET_KEEPALIVE === "true" });
|
|
161
|
+
if (env.BLAMEJS_SOCKET_KEEPALIVE_DELAY_MS) _setSocketKeepAlive({ initialDelayMs: parseInt(env.BLAMEJS_SOCKET_KEEPALIVE_DELAY_MS, 10) });
|
|
162
|
+
applied.socket = _socketDefaults();
|
|
163
|
+
|
|
164
|
+
var auditOn = opts.audit !== false;
|
|
165
|
+
if (auditOn) {
|
|
166
|
+
var sink;
|
|
167
|
+
try { sink = audit(); } catch (_e) { sink = null; }
|
|
168
|
+
if (sink && typeof sink.safeEmit === "function") {
|
|
169
|
+
try {
|
|
170
|
+
sink.safeEmit({
|
|
171
|
+
action: "network.boot.from_env",
|
|
172
|
+
outcome: "success",
|
|
173
|
+
metadata: applied,
|
|
174
|
+
});
|
|
175
|
+
} catch (_e) {}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
_emitObs("network.boot.from_env", { source: "env" });
|
|
179
|
+
return applied;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function snapshot() {
|
|
183
|
+
return {
|
|
184
|
+
ntp: {
|
|
185
|
+
servers: ntpFacade.getServers(),
|
|
186
|
+
thresholds: ntpCheck.getThresholds(),
|
|
187
|
+
},
|
|
188
|
+
dns: dns._stateForTest(),
|
|
189
|
+
proxy: proxy.snapshot(),
|
|
190
|
+
tls: {
|
|
191
|
+
systemTrust: trust.isSystemTrustEnabled(),
|
|
192
|
+
caCount: trust.getTrustStore().length,
|
|
193
|
+
},
|
|
194
|
+
heartbeat: heartbeat.statuses(),
|
|
195
|
+
socket: _socketDefaults(),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _emitObs(name, fields) {
|
|
200
|
+
try { observability().emit(name, fields || {}); } catch (_e) {}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function _resetForTest() {
|
|
204
|
+
ntpFacade._defaultServers = null;
|
|
205
|
+
ntpFacade._defaultTimeoutMs = null;
|
|
206
|
+
if (typeof ntpCheck._resetThresholdsForTest === "function") ntpCheck._resetThresholdsForTest();
|
|
207
|
+
dns._resetForTest();
|
|
208
|
+
proxy._resetForTest();
|
|
209
|
+
trust._resetForTest();
|
|
210
|
+
heartbeat._resetForTest();
|
|
211
|
+
SOCKET_DEFAULTS.noDelay = true;
|
|
212
|
+
SOCKET_DEFAULTS.keepAlive = true;
|
|
213
|
+
SOCKET_DEFAULTS.keepAliveInitialDelayMs = 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
ntp: ntpFacade,
|
|
218
|
+
dns: dns,
|
|
219
|
+
proxy: proxy,
|
|
220
|
+
tls: trust,
|
|
221
|
+
heartbeat: heartbeat,
|
|
222
|
+
socket: {
|
|
223
|
+
setDefaultNoDelay: _setSocketNoDelay,
|
|
224
|
+
setDefaultKeepAlive: _setSocketKeepAlive,
|
|
225
|
+
setDefaultLinger: _setSocketLinger,
|
|
226
|
+
defaults: _socketDefaults,
|
|
227
|
+
applyToSocket: applyToSocket,
|
|
228
|
+
},
|
|
229
|
+
bootFromEnv: bootFromEnv,
|
|
230
|
+
snapshot: snapshot,
|
|
231
|
+
NetworkError: NetworkError,
|
|
232
|
+
_resetForTest: _resetForTest,
|
|
233
|
+
};
|
package/lib/ntp-check.js
CHANGED
|
@@ -36,6 +36,43 @@ var DEFAULT_SERVERS = ["pool.ntp.org", "time.cloudflare.com"];
|
|
|
36
36
|
var DEFAULT_PORT = 123;
|
|
37
37
|
var DEFAULT_TIMEOUT_MS = 3000;
|
|
38
38
|
|
|
39
|
+
var DEFAULT_DRIFT_WARN_MS = C.TIME.minutes(5);
|
|
40
|
+
var DEFAULT_DRIFT_FATAL_MS = C.TIME.hours(1);
|
|
41
|
+
|
|
42
|
+
var thresholds = {
|
|
43
|
+
warnMs: DEFAULT_DRIFT_WARN_MS,
|
|
44
|
+
fatalMs: DEFAULT_DRIFT_FATAL_MS,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function setThresholds(opts) {
|
|
48
|
+
opts = opts || {};
|
|
49
|
+
if (opts.warnMs !== undefined) {
|
|
50
|
+
if (typeof opts.warnMs !== "number" || !isFinite(opts.warnMs) || opts.warnMs < 0) {
|
|
51
|
+
throw new TypeError("ntpCheck.setThresholds: warnMs must be non-negative finite number, got " + JSON.stringify(opts.warnMs));
|
|
52
|
+
}
|
|
53
|
+
thresholds.warnMs = opts.warnMs;
|
|
54
|
+
}
|
|
55
|
+
if (opts.fatalMs !== undefined) {
|
|
56
|
+
if (typeof opts.fatalMs !== "number" || !isFinite(opts.fatalMs) || opts.fatalMs < 0) {
|
|
57
|
+
throw new TypeError("ntpCheck.setThresholds: fatalMs must be non-negative finite number, got " + JSON.stringify(opts.fatalMs));
|
|
58
|
+
}
|
|
59
|
+
thresholds.fatalMs = opts.fatalMs;
|
|
60
|
+
}
|
|
61
|
+
if (thresholds.warnMs > thresholds.fatalMs && thresholds.fatalMs > 0) {
|
|
62
|
+
throw new RangeError("ntpCheck.setThresholds: warnMs (" + thresholds.warnMs +
|
|
63
|
+
") must be <= fatalMs (" + thresholds.fatalMs + ")");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getThresholds() {
|
|
68
|
+
return { warnMs: thresholds.warnMs, fatalMs: thresholds.fatalMs };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _resetThresholdsForTest() {
|
|
72
|
+
thresholds.warnMs = DEFAULT_DRIFT_WARN_MS;
|
|
73
|
+
thresholds.fatalMs = DEFAULT_DRIFT_FATAL_MS;
|
|
74
|
+
}
|
|
75
|
+
|
|
39
76
|
/**
|
|
40
77
|
* Query an NTP server once. Resolves with { driftMs, serverTimeMs } or
|
|
41
78
|
* rejects with { code, message } where code is one of:
|
|
@@ -142,22 +179,26 @@ async function bootCheck(opts) {
|
|
|
142
179
|
}
|
|
143
180
|
var absMs = Math.abs(result.driftMs);
|
|
144
181
|
var driftStr = (result.driftMs >= 0 ? "+" : "") + result.driftMs + "ms";
|
|
145
|
-
|
|
182
|
+
var fatalMs = (opts && typeof opts.driftFatalMs === "number") ? opts.driftFatalMs : thresholds.fatalMs;
|
|
183
|
+
var warnMs = (opts && typeof opts.driftWarnMs === "number") ? opts.driftWarnMs : thresholds.warnMs;
|
|
184
|
+
if (fatalMs > 0 && absMs >= fatalMs) {
|
|
146
185
|
return {
|
|
147
186
|
ok: false,
|
|
148
187
|
severity: "fatal",
|
|
149
188
|
driftMs: result.driftMs,
|
|
150
189
|
server: result.server,
|
|
151
|
-
message: "clock drift " + driftStr + " from " + result.server +
|
|
190
|
+
message: "clock drift " + driftStr + " from " + result.server +
|
|
191
|
+
" (>= " + fatalMs + "ms) — refuse to boot",
|
|
152
192
|
};
|
|
153
193
|
}
|
|
154
|
-
if (absMs >=
|
|
194
|
+
if (warnMs > 0 && absMs >= warnMs) {
|
|
155
195
|
return {
|
|
156
196
|
ok: true,
|
|
157
197
|
severity: "warning",
|
|
158
198
|
driftMs: result.driftMs,
|
|
159
199
|
server: result.server,
|
|
160
|
-
message: "clock drift " + driftStr + " from " + result.server +
|
|
200
|
+
message: "clock drift " + driftStr + " from " + result.server +
|
|
201
|
+
" (>= " + warnMs + "ms) — investigate",
|
|
161
202
|
};
|
|
162
203
|
}
|
|
163
204
|
return {
|
|
@@ -173,6 +214,11 @@ module.exports = {
|
|
|
173
214
|
querySingle: querySingle,
|
|
174
215
|
checkDrift: checkDrift,
|
|
175
216
|
bootCheck: bootCheck,
|
|
217
|
+
setThresholds: setThresholds,
|
|
218
|
+
getThresholds: getThresholds,
|
|
176
219
|
DEFAULT_SERVERS: DEFAULT_SERVERS,
|
|
220
|
+
DEFAULT_DRIFT_WARN_MS: DEFAULT_DRIFT_WARN_MS,
|
|
221
|
+
DEFAULT_DRIFT_FATAL_MS: DEFAULT_DRIFT_FATAL_MS,
|
|
177
222
|
NTP_TO_UNIX_OFFSET_SECONDS: NTP_TO_UNIX_OFFSET_SECONDS,
|
|
223
|
+
_resetThresholdsForTest: _resetThresholdsForTest,
|
|
178
224
|
};
|
|
@@ -400,49 +400,23 @@ function create(config) {
|
|
|
400
400
|
function presignedUploadUrl(opts) { return _presign("PUT", "cw", opts); }
|
|
401
401
|
function presignedDownloadUrl(opts) { return _presign("GET", "r", opts); }
|
|
402
402
|
|
|
403
|
-
// Azure SAS
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
403
|
+
// Azure SAS has no equivalent of S3 / GCS POST policy with a
|
|
404
|
+
// content-length-range constraint — the SAS spec carries permissions
|
|
405
|
+
// / start / expiry / IP / protocol / resource-content-headers but
|
|
406
|
+
// no body-size cap. Returning a PUT URL under the POST-policy name
|
|
407
|
+
// would be a silent shape mismatch (operators wiring an HTML form
|
|
408
|
+
// expecting multipart fields get a PUT URL with no fields), so the
|
|
409
|
+
// azure-blob backend refuses cleanly.
|
|
410
410
|
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
// 4xx the requester if Content-Length > opts.maxBytes.
|
|
421
|
-
function presignedUploadPolicy(opts) {
|
|
422
|
-
opts = opts || {};
|
|
423
|
-
if (typeof opts.maxBytes !== "number" || !Number.isFinite(opts.maxBytes) ||
|
|
424
|
-
opts.maxBytes <= 0) {
|
|
425
|
-
throw _err("INVALID_MAX_BYTES",
|
|
426
|
-
"presignedUploadPolicy: maxBytes (positive number of bytes) is required " +
|
|
427
|
-
"(advisory on Azure — see docstring for server-side enforcement)", true);
|
|
428
|
-
}
|
|
429
|
-
var underlying = _presign("PUT", "cw", opts);
|
|
430
|
-
return {
|
|
431
|
-
url: underlying.url,
|
|
432
|
-
method: "PUT",
|
|
433
|
-
// SAS uploads are PUT, not multipart POST — there are no form
|
|
434
|
-
// fields. Operators uploading attach the body directly to the
|
|
435
|
-
// returned URL with the headers in `headers`.
|
|
436
|
-
fields: null,
|
|
437
|
-
headers: underlying.headers,
|
|
438
|
-
expiresAt: underlying.expiresAt,
|
|
439
|
-
maxBytes: opts.maxBytes,
|
|
440
|
-
enforcement: "client-only",
|
|
441
|
-
enforcementNote:
|
|
442
|
-
"Azure SAS does not natively cap upload size. Operators needing " +
|
|
443
|
-
"strict size enforcement must HEAD the blob post-upload and reject " +
|
|
444
|
-
"if Content-Length exceeds maxBytes.",
|
|
445
|
-
};
|
|
411
|
+
// For strict server-side body-size enforcement on Azure: use
|
|
412
|
+
// presignedUploadUrl + a server-side post-upload HEAD that deletes
|
|
413
|
+
// and 4xxs the requester if Content-Length > limit.
|
|
414
|
+
function presignedUploadPolicy(_opts) {
|
|
415
|
+
throw _err("PRESIGN_NOT_SUPPORTED",
|
|
416
|
+
"azure-blob backend does not support presigned upload policies — " +
|
|
417
|
+
"Azure SAS has no body-size cap. Use presignedUploadUrl + a server-side " +
|
|
418
|
+
"HEAD-and-delete check, or switch to an S3 / GCS-compatible backend.",
|
|
419
|
+
true);
|
|
446
420
|
}
|
|
447
421
|
|
|
448
422
|
return {
|
package/lib/pagination.js
CHANGED
|
@@ -75,10 +75,17 @@
|
|
|
75
75
|
* module's offset() returns a `total` (from COUNT(*)) and computes
|
|
76
76
|
* `totalPages` so legacy clients can render numbered nav.
|
|
77
77
|
*
|
|
78
|
+
* Multi-column ordering:
|
|
79
|
+
* - orderBy accepts a string (single column), an array of strings
|
|
80
|
+
* (multiple columns, all using opts.direction), or an array of
|
|
81
|
+
* { column, direction } objects (mixed directions per column).
|
|
82
|
+
* The keyset WHERE expands to the standard OR cascade
|
|
83
|
+
* (col0 [op0] ? OR (col0 = ? AND col1 [op1] ?) OR ...)
|
|
84
|
+
* so successive pages can't repeat or skip rows when ties on the
|
|
85
|
+
* leading columns are broken by trailing ones. _id is appended as
|
|
86
|
+
* a tiebreaker if not already in the orderBy chain.
|
|
87
|
+
*
|
|
78
88
|
* Out of scope (with structural reasons documented):
|
|
79
|
-
* - Multi-column composite orderBy (orderBy: ["a", "b"]). Use raw
|
|
80
|
-
* SQL + encodeCursor / decodeCursor. The Query builder doesn't
|
|
81
|
-
* model multi-column ORDER BY today.
|
|
82
89
|
* - Cursor TTL / expiry. Operators who want time-limited cursors
|
|
83
90
|
* embed a timestamp in their own state and check at decode-time
|
|
84
91
|
* before passing to .cursor(). The framework's HMAC tag carries
|
|
@@ -202,6 +209,83 @@ function _resolveLimit(opts) {
|
|
|
202
209
|
|
|
203
210
|
// ---- Cursor pagination ----
|
|
204
211
|
|
|
212
|
+
// Normalize opts.orderBy into an array of { column, direction } entries.
|
|
213
|
+
// Accepts:
|
|
214
|
+
// undefined / null → [{ column: "_id", direction: opts.direction || "asc" }]
|
|
215
|
+
// "createdAt" → [{ column: "createdAt", direction: opts.direction || "asc" }]
|
|
216
|
+
// ["createdAt", "_id"] → all entries default to opts.direction || "asc"
|
|
217
|
+
// [{column:"a",direction:"desc"}, {column:"b"}]
|
|
218
|
+
// → mixed; missing direction defaults to opts.direction || "asc"
|
|
219
|
+
// Always appends an _id tiebreaker if not present, so cursor uniqueness
|
|
220
|
+
// is guaranteed regardless of the operator's spec.
|
|
221
|
+
function _normalizeOrderBy(opts) {
|
|
222
|
+
var defaultDir = (opts && opts.direction === "desc") ? "desc" : "asc";
|
|
223
|
+
var raw = opts && opts.orderBy;
|
|
224
|
+
var entries;
|
|
225
|
+
if (raw == null) {
|
|
226
|
+
entries = [{ column: "_id", direction: defaultDir }];
|
|
227
|
+
} else if (typeof raw === "string") {
|
|
228
|
+
entries = [{ column: raw, direction: defaultDir }];
|
|
229
|
+
} else if (Array.isArray(raw)) {
|
|
230
|
+
entries = raw.map(function (e) {
|
|
231
|
+
if (typeof e === "string") return { column: e, direction: defaultDir };
|
|
232
|
+
if (!e || typeof e !== "object" || typeof e.column !== "string") {
|
|
233
|
+
throw new PaginationError("pagination/bad-orderby",
|
|
234
|
+
"orderBy[] entries must be strings or { column, direction } objects, got " +
|
|
235
|
+
JSON.stringify(e));
|
|
236
|
+
}
|
|
237
|
+
var d = (e.direction || defaultDir).toLowerCase();
|
|
238
|
+
if (d !== "asc" && d !== "desc") {
|
|
239
|
+
throw new PaginationError("pagination/bad-orderby",
|
|
240
|
+
"orderBy[].direction must be 'asc' | 'desc', got " + JSON.stringify(e.direction));
|
|
241
|
+
}
|
|
242
|
+
return { column: e.column, direction: d };
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
throw new PaginationError("pagination/bad-orderby",
|
|
246
|
+
"orderBy must be a string, array, or omitted; got " + typeof raw);
|
|
247
|
+
}
|
|
248
|
+
for (var i = 0; i < entries.length; i++) {
|
|
249
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(entries[i].column)) {
|
|
250
|
+
throw new PaginationError("pagination/bad-orderby",
|
|
251
|
+
"orderBy column must match /^[A-Za-z_][A-Za-z0-9_]*$/ (identifier-safe), got " +
|
|
252
|
+
JSON.stringify(entries[i].column));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Append _id tiebreaker if not already in the chain. Direction
|
|
256
|
+
// matches the chain's last entry by convention so the tiebreaker
|
|
257
|
+
// doesn't reverse the natural reading order.
|
|
258
|
+
var hasId = entries.some(function (e) { return e.column === "_id"; });
|
|
259
|
+
if (!hasId) {
|
|
260
|
+
entries.push({ column: "_id", direction: entries[entries.length - 1].direction });
|
|
261
|
+
}
|
|
262
|
+
return entries;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Build the keyset WHERE clause for next-page navigation given the
|
|
266
|
+
// cursor's column values. Each (col, dir) entry expands the OR cascade:
|
|
267
|
+
// col0 [op0] ? OR (col0 = ? AND col1 [op1] ?) OR ... OR (col0 = ? AND ... AND coln [opn] ?)
|
|
268
|
+
// The compareOp per column flips by direction × forward (XNOR).
|
|
269
|
+
function _buildKeysetWhere(orderEntries, cursorVals, forward) {
|
|
270
|
+
var clauses = [];
|
|
271
|
+
var params = [];
|
|
272
|
+
for (var i = 0; i < orderEntries.length; i++) {
|
|
273
|
+
var entry = orderEntries[i];
|
|
274
|
+
// Effective direction: asc + forward → ">", desc + forward → "<", and reversed for backward.
|
|
275
|
+
var effectiveAsc = (entry.direction === "asc") === forward;
|
|
276
|
+
var op = effectiveAsc ? ">" : "<";
|
|
277
|
+
var equalChain = [];
|
|
278
|
+
for (var j = 0; j < i; j++) {
|
|
279
|
+
equalChain.push('"' + orderEntries[j].column + '" = ?');
|
|
280
|
+
params.push(cursorVals[j]);
|
|
281
|
+
}
|
|
282
|
+
equalChain.push('"' + entry.column + '" ' + op + ' ?');
|
|
283
|
+
params.push(cursorVals[i]);
|
|
284
|
+
clauses.push("(" + equalChain.join(" AND ") + ")");
|
|
285
|
+
}
|
|
286
|
+
return { sql: clauses.join(" OR "), params: params };
|
|
287
|
+
}
|
|
288
|
+
|
|
205
289
|
async function cursor(query, opts) {
|
|
206
290
|
if (!query || typeof query.where !== "function" || typeof query.orderBy !== "function" ||
|
|
207
291
|
typeof query.limit !== "function" || typeof query.all !== "function") {
|
|
@@ -213,110 +297,86 @@ async function cursor(query, opts) {
|
|
|
213
297
|
throw new PaginationError("pagination/no-secret",
|
|
214
298
|
"cursor: opts.secret is required (Buffer or non-empty string for HMAC tagging)");
|
|
215
299
|
}
|
|
216
|
-
var limit
|
|
217
|
-
var
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
// `req.query.orderBy` through doesn't create an SQL-injection vector.
|
|
222
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(orderBy)) {
|
|
223
|
-
throw new PaginationError("pagination/bad-orderby",
|
|
224
|
-
"cursor: orderBy must match /^[A-Za-z_][A-Za-z0-9_]*$/ (identifier-safe), got " +
|
|
225
|
-
JSON.stringify(orderBy));
|
|
226
|
-
}
|
|
227
|
-
var direction = (opts.direction === "desc") ? "desc" : "asc";
|
|
300
|
+
var limit = _resolveLimit(opts);
|
|
301
|
+
var orderEntries = _normalizeOrderBy(opts);
|
|
302
|
+
// Cursor compatibility key: array of `column:direction` pairs canonicalizes
|
|
303
|
+
// the orderBy spec so a stored cursor must match exactly to be replayed.
|
|
304
|
+
var orderKey = orderEntries.map(function (e) { return e.column + ":" + e.direction; });
|
|
228
305
|
|
|
229
|
-
// Decode incoming cursor (if any) and override direction from cursor.
|
|
230
|
-
// The cursor authoritatively encodes which way we're paging — operators
|
|
231
|
-
// shouldn't need to round-trip direction in the URL.
|
|
232
306
|
var cursorState = null;
|
|
233
307
|
var forward = (opts.forward !== false);
|
|
234
308
|
if (opts.cursor) {
|
|
235
309
|
cursorState = decodeCursor(opts.cursor, opts.secret);
|
|
236
|
-
|
|
310
|
+
var cursorKey = Array.isArray(cursorState.orderKey) ? cursorState.orderKey :
|
|
311
|
+
// Back-compat with v0.6.20 single-column cursors: synthesize the
|
|
312
|
+
// key from the legacy orderBy/dir fields.
|
|
313
|
+
[cursorState.orderBy + ":" + cursorState.dir];
|
|
314
|
+
if (JSON.stringify(cursorKey) !== JSON.stringify(orderKey)) {
|
|
237
315
|
throw new PaginationError("pagination/cursor-mismatch",
|
|
238
|
-
"cursor
|
|
239
|
-
|
|
240
|
-
direction + "' — operator must use the same opts the cursor was issued under");
|
|
316
|
+
"cursor orderKey [" + cursorKey.join(", ") + "] does not match call orderKey [" +
|
|
317
|
+
orderKey.join(", ") + "] — operator must use the same orderBy/direction spec");
|
|
241
318
|
}
|
|
242
319
|
if (typeof cursorState.forward === "boolean") forward = cursorState.forward;
|
|
243
320
|
}
|
|
244
321
|
|
|
245
|
-
// Apply the
|
|
246
|
-
// asc + forward → strictly greater than (orderByVal, _id)
|
|
247
|
-
// asc + backward → strictly less than (orderByVal, _id)
|
|
248
|
-
// desc + forward → strictly less than
|
|
249
|
-
// desc + backward → strictly greater than
|
|
250
|
-
// We always SELECT in the direction that matches forward (so the
|
|
251
|
-
// result rows arrive in the right reading order), then if
|
|
252
|
-
// backward we reverse client-side at the end.
|
|
253
|
-
var effectiveAsc = (direction === "asc") === forward; // XNOR
|
|
254
|
-
var compareOp;
|
|
322
|
+
// Apply the keyset WHERE if a cursor is present.
|
|
255
323
|
if (cursorState) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
324
|
+
var cursorVals;
|
|
325
|
+
if (Array.isArray(cursorState.vals)) {
|
|
326
|
+
cursorVals = cursorState.vals;
|
|
327
|
+
} else {
|
|
328
|
+
// v0.6.20 single-column shape — synthesize the vals array.
|
|
329
|
+
cursorVals = [cursorState.orderByVal, cursorState.id];
|
|
330
|
+
// Drop the synthetic _id append if the legacy cursor already had it
|
|
331
|
+
if (orderEntries.length === 1) cursorVals = [cursorState.orderByVal];
|
|
332
|
+
}
|
|
333
|
+
if (cursorVals.length !== orderEntries.length) {
|
|
334
|
+
throw new PaginationError("pagination/cursor-mismatch",
|
|
335
|
+
"cursor encoded " + cursorVals.length + " column value(s) but orderBy has " +
|
|
336
|
+
orderEntries.length + " — operator changed the orderBy spec mid-flight");
|
|
337
|
+
}
|
|
338
|
+
var where = _buildKeysetWhere(orderEntries, cursorVals, forward);
|
|
339
|
+
query.whereRaw(where.sql, where.params);
|
|
264
340
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// No raw orderBy needed because the WHERE condition above
|
|
274
|
-
// strictly disambiguates (orderByVal, _id) tuples — successive
|
|
275
|
-
// pages can't repeat or skip a row even with ties on orderBy.
|
|
341
|
+
|
|
342
|
+
// Apply ORDER BY for each entry. When forward=false we reverse direction
|
|
343
|
+
// per entry so the SQL returns rows in the right reading order; we then
|
|
344
|
+
// reverse client-side at the end.
|
|
345
|
+
for (var oi = 0; oi < orderEntries.length; oi++) {
|
|
346
|
+
var entry = orderEntries[oi];
|
|
347
|
+
var effectiveDir = ((entry.direction === "asc") === forward) ? "asc" : "desc";
|
|
348
|
+
query.orderBy(entry.column, effectiveDir);
|
|
276
349
|
}
|
|
277
350
|
query.limit(limit + 1);
|
|
278
351
|
|
|
279
352
|
var rows = await Promise.resolve(query.all());
|
|
280
353
|
|
|
281
|
-
// Tiebreaker stability: when orderBy != _id, the SQL only sorts by
|
|
282
|
-
// orderBy. Within an orderByVal cluster, sort by _id in JS so the
|
|
283
|
-
// cursor predicate's _id-based tiebreaker stays consistent with the
|
|
284
|
-
// returned ordering.
|
|
285
|
-
if (orderBy !== "_id") {
|
|
286
|
-
rows.sort(function (a, b) {
|
|
287
|
-
var av = a[orderBy], bv = b[orderBy];
|
|
288
|
-
if (av < bv) return effectiveAsc ? -1 : 1;
|
|
289
|
-
if (av > bv) return effectiveAsc ? 1 : -1;
|
|
290
|
-
var ai = String(a._id), bi = String(b._id);
|
|
291
|
-
if (ai < bi) return effectiveAsc ? -1 : 1;
|
|
292
|
-
if (ai > bi) return effectiveAsc ? 1 : -1;
|
|
293
|
-
return 0;
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
354
|
var hasMore = rows.length > limit;
|
|
298
355
|
var page = hasMore ? rows.slice(0, limit) : rows.slice();
|
|
299
356
|
if (!forward) page.reverse();
|
|
300
357
|
|
|
358
|
+
function _valsForRow(row) {
|
|
359
|
+
return orderEntries.map(function (e) {
|
|
360
|
+
return e.column === "_id" ? String(row._id) : row[e.column];
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
301
364
|
var nextCursor = null;
|
|
302
365
|
var prevCursor = null;
|
|
303
366
|
if (hasMore && page.length > 0) {
|
|
304
367
|
var last = page[page.length - 1];
|
|
305
368
|
nextCursor = encodeCursor({
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
forward:
|
|
369
|
+
orderKey: orderKey,
|
|
370
|
+
vals: _valsForRow(last),
|
|
371
|
+
forward: true,
|
|
309
372
|
}, opts.secret);
|
|
310
373
|
}
|
|
311
|
-
// Always emit a prev cursor when we have a starting position (the
|
|
312
|
-
// operator was on a non-first page). Operator UI hides it on the
|
|
313
|
-
// first page.
|
|
314
374
|
if (cursorState && page.length > 0) {
|
|
315
375
|
var first = page[0];
|
|
316
376
|
prevCursor = encodeCursor({
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
forward:
|
|
377
|
+
orderKey: orderKey,
|
|
378
|
+
vals: _valsForRow(first),
|
|
379
|
+
forward: false,
|
|
320
380
|
}, opts.secret);
|
|
321
381
|
}
|
|
322
382
|
|