@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
package/lib/sandbox.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.sandbox - isolation harness for operator-supplied transforms.
|
|
4
|
+
*
|
|
5
|
+
* Some primitives (b.template with sandbox: true, custom audit-export
|
|
6
|
+
* formatters, response-shape rewriters, ETL hooks) need to run JS
|
|
7
|
+
* source the operator wrote against per-request input. In-process eval
|
|
8
|
+
* gives that source the framework full module graph: filesystem,
|
|
9
|
+
* network, process, child_process, the entire b.* surface, vault keys,
|
|
10
|
+
* and audit-bypass via direct DB writes. b.sandbox runs the source
|
|
11
|
+
* inside a fresh worker_threads.Worker with strict resource limits and
|
|
12
|
+
* a hand-built scope that exposes ONLY the globals the operator
|
|
13
|
+
* allowlists at create() time.
|
|
14
|
+
*
|
|
15
|
+
* var result = await b.sandbox.run({
|
|
16
|
+
* source: "return { upper: input.name.toUpperCase() };",
|
|
17
|
+
* input: { name: "alice" },
|
|
18
|
+
* timeoutMs: 250,
|
|
19
|
+
* maxBytes: C.BYTES.mib(8),
|
|
20
|
+
* allowed: ["JSON", "Math", "Date"],
|
|
21
|
+
* });
|
|
22
|
+
* // result -> { result: { upper: "ALICE" }, runtimeMs: 12, peakBytes: 4194304 }
|
|
23
|
+
*
|
|
24
|
+
* Default-deny posture (lib/sandbox-worker.js docstring has the full list):
|
|
25
|
+
* - No require / process / Buffer / setTimeout / setImmediate /
|
|
26
|
+
* setInterval / queueMicrotask / global. The bootstrap deletes
|
|
27
|
+
* each off globalThis BEFORE compiling operator source.
|
|
28
|
+
* - No filesystem / network / child_process / spawn / dns -
|
|
29
|
+
* unreachable once require is gone.
|
|
30
|
+
* - No worker re-entry - worker_threads itself unreachable.
|
|
31
|
+
* - Timeout (default: 250ms, max: 10s) terminates the worker.
|
|
32
|
+
* - Heap caps (maxOldGenerationSizeMb / maxYoungGenerationSizeMb)
|
|
33
|
+
* derived from maxBytes; v8 kills the worker on overflow.
|
|
34
|
+
* - Result size cap = maxBytes / 4.
|
|
35
|
+
*
|
|
36
|
+
* Allowed-globals list:
|
|
37
|
+
* The allowed opt names which extra globals operator source may
|
|
38
|
+
* reference. The list is intersected against KNOWN_SAFE_BUILTINS at
|
|
39
|
+
* the host before being shipped to the worker - anything outside
|
|
40
|
+
* the allowlist refuses at the call site. JS-language primitives
|
|
41
|
+
* (Object, Array, String, Number, Boolean, Symbol, Promise, Error,
|
|
42
|
+
* TypeError, RangeError, RegExp) survive regardless because they
|
|
43
|
+
* cannot be removed without breaking literal expressions.
|
|
44
|
+
*
|
|
45
|
+
* Composability with b.template:
|
|
46
|
+
* b.template.create({ sandbox: true }) routes operator-supplied
|
|
47
|
+
* helper-function bodies through b.sandbox before exposing them as
|
|
48
|
+
* helpers in the template scope. The template engine itself remains
|
|
49
|
+
* eval-free - sandbox is the secondary defense for the rare cases
|
|
50
|
+
* where an operator NEEDS to ship a transform alongside a template
|
|
51
|
+
* (date formatters with locale-dependent fallbacks, etc.).
|
|
52
|
+
*
|
|
53
|
+
* Audit shape:
|
|
54
|
+
* - sandbox.run - outcome=success; metadata: { runtimeMs, peakBytes, sourceBytes }
|
|
55
|
+
* - sandbox.run.refused - outcome=failure; metadata: { reason, runtimeMs, peakBytes, sourceBytes }
|
|
56
|
+
*
|
|
57
|
+
* Failure modes (every one throws SandboxError):
|
|
58
|
+
* - sandbox/bad-opts - unknown opts key
|
|
59
|
+
* - sandbox/bad-source - source is not a non-empty string
|
|
60
|
+
* - sandbox/bad-allowed - allowed contains non-string or non-allowlisted name
|
|
61
|
+
* - sandbox/bad-timeout - timeoutMs is not a positive finite int (or > MAX_TIMEOUT_MS)
|
|
62
|
+
* - sandbox/bad-max-bytes - maxBytes is not a positive finite int (or out of range)
|
|
63
|
+
* - sandbox/bad-input - input is not JSON-serializable
|
|
64
|
+
* - sandbox/input-too-large - JSON.stringify(input).length > maxBytes
|
|
65
|
+
* - sandbox/timeout - worker exceeded timeoutMs
|
|
66
|
+
* - sandbox/oversized-result - worker output > maxBytes / 4
|
|
67
|
+
* - sandbox/parse-error - source did not parse inside the worker
|
|
68
|
+
* - sandbox/runtime-error - operator transform threw
|
|
69
|
+
* - sandbox/spawn-failed - worker thread failed to spawn
|
|
70
|
+
* - sandbox/worker-error - worker thread errored after spawn
|
|
71
|
+
* - sandbox/worker-nonzero-exit - worker died (heap-cap kill class)
|
|
72
|
+
* - sandbox/no-result - worker exited without posting (heap-cap class)
|
|
73
|
+
* - sandbox/no-worker-threads - runtime lacks node:worker_threads
|
|
74
|
+
*
|
|
75
|
+
* Operators feeding untrusted source MUST also pair this with their
|
|
76
|
+
* own posture (operator-uploaded transforms only after a code-review
|
|
77
|
+
* gate, etc.) - sandbox is one defense layer, not a license to accept
|
|
78
|
+
* arbitrary source from the public internet.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
var path = require("path");
|
|
82
|
+
var lazyRequire = require("./lazy-require");
|
|
83
|
+
var validateOpts = require("./validate-opts");
|
|
84
|
+
var numericBounds = require("./numeric-bounds");
|
|
85
|
+
var constants = require("./constants");
|
|
86
|
+
var { SandboxError } = require("./framework-error");
|
|
87
|
+
|
|
88
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
89
|
+
|
|
90
|
+
// Built-in allowlist for the allowed opt. Filesystem / network /
|
|
91
|
+
// process / require are deliberately absent. JS-language primitives
|
|
92
|
+
// stay reachable inside the worker regardless of this list.
|
|
93
|
+
var KNOWN_SAFE_BUILTINS = Object.freeze({
|
|
94
|
+
JSON: true, Math: true, Date: true,
|
|
95
|
+
Map: true, Set: true, WeakMap: true, WeakSet: true,
|
|
96
|
+
RegExp: true, Error: true, TypeError: true, RangeError: true,
|
|
97
|
+
Number: true, String: true, Boolean: true,
|
|
98
|
+
Array: true, Object: true, ArrayBuffer: true,
|
|
99
|
+
Uint8Array: true, Uint16Array: true, Uint32Array: true,
|
|
100
|
+
Int8Array: true, Int16Array: true, Int32Array: true,
|
|
101
|
+
Float32Array: true, Float64Array: true,
|
|
102
|
+
DataView: true, Symbol: true, Promise: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// JS-language primitives that survive regardless of allowlist -
|
|
106
|
+
// stripping these would mean operator transforms cannot evaluate
|
|
107
|
+
// even simple literals. Mirrors lib/sandbox-worker.js.
|
|
108
|
+
var ALWAYS_AVAILABLE = Object.freeze([
|
|
109
|
+
"Object", "Array", "String", "Number", "Boolean", "Symbol",
|
|
110
|
+
"Promise", "Error", "TypeError", "RangeError", "RegExp",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
var WORKER_PATH = path.resolve(__dirname, "sandbox-worker.js");
|
|
114
|
+
|
|
115
|
+
// Default caps. Sourced from C.* helpers so the unit lives at the call site.
|
|
116
|
+
var DEFAULT_TIMEOUT_MS = 250;
|
|
117
|
+
var MAX_TIMEOUT_MS = constants.TIME.seconds(10);
|
|
118
|
+
var DEFAULT_MAX_BYTES = constants.BYTES.mib(64);
|
|
119
|
+
var MAX_MAX_BYTES = constants.BYTES.gib(1);
|
|
120
|
+
var MIN_MAX_BYTES = constants.BYTES.mib(4);
|
|
121
|
+
|
|
122
|
+
function _validateAllowed(allowed) {
|
|
123
|
+
if (allowed === undefined || allowed === null) return [];
|
|
124
|
+
if (!Array.isArray(allowed)) {
|
|
125
|
+
throw new SandboxError("sandbox/bad-allowed",
|
|
126
|
+
"sandbox.run: opts.allowed must be an array of identifier strings");
|
|
127
|
+
}
|
|
128
|
+
var out = [];
|
|
129
|
+
for (var i = 0; i < allowed.length; i += 1) {
|
|
130
|
+
var name = allowed[i];
|
|
131
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
132
|
+
throw new SandboxError("sandbox/bad-allowed",
|
|
133
|
+
"sandbox.run: opts.allowed[" + i + "] must be a non-empty identifier string");
|
|
134
|
+
}
|
|
135
|
+
if (!KNOWN_SAFE_BUILTINS[name]) {
|
|
136
|
+
throw new SandboxError("sandbox/bad-allowed",
|
|
137
|
+
"sandbox.run: opts.allowed[" + i + "] = " + JSON.stringify(name) +
|
|
138
|
+
" is not in the sandbox built-in allowlist " +
|
|
139
|
+
"(known-safe: " + Object.keys(KNOWN_SAFE_BUILTINS).join(", ") + ")");
|
|
140
|
+
}
|
|
141
|
+
if (out.indexOf(name) === -1) out.push(name);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _emitAudit(action, outcome, metadata) {
|
|
147
|
+
try {
|
|
148
|
+
audit().safeEmit({
|
|
149
|
+
action: action,
|
|
150
|
+
outcome: outcome,
|
|
151
|
+
metadata: metadata,
|
|
152
|
+
});
|
|
153
|
+
} catch (_e) { /* drop-silent - audit best-effort */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function run(opts) {
|
|
157
|
+
opts = opts || {};
|
|
158
|
+
try {
|
|
159
|
+
validateOpts(opts, ["source", "input", "timeoutMs", "maxBytes", "allowed"], "sandbox.run");
|
|
160
|
+
} catch (e) { return Promise.reject(new SandboxError("sandbox/bad-opts", e.message)); }
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
validateOpts.requireNonEmptyString(opts.source,
|
|
164
|
+
"sandbox.run: opts.source", SandboxError, "sandbox/bad-source");
|
|
165
|
+
} catch (e) { return Promise.reject(e); }
|
|
166
|
+
var sourceBytes = Buffer.byteLength(opts.source, "utf8");
|
|
167
|
+
|
|
168
|
+
var timeoutMs;
|
|
169
|
+
try {
|
|
170
|
+
timeoutMs = (opts.timeoutMs === undefined) ? DEFAULT_TIMEOUT_MS : opts.timeoutMs;
|
|
171
|
+
numericBounds.requirePositiveFiniteIntIfPresent(timeoutMs,
|
|
172
|
+
"sandbox.run: opts.timeoutMs", SandboxError, "sandbox/bad-timeout");
|
|
173
|
+
} catch (e) { return Promise.reject(e); }
|
|
174
|
+
if (timeoutMs > MAX_TIMEOUT_MS) {
|
|
175
|
+
return Promise.reject(new SandboxError("sandbox/bad-timeout",
|
|
176
|
+
"sandbox.run: opts.timeoutMs (" + timeoutMs + ") exceeds the framework cap of " + MAX_TIMEOUT_MS + " ms"));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var maxBytes;
|
|
180
|
+
try {
|
|
181
|
+
maxBytes = (opts.maxBytes === undefined) ? DEFAULT_MAX_BYTES : opts.maxBytes;
|
|
182
|
+
numericBounds.requirePositiveFiniteIntIfPresent(maxBytes,
|
|
183
|
+
"sandbox.run: opts.maxBytes", SandboxError, "sandbox/bad-max-bytes");
|
|
184
|
+
} catch (e) { return Promise.reject(e); }
|
|
185
|
+
if (maxBytes < MIN_MAX_BYTES) {
|
|
186
|
+
return Promise.reject(new SandboxError("sandbox/bad-max-bytes",
|
|
187
|
+
"sandbox.run: opts.maxBytes (" + maxBytes + ") below the framework floor of " + MIN_MAX_BYTES + " bytes"));
|
|
188
|
+
}
|
|
189
|
+
if (maxBytes > MAX_MAX_BYTES) {
|
|
190
|
+
return Promise.reject(new SandboxError("sandbox/bad-max-bytes",
|
|
191
|
+
"sandbox.run: opts.maxBytes (" + maxBytes + ") exceeds the framework cap of " + MAX_MAX_BYTES + " bytes"));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
var allowedGlobals;
|
|
195
|
+
try { allowedGlobals = _validateAllowed(opts.allowed); }
|
|
196
|
+
catch (e) { return Promise.reject(e); }
|
|
197
|
+
|
|
198
|
+
var inputJson;
|
|
199
|
+
try { inputJson = (opts.input === undefined) ? null : JSON.stringify(opts.input); }
|
|
200
|
+
catch (eSer) {
|
|
201
|
+
return Promise.reject(new SandboxError("sandbox/bad-input",
|
|
202
|
+
"sandbox.run: opts.input is not JSON-serializable: " + (eSer && eSer.message)));
|
|
203
|
+
}
|
|
204
|
+
if (inputJson !== null && inputJson.length > maxBytes) {
|
|
205
|
+
return Promise.reject(new SandboxError("sandbox/input-too-large",
|
|
206
|
+
"sandbox.run: opts.input serialized to " + inputJson.length + " bytes (>" + maxBytes + ")"));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var workerThreads;
|
|
210
|
+
try { workerThreads = require("node:worker_threads"); }
|
|
211
|
+
catch (_e) {
|
|
212
|
+
return Promise.reject(new SandboxError("sandbox/no-worker-threads",
|
|
213
|
+
"sandbox.run: node:worker_threads is unavailable in this runtime"));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// resourceLimits in MiB. Derive from maxBytes - keep a small headroom
|
|
217
|
+
// floor so the worker can boot. Round each cap down to a MiB integer.
|
|
218
|
+
// Floors / caps are quanta of MiB chosen to fit a small embedded
|
|
219
|
+
// worker; passed straight to v8's resourceLimits.
|
|
220
|
+
var oneMib = constants.BYTES.mib(1);
|
|
221
|
+
// The MiB-unit caps below are integers passed directly to v8's
|
|
222
|
+
// resourceLimits (already typed in MiB by the v8 API), not byte
|
|
223
|
+
// counts - the constants helpers don't apply.
|
|
224
|
+
var minHeapFloorMib = 64; // allow:raw-byte-literal — MiB unit count, not bytes
|
|
225
|
+
var youngGenCapMib = 32; // allow:raw-byte-literal — MiB unit count, not bytes
|
|
226
|
+
var youngGenFloorMib = 8; // allow:raw-byte-literal — MiB unit count, not bytes
|
|
227
|
+
var codeRangeCapMib = 16; // allow:raw-byte-literal — MiB unit count, not bytes
|
|
228
|
+
var codeRangeFloorMib = 8; // allow:raw-byte-literal — MiB unit count, not bytes
|
|
229
|
+
var stackMib = 4; // MiB unit count, not bytes
|
|
230
|
+
var heapMib = Math.max(minHeapFloorMib, Math.floor(maxBytes / oneMib));
|
|
231
|
+
var resourceLimits = {
|
|
232
|
+
maxOldGenerationSizeMb: heapMib,
|
|
233
|
+
maxYoungGenerationSizeMb: Math.max(youngGenFloorMib, Math.min(heapMib, youngGenCapMib)),
|
|
234
|
+
codeRangeSizeMb: Math.max(codeRangeFloorMib, Math.min(heapMib, codeRangeCapMib)),
|
|
235
|
+
stackSizeMb: stackMib,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Reserve 1/4 of maxBytes as the per-result hard cap. The worker
|
|
239
|
+
// refuses any result whose stringified form exceeds this.
|
|
240
|
+
var maxResultBytes = Math.floor(maxBytes / 4);
|
|
241
|
+
|
|
242
|
+
return new Promise(function (resolve, reject) {
|
|
243
|
+
var startedAt = Date.now();
|
|
244
|
+
var settled = false;
|
|
245
|
+
var worker;
|
|
246
|
+
try {
|
|
247
|
+
worker = new workerThreads.Worker(WORKER_PATH, {
|
|
248
|
+
workerData: {
|
|
249
|
+
source: opts.source,
|
|
250
|
+
input: opts.input,
|
|
251
|
+
allowedGlobals: allowedGlobals,
|
|
252
|
+
maxResultBytes: maxResultBytes,
|
|
253
|
+
},
|
|
254
|
+
resourceLimits: resourceLimits,
|
|
255
|
+
stdout: true,
|
|
256
|
+
stderr: true,
|
|
257
|
+
});
|
|
258
|
+
} catch (eSpawn) {
|
|
259
|
+
var spawnRuntimeMs = Date.now() - startedAt;
|
|
260
|
+
_emitAudit("sandbox.run.refused", "failure", {
|
|
261
|
+
reason: "sandbox/spawn-failed", runtimeMs: spawnRuntimeMs, peakBytes: 0, sourceBytes: sourceBytes,
|
|
262
|
+
});
|
|
263
|
+
reject(new SandboxError("sandbox/spawn-failed",
|
|
264
|
+
"sandbox.run: failed to spawn worker: " + (eSpawn && eSpawn.message)));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
var timer = setTimeout(function () {
|
|
269
|
+
if (settled) return;
|
|
270
|
+
settled = true;
|
|
271
|
+
try { worker.terminate(); } catch (_e) { /* terminate best-effort */ }
|
|
272
|
+
var elapsed = Date.now() - startedAt;
|
|
273
|
+
_emitAudit("sandbox.run.refused", "failure", {
|
|
274
|
+
reason: "sandbox/timeout", runtimeMs: elapsed, peakBytes: 0, sourceBytes: sourceBytes,
|
|
275
|
+
});
|
|
276
|
+
reject(new SandboxError("sandbox/timeout",
|
|
277
|
+
"sandbox.run: worker exceeded timeoutMs=" + timeoutMs + " (elapsed " + elapsed + "ms)"));
|
|
278
|
+
}, timeoutMs);
|
|
279
|
+
|
|
280
|
+
worker.on("message", function (msg) {
|
|
281
|
+
if (settled) return;
|
|
282
|
+
settled = true;
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
try { worker.terminate(); } catch (_e) { /* terminate best-effort */ }
|
|
285
|
+
if (!msg || typeof msg !== "object") {
|
|
286
|
+
_emitAudit("sandbox.run.refused", "failure", {
|
|
287
|
+
reason: "sandbox/bad-worker-message", runtimeMs: Date.now() - startedAt, peakBytes: 0, sourceBytes: sourceBytes,
|
|
288
|
+
});
|
|
289
|
+
return reject(new SandboxError("sandbox/bad-worker-message",
|
|
290
|
+
"sandbox.run: worker returned a non-object message"));
|
|
291
|
+
}
|
|
292
|
+
var runtimeMs = (typeof msg.runtimeMs === "number") ? msg.runtimeMs : (Date.now() - startedAt);
|
|
293
|
+
var peakBytes = (typeof msg.peakBytes === "number") ? msg.peakBytes : 0;
|
|
294
|
+
if (msg.ok) {
|
|
295
|
+
var parsed;
|
|
296
|
+
try { parsed = (msg.resultJson === undefined) ? undefined : JSON.parse(msg.resultJson); } // allow:bare-json-parse — resultJson is produced by lib/sandbox-worker.js via JSON.stringify and bounded by maxResultBytes; never directly from operator/network input
|
|
297
|
+
catch (eParse) {
|
|
298
|
+
_emitAudit("sandbox.run.refused", "failure", {
|
|
299
|
+
reason: "sandbox/bad-result-json", runtimeMs: runtimeMs, peakBytes: peakBytes, sourceBytes: sourceBytes,
|
|
300
|
+
});
|
|
301
|
+
return reject(new SandboxError("sandbox/bad-result-json",
|
|
302
|
+
"sandbox.run: worker result was not parseable JSON: " + (eParse && eParse.message)));
|
|
303
|
+
}
|
|
304
|
+
_emitAudit("sandbox.run", "success", {
|
|
305
|
+
runtimeMs: runtimeMs, peakBytes: peakBytes, sourceBytes: sourceBytes,
|
|
306
|
+
});
|
|
307
|
+
return resolve({ result: parsed, runtimeMs: runtimeMs, peakBytes: peakBytes });
|
|
308
|
+
}
|
|
309
|
+
_emitAudit("sandbox.run.refused", "failure", {
|
|
310
|
+
reason: msg.code || "sandbox/runtime-error", runtimeMs: runtimeMs, peakBytes: peakBytes, sourceBytes: sourceBytes,
|
|
311
|
+
});
|
|
312
|
+
return reject(new SandboxError(msg.code || "sandbox/runtime-error",
|
|
313
|
+
msg.message || "sandbox.run: worker reported a refusal"));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
worker.on("error", function (err) {
|
|
317
|
+
if (settled) return;
|
|
318
|
+
settled = true;
|
|
319
|
+
clearTimeout(timer);
|
|
320
|
+
var elapsed = Date.now() - startedAt;
|
|
321
|
+
_emitAudit("sandbox.run.refused", "failure", {
|
|
322
|
+
reason: "sandbox/worker-error", runtimeMs: elapsed, peakBytes: 0, sourceBytes: sourceBytes,
|
|
323
|
+
});
|
|
324
|
+
reject(new SandboxError("sandbox/worker-error",
|
|
325
|
+
"sandbox.run: worker errored: " + (err && err.message ? err.message : String(err))));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
worker.on("exit", function (code) {
|
|
329
|
+
if (settled) return;
|
|
330
|
+
settled = true;
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
var elapsed = Date.now() - startedAt;
|
|
333
|
+
// Code 0 with no message means the worker exited without posting -
|
|
334
|
+
// usually a heap-cap kill. Surface as oversized.
|
|
335
|
+
var reason = (code === 0) ? "sandbox/no-result" : "sandbox/worker-nonzero-exit";
|
|
336
|
+
var message = (code === 0)
|
|
337
|
+
? "sandbox.run: worker exited without posting a result (heap cap or premature return)"
|
|
338
|
+
: "sandbox.run: worker exited with code " + code + " (likely resource-limit kill)";
|
|
339
|
+
_emitAudit("sandbox.run.refused", "failure", {
|
|
340
|
+
reason: reason, runtimeMs: elapsed, peakBytes: 0, sourceBytes: sourceBytes,
|
|
341
|
+
});
|
|
342
|
+
reject(new SandboxError(reason, message));
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = {
|
|
348
|
+
run: run,
|
|
349
|
+
KNOWN_SAFE_BUILTINS: KNOWN_SAFE_BUILTINS,
|
|
350
|
+
ALWAYS_AVAILABLE: ALWAYS_AVAILABLE,
|
|
351
|
+
DEFAULT_TIMEOUT_MS: DEFAULT_TIMEOUT_MS,
|
|
352
|
+
MAX_TIMEOUT_MS: MAX_TIMEOUT_MS,
|
|
353
|
+
DEFAULT_MAX_BYTES: DEFAULT_MAX_BYTES,
|
|
354
|
+
MAX_MAX_BYTES: MAX_MAX_BYTES,
|
|
355
|
+
MIN_MAX_BYTES: MIN_MAX_BYTES,
|
|
356
|
+
WORKER_PATH: WORKER_PATH,
|
|
357
|
+
SandboxError: SandboxError,
|
|
358
|
+
};
|
package/lib/scheduler.js
CHANGED
|
@@ -1,76 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.scheduler
|
|
4
|
+
* @featured true
|
|
5
|
+
* @nav Production
|
|
6
|
+
* @title Scheduler
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Cron-style task scheduler with cluster leader gating, deduplicated
|
|
10
|
+
* ticks, drift correction, and an audit event on every tick.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* name: "nightly-cleanup",
|
|
17
|
-
* cron: "0 2 * * *", // POSIX 5-field cron
|
|
18
|
-
* timezone: "America/New_York", // IANA name; default = server-local
|
|
19
|
-
* job: "cleanup", // dispatched via jobs.enqueue
|
|
20
|
-
* payload: { scope: "all" },
|
|
21
|
-
* });
|
|
22
|
-
*
|
|
23
|
-
* sched.schedule({
|
|
24
|
-
* name: "stats-aggregation",
|
|
25
|
-
* every: 300000, // ms between runs
|
|
26
|
-
* baseline: "00:00", // HH:MM anchor (optional)
|
|
27
|
-
* timezone: "America/New_York",
|
|
28
|
-
* job: "aggregate-stats",
|
|
29
|
-
* });
|
|
30
|
-
*
|
|
31
|
-
* sched.schedule({
|
|
32
|
-
* name: "heartbeat",
|
|
33
|
-
* every: 60000,
|
|
34
|
-
* run: async function () { … }, // direct function (no jobs needed)
|
|
35
|
-
* });
|
|
12
|
+
* Two registration shapes share the same engine: 5-field POSIX cron
|
|
13
|
+
* (`"0 2 * * *"`) for wall-clock schedules and `every: ms` (with an
|
|
14
|
+
* optional `baseline: "HH:MM"` anchor) for interval schedules.
|
|
15
|
+
* Timezones are IANA names; without one the schedule follows the
|
|
16
|
+
* server's local clock. Cron shorthands `@hourly`, `@daily`,
|
|
17
|
+
* `@midnight`, `@weekly`, `@monthly`, `@yearly` and `@annually` are
|
|
18
|
+
* accepted.
|
|
36
19
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
20
|
+
* When opts.cluster is wired, fires are gated to the current leader.
|
|
21
|
+
* Every fire INSERTs a row into _blamejs_scheduler_ticks keyed on
|
|
22
|
+
* (name, scheduledAtUnix); the PRIMARY KEY race deduplicates across a
|
|
23
|
+
* split-brain window — losers increment task.tickClaimLost and skip.
|
|
24
|
+
* Tick-claim rows older than opts.tickRetentionMs (default 7 days)
|
|
25
|
+
* are pruned automatically by the leader, throttled to at most one
|
|
26
|
+
* sweep per opts.pruneIntervalMs (default 60s). Operators can force a
|
|
27
|
+
* sweep with sched.pruneTickClaims(olderThanMs?).
|
|
39
28
|
*
|
|
40
|
-
*
|
|
29
|
+
* Drift correction: nextRun is computed forward from now (not from
|
|
30
|
+
* the nominal scheduled time) so a long-running fire never queues a
|
|
31
|
+
* backlog of catch-up ticks. A watchdog clears the `running` flag if
|
|
32
|
+
* a fire's promise hasn't settled after opts.maxJobMs (default 10
|
|
33
|
+
* minutes) so a hung handler can't permanently lock out future fires.
|
|
34
|
+
* Every state transition emits an audit event under
|
|
35
|
+
* `system.scheduler.*` so operators see every fire, miss, watchdog
|
|
36
|
+
* reset, and tick-claim race in their audit log.
|
|
41
37
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* minute (0–59) hour (0–23) dom (1–31) month (1–12) dow (0–7; 0/7=Sun)
|
|
45
|
-
*
|
|
46
|
-
* Each field accepts: * N N,M,… A-B *\/N A-B/N
|
|
47
|
-
*
|
|
48
|
-
* Shorthands: @hourly @daily @midnight @weekly @monthly @yearly @annually
|
|
49
|
-
*
|
|
50
|
-
* Cluster gating: when opts.cluster is wired and the local node is not
|
|
51
|
-
* the leader, schedule fires no-op. The leader still computes nextRun
|
|
52
|
-
* locally so a leader transition picks up cleanly.
|
|
53
|
-
*
|
|
54
|
-
* Exactly-once-globally: when opts.cluster is wired, every fire first
|
|
55
|
-
* INSERTs a row into _blamejs_scheduler_ticks keyed on (taskName,
|
|
56
|
-
* scheduledAtUnix). The PRIMARY KEY race ensures that even if two
|
|
57
|
-
* nodes briefly believe they are the leader (split-brain on lease
|
|
58
|
-
* boundary), only the row-winner runs the task. The loser increments
|
|
59
|
-
* task.tickClaimLost (visible via list()) and skips silently. Task
|
|
60
|
-
* handlers should still be idempotent — operators may add jobs.enqueue
|
|
61
|
-
* dedup keys for defense-in-depth.
|
|
62
|
-
*
|
|
63
|
-
* Tick-claim retention: rows older than opts.tickRetentionMs (default
|
|
64
|
-
* 7 days) are pruned automatically — at most once per opts.pruneInterval
|
|
65
|
-
* Ms (default 60s) — by the leader on its next successful fire. Operators
|
|
66
|
-
* can also call sched.pruneTickClaims(olderThanMs?) on demand to force
|
|
67
|
-
* a sweep (e.g. from a maintenance script) and observe the count via
|
|
68
|
-
* the system.scheduler.tick.pruned audit event.
|
|
69
|
-
*
|
|
70
|
-
* Watchdog: if a fire's promise hasn't settled after MAX_JOB_MS
|
|
71
|
-
* (10min default; opts.maxJobMs to override), the running flag is
|
|
72
|
-
* force-cleared and a warning emitted, so a hung job doesn't lock out
|
|
73
|
-
* future fires.
|
|
38
|
+
* @card
|
|
39
|
+
* Cron-style task scheduler with cluster leader gating, deduplicated ticks, drift correction, and an audit event on every tick.
|
|
74
40
|
*/
|
|
75
41
|
|
|
76
42
|
var lazyRequire = require("./lazy-require");
|
|
@@ -168,6 +134,29 @@ function _parseCronField(text, range) {
|
|
|
168
134
|
return set;
|
|
169
135
|
}
|
|
170
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @primitive b.scheduler.parseCron
|
|
139
|
+
* @signature b.scheduler.parseCron(expr)
|
|
140
|
+
* @since 0.5.0
|
|
141
|
+
* @related b.scheduler.create, b.scheduler.nextCronFire
|
|
142
|
+
*
|
|
143
|
+
* Parse a 5-field POSIX cron expression (or one of the `@hourly`,
|
|
144
|
+
* `@daily`, `@midnight`, `@weekly`, `@monthly`, `@yearly`, `@annually`
|
|
145
|
+
* shorthands) into a struct of populated minute / hour / dom / month /
|
|
146
|
+
* dow sets plus the normalized expression text. Throws SchedulerError
|
|
147
|
+
* (`scheduler/invalid-cron`) on malformed input — empty fields, bad
|
|
148
|
+
* step / range syntax, or values outside each field's bounds. The
|
|
149
|
+
* `dow` field accepts both 0 and 7 for Sunday and normalizes to 0.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* var cron = b.scheduler.parseCron("0 2 * * *");
|
|
153
|
+
* cron.expr; // → "0 2 * * *"
|
|
154
|
+
* cron.minute.has(0); // → true
|
|
155
|
+
* cron.hour.has(2); // → true
|
|
156
|
+
*
|
|
157
|
+
* var weekly = b.scheduler.parseCron("@weekly");
|
|
158
|
+
* weekly.expr; // → "0 0 * * 0"
|
|
159
|
+
*/
|
|
171
160
|
function parseCron(expr) {
|
|
172
161
|
if (typeof expr !== "string" || expr.length === 0) {
|
|
173
162
|
throw new SchedulerError("scheduler/invalid-cron",
|
|
@@ -268,9 +257,27 @@ function _matchesCron(cron, parts) {
|
|
|
268
257
|
return true; // both fully wild
|
|
269
258
|
}
|
|
270
259
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
260
|
+
/**
|
|
261
|
+
* @primitive b.scheduler.nextCronFire
|
|
262
|
+
* @signature b.scheduler.nextCronFire(cron, after, timeZone)
|
|
263
|
+
* @since 0.5.0
|
|
264
|
+
* @related b.scheduler.parseCron, b.scheduler.nextBaselineFire
|
|
265
|
+
*
|
|
266
|
+
* Earliest UTC millisecond strictly after `after` whose wall-clock in
|
|
267
|
+
* `timeZone` matches the parsed cron sets. Walks minute-by-minute; the
|
|
268
|
+
* search is bounded at one year plus a one-hour DST cushion before
|
|
269
|
+
* throwing SchedulerError (`scheduler/cron-no-fire`) so an impossible
|
|
270
|
+
* date constraint surfaces loudly instead of looping forever. Pass
|
|
271
|
+
* `null` for `timeZone` to follow the server's local clock.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* var cron = b.scheduler.parseCron("0 2 * * *");
|
|
275
|
+
* var when = b.scheduler.nextCronFire(cron, new Date("2026-05-09T00:00:00Z"), "UTC");
|
|
276
|
+
* new Date(when).toISOString();
|
|
277
|
+
* // → "2026-05-09T02:00:00.000Z"
|
|
278
|
+
*/
|
|
279
|
+
// Walks minute by minute; bounded at ~530K iterations (1 year of
|
|
280
|
+
// minutes) before giving up with a clear error.
|
|
274
281
|
function nextCronFire(cron, after, timeZone) {
|
|
275
282
|
var MINUTE_MS = C.TIME.minutes(1);
|
|
276
283
|
// Round up to the next whole minute boundary
|
|
@@ -288,7 +295,28 @@ function nextCronFire(cron, after, timeZone) {
|
|
|
288
295
|
"(impossible date constraint?)", true);
|
|
289
296
|
}
|
|
290
297
|
|
|
291
|
-
|
|
298
|
+
/**
|
|
299
|
+
* @primitive b.scheduler.nextBaselineFire
|
|
300
|
+
* @signature b.scheduler.nextBaselineFire(timeOfDay, timeZone, after)
|
|
301
|
+
* @since 0.5.0
|
|
302
|
+
* @related b.scheduler.nextCronFire, b.scheduler.create
|
|
303
|
+
*
|
|
304
|
+
* Earliest UTC millisecond strictly after `after` whose wall-clock in
|
|
305
|
+
* `timeZone` matches the supplied `HH:MM` time-of-day. Used internally
|
|
306
|
+
* to anchor `every`-shaped tasks to a daily baseline; exposed so
|
|
307
|
+
* operators can compute the same instant for fixtures or external
|
|
308
|
+
* coordination. Throws SchedulerError on malformed input
|
|
309
|
+
* (`scheduler/invalid-baseline`) or on a no-fire-within-24h timezone
|
|
310
|
+
* bug (`scheduler/baseline-no-fire`). Pass `null` for `timeZone` to
|
|
311
|
+
* follow the server's local clock.
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* var when = b.scheduler.nextBaselineFire(
|
|
315
|
+
* "02:30", "UTC", new Date("2026-05-09T01:00:00Z")
|
|
316
|
+
* );
|
|
317
|
+
* new Date(when).toISOString();
|
|
318
|
+
* // → "2026-05-09T02:30:00.000Z"
|
|
319
|
+
*/
|
|
292
320
|
function nextBaselineFire(timeOfDay, timeZone, after) {
|
|
293
321
|
var match = String(timeOfDay).match(/^(\d{1,2}):(\d{2})$/);
|
|
294
322
|
if (!match) {
|
|
@@ -316,6 +344,43 @@ function nextBaselineFire(timeOfDay, timeZone, after) {
|
|
|
316
344
|
|
|
317
345
|
// ---- Engine ----
|
|
318
346
|
|
|
347
|
+
/**
|
|
348
|
+
* @primitive b.scheduler.create
|
|
349
|
+
* @signature b.scheduler.create(opts)
|
|
350
|
+
* @since 0.5.0
|
|
351
|
+
* @related b.scheduler.parseCron, b.cluster.init, b.jobs.create
|
|
352
|
+
*
|
|
353
|
+
* Build a scheduler instance. Returns a facade exposing `schedule`,
|
|
354
|
+
* `register`, `start`, `stop`, `list`, `getStatus`, and
|
|
355
|
+
* `pruneTickClaims`. Tasks are registered before `start()`; `start()`
|
|
356
|
+
* arms timers, `stop()` clears them and drops pending fires. When
|
|
357
|
+
* `opts.cluster` is supplied, fires are gated to the leader and a
|
|
358
|
+
* tick-claim row in `_blamejs_scheduler_ticks` deduplicates split-brain
|
|
359
|
+
* windows. When `opts.jobs` is supplied, tasks declared with
|
|
360
|
+
* `{ job: "name" }` dispatch via the jobs queue; tasks declared with
|
|
361
|
+
* `{ run: fn }` execute the function directly.
|
|
362
|
+
*
|
|
363
|
+
* @opts
|
|
364
|
+
* jobs: object, // optional jobs instance for { job: "name" } tasks
|
|
365
|
+
* cluster: object, // optional cluster instance — gates fires to leader
|
|
366
|
+
* audit: boolean, // emit system.scheduler.* audit events (default true)
|
|
367
|
+
* maxJobMs: number, // watchdog reset threshold (default 10 minutes)
|
|
368
|
+
* tickRetentionMs: number, // tick-claim row retention (default 7 days)
|
|
369
|
+
* pruneIntervalMs: number, // throttle for opportunistic prune (default 60s)
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* var sched = b.scheduler.create({ audit: true });
|
|
373
|
+
* sched.schedule({
|
|
374
|
+
* name: "nightly-cleanup",
|
|
375
|
+
* cron: "0 2 * * *",
|
|
376
|
+
* timezone: "UTC",
|
|
377
|
+
* run: async function () { return "ok"; },
|
|
378
|
+
* });
|
|
379
|
+
* await sched.start();
|
|
380
|
+
* var snapshot = sched.list();
|
|
381
|
+
* snapshot[0].name; // → "nightly-cleanup"
|
|
382
|
+
* await sched.stop();
|
|
383
|
+
*/
|
|
319
384
|
function create(opts) {
|
|
320
385
|
opts = opts || {};
|
|
321
386
|
validateOpts(opts, [
|