@blamejs/core 0.8.43 → 0.8.50
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 +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.network.byteQuota — per-key rolling 24-hour byte budget primitive.
|
|
4
|
+
*
|
|
5
|
+
* Provides a callable preflight + record surface used by handlers that
|
|
6
|
+
* already know the byte cost of an operation BEFORE accepting it (e.g.
|
|
7
|
+
* a multipart upload whose Content-Length is known at headers-parsed
|
|
8
|
+
* time, an SSE feed whose payload size is computed by the originator,
|
|
9
|
+
* a webhook whose body size is asserted via signed manifest):
|
|
10
|
+
*
|
|
11
|
+
* var quota = b.network.byteQuota.create({
|
|
12
|
+
* bytesPerDay: b.constants.BYTES.gib(2),
|
|
13
|
+
* audit: b.audit,
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Preflight — returns { allowed, remaining, total, quota } without
|
|
17
|
+
* // mutating the counter. Refusal emits network.byte_quota.exceeded.
|
|
18
|
+
* var verdict = await quota.check(req.ip, fileSize);
|
|
19
|
+
* if (!verdict.allowed) return res.writeHead(429).end();
|
|
20
|
+
*
|
|
21
|
+
* // Commit — mutates the counter for the rolling-window slot.
|
|
22
|
+
* await quota.record(req.ip, fileSize);
|
|
23
|
+
*
|
|
24
|
+
* // Operator helpers
|
|
25
|
+
* await quota.reset(req.ip);
|
|
26
|
+
* var snap = await quota.snapshot(); // [{ key, total, quota, remaining }]
|
|
27
|
+
*
|
|
28
|
+
* The middleware in lib/middleware/daily-byte-quota.js composes this
|
|
29
|
+
* primitive — there's no parallel byte-counter store.
|
|
30
|
+
*
|
|
31
|
+
* Failure modes:
|
|
32
|
+
* - cache backend unreachable → fail-open on check (verdict.allowed
|
|
33
|
+
* true with verdict.degraded = true) so a flaky cache can't take
|
|
34
|
+
* down the framework; record swallows the error after audit so the
|
|
35
|
+
* handler that already accepted the bytes isn't punished. Both
|
|
36
|
+
* paths emit network.byte_quota.backend_error.
|
|
37
|
+
* - bytesPerDay <= 0 / non-finite at create() throws. Per-call byte
|
|
38
|
+
* counts <0 / non-finite at check/record throw NetworkError.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
var C = require("./constants");
|
|
42
|
+
var defineClass = require("./framework-error").defineClass;
|
|
43
|
+
var lazyRequire = require("./lazy-require");
|
|
44
|
+
var validateOpts = require("./validate-opts");
|
|
45
|
+
|
|
46
|
+
var auditFwk = lazyRequire(function () { return require("./audit"); });
|
|
47
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
48
|
+
|
|
49
|
+
var ByteQuotaError = defineClass("ByteQuotaError", { alwaysPermanent: true });
|
|
50
|
+
|
|
51
|
+
var BINS_PER_DAY = 24; // allow:raw-byte-literal — 24 hours in a day
|
|
52
|
+
var BIN_MS = C.TIME.hours(1);
|
|
53
|
+
|
|
54
|
+
function _hourBin(nowMs) { return Math.floor(nowMs / BIN_MS); }
|
|
55
|
+
function _newEntry() { return { bins: new Array(BINS_PER_DAY).fill(0), startHour: 0 }; }
|
|
56
|
+
|
|
57
|
+
// Shared sliding-window helper — both backends call this so the per-bin
|
|
58
|
+
// shift / zero / total math lives in one place. Caller persists the
|
|
59
|
+
// returned entry when it's shared state (cache backend writes back).
|
|
60
|
+
function _slideAndSum(entry, nowHour) {
|
|
61
|
+
if (entry.startHour === 0) entry.startHour = nowHour - (BINS_PER_DAY - 1);
|
|
62
|
+
var advance = nowHour - (entry.startHour + (BINS_PER_DAY - 1));
|
|
63
|
+
var moved = false;
|
|
64
|
+
if (advance > 0) {
|
|
65
|
+
moved = true;
|
|
66
|
+
if (advance >= BINS_PER_DAY) {
|
|
67
|
+
for (var i = 0; i < BINS_PER_DAY; i++) entry.bins[i] = 0;
|
|
68
|
+
} else {
|
|
69
|
+
for (var j = 0; j < BINS_PER_DAY - advance; j++) entry.bins[j] = entry.bins[j + advance];
|
|
70
|
+
for (var k = BINS_PER_DAY - advance; k < BINS_PER_DAY; k++) entry.bins[k] = 0;
|
|
71
|
+
}
|
|
72
|
+
entry.startHour = nowHour - (BINS_PER_DAY - 1);
|
|
73
|
+
}
|
|
74
|
+
var total = 0;
|
|
75
|
+
for (var t = 0; t < BINS_PER_DAY; t++) total += entry.bins[t];
|
|
76
|
+
return { entry: entry, total: total, moved: moved };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _memoryBackend() {
|
|
80
|
+
var store = new Map();
|
|
81
|
+
function _get(key) {
|
|
82
|
+
var entry = store.get(key);
|
|
83
|
+
if (!entry) { entry = _newEntry(); store.set(key, entry); }
|
|
84
|
+
return entry;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
async total(key, nowMs) {
|
|
88
|
+
return _slideAndSum(_get(key), _hourBin(nowMs)).total;
|
|
89
|
+
},
|
|
90
|
+
async account(key, bytes, nowMs) {
|
|
91
|
+
var slid = _slideAndSum(_get(key), _hourBin(nowMs));
|
|
92
|
+
slid.entry.bins[BINS_PER_DAY - 1] += bytes;
|
|
93
|
+
},
|
|
94
|
+
async reset(key) {
|
|
95
|
+
store.delete(key);
|
|
96
|
+
},
|
|
97
|
+
async snapshot(nowMs) {
|
|
98
|
+
var nowHour = _hourBin(nowMs);
|
|
99
|
+
var out = [];
|
|
100
|
+
for (var key of store.keys()) {
|
|
101
|
+
var slid = _slideAndSum(_get(key), nowHour);
|
|
102
|
+
out.push({ key: key, total: slid.total });
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
},
|
|
106
|
+
_resetForTest: function () { store.clear(); },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _cacheBackend(cache) {
|
|
111
|
+
function _key(k) { return "byteQuota:" + k; }
|
|
112
|
+
async function _read(key) {
|
|
113
|
+
var raw = await cache.get(_key(key));
|
|
114
|
+
return raw && typeof raw === "object" && Array.isArray(raw.bins) ? raw : _newEntry();
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
async total(key, nowMs) {
|
|
118
|
+
var entry = await _read(key);
|
|
119
|
+
var slid = _slideAndSum(entry, _hourBin(nowMs));
|
|
120
|
+
if (slid.moved) await cache.set(_key(key), slid.entry, { ttlMs: BIN_MS * BINS_PER_DAY });
|
|
121
|
+
return slid.total;
|
|
122
|
+
},
|
|
123
|
+
async account(key, bytes, nowMs) {
|
|
124
|
+
var entry = await _read(key);
|
|
125
|
+
var slid = _slideAndSum(entry, _hourBin(nowMs));
|
|
126
|
+
slid.entry.bins[BINS_PER_DAY - 1] += bytes;
|
|
127
|
+
await cache.set(_key(key), slid.entry, { ttlMs: BIN_MS * BINS_PER_DAY });
|
|
128
|
+
},
|
|
129
|
+
async reset(key) {
|
|
130
|
+
if (typeof cache.delete === "function") await cache.delete(_key(key));
|
|
131
|
+
else if (typeof cache.del === "function") await cache.del(_key(key));
|
|
132
|
+
else await cache.set(_key(key), _newEntry(), { ttlMs: 1 });
|
|
133
|
+
},
|
|
134
|
+
// Cache backends don't enumerate by prefix portably — snapshot()
|
|
135
|
+
// returns an empty list when wired with a cache backend. Operators
|
|
136
|
+
// that need cluster-wide enumeration query the cache directly with
|
|
137
|
+
// their backend's idiomatic scan op.
|
|
138
|
+
async snapshot(_nowMs) { return []; },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _requirePositiveBytes(name, value) {
|
|
143
|
+
if (typeof value !== "number" || !isFinite(value) || value <= 0) {
|
|
144
|
+
throw new ByteQuotaError(
|
|
145
|
+
"byte-quota/bad-quota",
|
|
146
|
+
"network.byteQuota: " + name + " must be a positive finite number; " +
|
|
147
|
+
"use b.constants.BYTES.gib(N) / mib(N) for readable values"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _requireNonNegativeBytes(name, value) {
|
|
153
|
+
if (typeof value !== "number" || !isFinite(value) || value < 0) {
|
|
154
|
+
throw new ByteQuotaError(
|
|
155
|
+
"byte-quota/bad-bytes",
|
|
156
|
+
"network.byteQuota: " + name + " must be a non-negative finite number, got " + JSON.stringify(value)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _requireKey(key) {
|
|
162
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
163
|
+
throw new ByteQuotaError(
|
|
164
|
+
"byte-quota/bad-key",
|
|
165
|
+
"network.byteQuota: key must be a non-empty string, got " + JSON.stringify(key)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function create(opts) {
|
|
171
|
+
opts = opts || {};
|
|
172
|
+
validateOpts(opts, ["bytesPerDay", "cache", "audit", "now"], "network.byteQuota");
|
|
173
|
+
_requirePositiveBytes("bytesPerDay", opts.bytesPerDay);
|
|
174
|
+
var bytesPerDay = opts.bytesPerDay;
|
|
175
|
+
var auditOn = opts.audit !== false;
|
|
176
|
+
var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
|
|
177
|
+
var backend = opts.cache && typeof opts.cache.get === "function"
|
|
178
|
+
? _cacheBackend(opts.cache)
|
|
179
|
+
: _memoryBackend();
|
|
180
|
+
|
|
181
|
+
function _emitAudit(action, outcome, metadata) {
|
|
182
|
+
if (!auditOn) return;
|
|
183
|
+
try {
|
|
184
|
+
auditFwk().safeEmit({
|
|
185
|
+
action: "network.byte_quota." + action,
|
|
186
|
+
outcome: outcome,
|
|
187
|
+
metadata: metadata || {},
|
|
188
|
+
});
|
|
189
|
+
} catch (_e) { /* drop-silent — audit is best-effort */ }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function _emitMetric(verb, n, labels) {
|
|
193
|
+
try { observability().safeEvent("network.byte_quota." + verb, n || 1, labels || {}); }
|
|
194
|
+
catch (_e) { /* drop-silent */ }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// check(key, bytes) — preflight without mutation. Returns
|
|
198
|
+
// { allowed, total, remaining, quota, retryAfterSec, degraded }
|
|
199
|
+
// `degraded: true` indicates a backend error caused the verdict to
|
|
200
|
+
// fail-open; operators that want fail-closed inspect this flag.
|
|
201
|
+
async function check(key, bytes) {
|
|
202
|
+
_requireKey(key);
|
|
203
|
+
_requireNonNegativeBytes("bytes", bytes);
|
|
204
|
+
var nowMs = now();
|
|
205
|
+
var total;
|
|
206
|
+
try { total = await backend.total(key, nowMs); }
|
|
207
|
+
catch (e) {
|
|
208
|
+
_emitAudit("backend_error", "failure", { phase: "check", error: (e && e.message) || String(e) });
|
|
209
|
+
return {
|
|
210
|
+
allowed: true,
|
|
211
|
+
total: 0,
|
|
212
|
+
remaining: bytesPerDay,
|
|
213
|
+
quota: bytesPerDay,
|
|
214
|
+
retryAfterSec: 0,
|
|
215
|
+
degraded: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
var projected = total + bytes;
|
|
219
|
+
var remaining = Math.max(0, bytesPerDay - total);
|
|
220
|
+
if (projected > bytesPerDay) {
|
|
221
|
+
_emitMetric("refused", 1, { reason: "quota-exceeded" });
|
|
222
|
+
_emitAudit("exceeded", "denied", { key: key, total: total, requested: bytes, quota: bytesPerDay });
|
|
223
|
+
return {
|
|
224
|
+
allowed: false,
|
|
225
|
+
total: total,
|
|
226
|
+
remaining: remaining,
|
|
227
|
+
quota: bytesPerDay,
|
|
228
|
+
retryAfterSec: Math.ceil(BIN_MS / C.TIME.seconds(1)),
|
|
229
|
+
degraded: false,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
allowed: true,
|
|
234
|
+
total: total,
|
|
235
|
+
remaining: Math.max(0, bytesPerDay - projected),
|
|
236
|
+
quota: bytesPerDay,
|
|
237
|
+
retryAfterSec: 0,
|
|
238
|
+
degraded: false,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// record(key, bytes) — commit the mutation. Used after the operation
|
|
243
|
+
// succeeded (or for in-flight middleware accounting via the rolling-
|
|
244
|
+
// counter middleware wrapper).
|
|
245
|
+
async function record(key, bytes) {
|
|
246
|
+
_requireKey(key);
|
|
247
|
+
_requireNonNegativeBytes("bytes", bytes);
|
|
248
|
+
if (bytes === 0) return;
|
|
249
|
+
var nowMs = now();
|
|
250
|
+
try { await backend.account(key, bytes, nowMs); }
|
|
251
|
+
catch (e) {
|
|
252
|
+
_emitAudit("backend_error", "failure", { phase: "record", key: key, bytes: bytes, error: (e && e.message) || String(e) });
|
|
253
|
+
// Drop-silent after audit — the operation already succeeded; the
|
|
254
|
+
// alternative throw would punish the handler that already accepted bytes.
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
_emitMetric("recorded", bytes, {});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function reset(key) {
|
|
261
|
+
_requireKey(key);
|
|
262
|
+
try { await backend.reset(key); }
|
|
263
|
+
catch (e) {
|
|
264
|
+
_emitAudit("backend_error", "failure", { phase: "reset", error: (e && e.message) || String(e) });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function snapshot() {
|
|
269
|
+
var nowMs = now();
|
|
270
|
+
try {
|
|
271
|
+
var rows = await backend.snapshot(nowMs);
|
|
272
|
+
return rows.map(function (r) {
|
|
273
|
+
return {
|
|
274
|
+
key: r.key,
|
|
275
|
+
total: r.total,
|
|
276
|
+
quota: bytesPerDay,
|
|
277
|
+
remaining: Math.max(0, bytesPerDay - r.total),
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
} catch (e) {
|
|
281
|
+
_emitAudit("backend_error", "failure", { phase: "snapshot", error: (e && e.message) || String(e) });
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
check: check,
|
|
288
|
+
record: record,
|
|
289
|
+
reset: reset,
|
|
290
|
+
snapshot: snapshot,
|
|
291
|
+
// Internals exposed for the middleware composition seam — same
|
|
292
|
+
// backend instance can serve both APIs (so middleware account()
|
|
293
|
+
// and standalone record() agree on the counter state).
|
|
294
|
+
_backend: backend,
|
|
295
|
+
_bytesPerDay: bytesPerDay,
|
|
296
|
+
_now: now,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
create: create,
|
|
302
|
+
ByteQuotaError: ByteQuotaError,
|
|
303
|
+
BINS_PER_DAY: BINS_PER_DAY,
|
|
304
|
+
// Internals exposed for tests + the middleware composition seam.
|
|
305
|
+
_memoryBackend: _memoryBackend,
|
|
306
|
+
_cacheBackend: _cacheBackend,
|
|
307
|
+
_slideAndSum: _slideAndSum,
|
|
308
|
+
};
|
package/lib/network-heartbeat.js
CHANGED
|
@@ -18,6 +18,13 @@ var DEFAULT_INTERVAL_MS = C.TIME.seconds(15);
|
|
|
18
18
|
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(5);
|
|
19
19
|
var DEFAULT_THRESHOLD = 3;
|
|
20
20
|
|
|
21
|
+
// Passive heartbeats — caller (typically a WebSocket / SSE / long-poll
|
|
22
|
+
// handler) records each inbound ping/pong and the framework fires
|
|
23
|
+
// `onTimeout` once when the keepalive grace window elapses without a
|
|
24
|
+
// recordPong call. Distinct from `start()`, which runs an active
|
|
25
|
+
// outbound probe loop.
|
|
26
|
+
var DEFAULT_PASSIVE_TIMEOUT_MS = C.TIME.seconds(90);
|
|
27
|
+
|
|
21
28
|
var TARGETS = new Map();
|
|
22
29
|
|
|
23
30
|
function _validateTarget(t, idx) {
|
|
@@ -275,6 +282,133 @@ function _emitAuditStateChange(entry, prevState) {
|
|
|
275
282
|
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
276
283
|
}
|
|
277
284
|
|
|
285
|
+
// b.network.heartbeat.passive(opts) — passive (server-pushes-pings)
|
|
286
|
+
// keepalive watchdog. Caller invokes the returned `recordPong()` each
|
|
287
|
+
// time a heartbeat frame arrives from the peer; if `timeoutMs` elapses
|
|
288
|
+
// with no `recordPong`, the watchdog fires `onTimeout()` exactly once
|
|
289
|
+
// and stops. Operator restarts surveillance by calling `passive()`
|
|
290
|
+
// again — the primitive deliberately doesn't auto-rearm because the
|
|
291
|
+
// post-timeout strategy (close socket, re-handshake, retry, alert) is
|
|
292
|
+
// caller-specific.
|
|
293
|
+
//
|
|
294
|
+
// Returns:
|
|
295
|
+
// { recordPong, stop }
|
|
296
|
+
//
|
|
297
|
+
// `onPong` is the per-pong observability hook (optional). `onTimeout`
|
|
298
|
+
// is the callback fired when the timeout elapses (required). Both
|
|
299
|
+
// callbacks are invoked outside try/catch — operator callbacks throw
|
|
300
|
+
// only if the operator wants the host process to crash.
|
|
301
|
+
function passive(opts) {
|
|
302
|
+
opts = opts || {};
|
|
303
|
+
validateOpts(opts, ["onPong", "timeoutMs", "onTimeout"], "heartbeat.passive");
|
|
304
|
+
if (typeof opts.onTimeout !== "function") {
|
|
305
|
+
throw new HeartbeatError("heartbeat/bad-on-timeout",
|
|
306
|
+
"heartbeat.passive: onTimeout must be a function");
|
|
307
|
+
}
|
|
308
|
+
validateOpts.optionalFunction(opts.onPong, "heartbeat.passive: onPong",
|
|
309
|
+
HeartbeatError, "heartbeat/bad-on-pong");
|
|
310
|
+
var timeoutMs = opts.timeoutMs === undefined ? DEFAULT_PASSIVE_TIMEOUT_MS : opts.timeoutMs;
|
|
311
|
+
if (typeof timeoutMs !== "number" || !isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
312
|
+
throw new HeartbeatError("heartbeat/bad-timeout",
|
|
313
|
+
"heartbeat.passive: timeoutMs must be a positive finite number");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
var state = {
|
|
317
|
+
timer: null,
|
|
318
|
+
stopped: false,
|
|
319
|
+
timedOut: false,
|
|
320
|
+
startMs: Date.now(),
|
|
321
|
+
lastPongMs: null,
|
|
322
|
+
pongCount: 0,
|
|
323
|
+
onPong: opts.onPong || null,
|
|
324
|
+
onTimeout: opts.onTimeout,
|
|
325
|
+
timeoutMs: timeoutMs,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
function _arm() {
|
|
329
|
+
state.timer = setTimeout(_fire, state.timeoutMs);
|
|
330
|
+
if (state.timer && typeof state.timer.unref === "function") state.timer.unref();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function _fire() {
|
|
334
|
+
if (state.stopped || state.timedOut) return;
|
|
335
|
+
state.timedOut = true;
|
|
336
|
+
state.stopped = true;
|
|
337
|
+
state.timer = null;
|
|
338
|
+
_emitObsTimeout(state);
|
|
339
|
+
_emitAuditPassiveTimeout(state);
|
|
340
|
+
try { state.onTimeout({ pongCount: state.pongCount, lastPongMs: state.lastPongMs, timeoutMs: state.timeoutMs }); }
|
|
341
|
+
catch (_e) { /* operator callback best-effort */ }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function recordPong() {
|
|
345
|
+
if (state.stopped || state.timedOut) return false;
|
|
346
|
+
state.pongCount += 1;
|
|
347
|
+
state.lastPongMs = Date.now();
|
|
348
|
+
if (state.timer) {
|
|
349
|
+
try { clearTimeout(state.timer); } catch (_e) { /* best-effort timer teardown */ }
|
|
350
|
+
state.timer = null;
|
|
351
|
+
}
|
|
352
|
+
_emitObsPong(state);
|
|
353
|
+
if (typeof state.onPong === "function") {
|
|
354
|
+
try { state.onPong({ pongCount: state.pongCount, lastPongMs: state.lastPongMs }); }
|
|
355
|
+
catch (_e) { /* operator callback best-effort */ }
|
|
356
|
+
}
|
|
357
|
+
_arm();
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function stop() {
|
|
362
|
+
if (state.stopped) return false;
|
|
363
|
+
state.stopped = true;
|
|
364
|
+
if (state.timer) {
|
|
365
|
+
try { clearTimeout(state.timer); } catch (_e) { /* best-effort timer teardown */ }
|
|
366
|
+
state.timer = null;
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_arm();
|
|
372
|
+
return { recordPong: recordPong, stop: stop };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function _emitObsPong(state) {
|
|
376
|
+
try {
|
|
377
|
+
observability().emit("network.heartbeat.passive.pong", {
|
|
378
|
+
pongCount: state.pongCount,
|
|
379
|
+
timeoutMs: state.timeoutMs,
|
|
380
|
+
});
|
|
381
|
+
} catch (_e) { /* obs best-effort */ }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function _emitObsTimeout(state) {
|
|
385
|
+
try {
|
|
386
|
+
observability().emit("network.heartbeat.passive.timeout", {
|
|
387
|
+
pongCount: state.pongCount,
|
|
388
|
+
lastPongMs: state.lastPongMs,
|
|
389
|
+
timeoutMs: state.timeoutMs,
|
|
390
|
+
});
|
|
391
|
+
} catch (_e) { /* obs best-effort */ }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function _emitAuditPassiveTimeout(state) {
|
|
395
|
+
var sink;
|
|
396
|
+
try { sink = audit(); } catch (_e) { return; }
|
|
397
|
+
if (!sink || typeof sink.safeEmit !== "function") return;
|
|
398
|
+
try {
|
|
399
|
+
sink.safeEmit({
|
|
400
|
+
action: "networkheartbeat.passive.timeout",
|
|
401
|
+
outcome: "failure",
|
|
402
|
+
metadata: {
|
|
403
|
+
pongCount: state.pongCount,
|
|
404
|
+
lastPongMs: state.lastPongMs,
|
|
405
|
+
timeoutMs: state.timeoutMs,
|
|
406
|
+
startMs: state.startMs,
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
410
|
+
}
|
|
411
|
+
|
|
278
412
|
function _resetForTest() {
|
|
279
413
|
stopAll();
|
|
280
414
|
}
|
|
@@ -285,6 +419,7 @@ module.exports = {
|
|
|
285
419
|
stopAll: stopAll,
|
|
286
420
|
status: status,
|
|
287
421
|
statuses: statuses,
|
|
422
|
+
passive: passive,
|
|
288
423
|
HeartbeatError: HeartbeatError,
|
|
289
424
|
_resetForTest: _resetForTest,
|
|
290
425
|
};
|