@blamejs/core 0.8.43 → 0.8.49
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 +92 -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/cluster.js
CHANGED
|
@@ -1,54 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.cluster
|
|
4
|
+
* @featured true
|
|
5
|
+
* @nav Production
|
|
6
|
+
* @title Cluster
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Opt-in active/active leader election with fencing-tokenized writes.
|
|
10
|
+
* An external database is required: the framework's default provider
|
|
11
|
+
* stores the leader-election row, the per-chain tip rows, and the
|
|
12
|
+
* shared vault-key fingerprint in the same backend so every node sees
|
|
13
|
+
* one source of truth. When `b.cluster.init` is never called, the
|
|
14
|
+
* local process behaves as a permanent single leader: `isLeader()`
|
|
15
|
+
* always returns true, `fencingToken()` returns 0, no heartbeat runs,
|
|
16
|
+
* no DB is touched. Single-node deployments pay zero overhead.
|
|
9
17
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
18
|
+
* When init IS called, the framework starts a heartbeat that renews
|
|
19
|
+
* the leader lease via the configured provider. On lease loss (network
|
|
20
|
+
* partition, takeover, lease expiry) the node transitions to follower
|
|
21
|
+
* and write-side framework primitives throw `NotLeaderError`. The
|
|
22
|
+
* audit + consent chains carry a fencing token alongside every row so
|
|
23
|
+
* a stale leader cannot silently extend the chain after losing its
|
|
24
|
+
* lease — the audit-tip CHECK constraint refuses the stale token at
|
|
25
|
+
* the database layer. The application-level `requireLeader()` gate is
|
|
26
|
+
* an early-rejection optimisation; the DB constraint is canonical.
|
|
14
27
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
28
|
+
* Threat model:
|
|
29
|
+
* - Two leaders writing simultaneously — prevented by fencing
|
|
30
|
+
* tokens carried into the audit-tip row.
|
|
31
|
+
* - Follower receiving a write — rejected at the framework boundary
|
|
32
|
+
* via NotLeaderError. Operators front the cluster with a load
|
|
33
|
+
* balancer that routes write paths to the current leader; the
|
|
34
|
+
* discovery handler exposes which node holds the lease.
|
|
35
|
+
* - External-db unreachable — heartbeat fails; after `leaseTtl` no
|
|
36
|
+
* leader exists and writes fail closed. When the DB recovers,
|
|
37
|
+
* election resumes.
|
|
38
|
+
* - Vault-key drift — every node fingerprints its vault keys on
|
|
39
|
+
* boot and compares against a canonical fingerprint stored in
|
|
40
|
+
* the cluster-state row. A node holding a different key refuses
|
|
41
|
+
* to participate, preventing silent sealed-column corruption.
|
|
27
42
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* cluster.isLeader() sync; true on leader (or single-node)
|
|
31
|
-
* cluster.currentNodeId() sync; configured nodeId
|
|
32
|
-
* cluster.endpoint() sync; this node's routable URL
|
|
33
|
-
* (operator-supplied at init), or
|
|
34
|
-
* null if unconfigured. Stored in
|
|
35
|
-
* the leader-election row so
|
|
36
|
-
* external observers can resolve
|
|
37
|
-
* "where is the current leader?"
|
|
38
|
-
* cluster.fencingToken() sync; current monotonic token
|
|
39
|
-
* cluster.requireLeader() sync; throws NotLeaderError
|
|
40
|
-
* cluster.currentLeader() async; { nodeId, leaseExpiresAt,
|
|
41
|
-
* fencingToken,
|
|
42
|
-
* endpoint } | null
|
|
43
|
-
* cluster.discoveryHandler() returns an HTTP request handler
|
|
44
|
-
* (req, res) → JSON. Mount on any
|
|
45
|
-
* route to expose the current
|
|
46
|
-
* leader for service-mesh / LB
|
|
47
|
-
* consumption. 200 with leader,
|
|
48
|
-
* 503 with `{ leader: null }`
|
|
49
|
-
* when no leader.
|
|
50
|
-
* cluster.onTransition(fn) register transition handler
|
|
51
|
-
* await cluster.shutdown() releases lease, stops heartbeat
|
|
43
|
+
* @card
|
|
44
|
+
* Opt-in active/active leader election with fencing-tokenized writes.
|
|
52
45
|
*/
|
|
53
46
|
var C = require("./constants");
|
|
54
47
|
var clusterProviderDb = require("./cluster-provider-db");
|
|
@@ -131,6 +124,52 @@ function _emitTransition(kind, detail) {
|
|
|
131
124
|
|
|
132
125
|
// ---- init ----
|
|
133
126
|
|
|
127
|
+
/**
|
|
128
|
+
* @primitive b.cluster.init
|
|
129
|
+
* @signature b.cluster.init(opts)
|
|
130
|
+
* @since 0.4.0
|
|
131
|
+
* @status stable
|
|
132
|
+
* @compliance soc2, dora
|
|
133
|
+
* @related b.cluster.shutdown, b.cluster.requireLeader, b.cluster.currentLeader
|
|
134
|
+
*
|
|
135
|
+
* One-time cluster bootstrap. Configures the leader-election provider,
|
|
136
|
+
* validates the operator-supplied endpoint, runs boot-time rollback
|
|
137
|
+
* detection on the audit + consent chains, fingerprints this node's
|
|
138
|
+
* vault keys against the canonical cluster-state row, then starts the
|
|
139
|
+
* heartbeat that acquires and renews the leader lease. Throws on
|
|
140
|
+
* second invocation, on missing nodeId, on a leaseTtl below 10s, on a
|
|
141
|
+
* heartbeat that doesn't fit comfortably inside the lease, on a role
|
|
142
|
+
* outside `leader` / `follower`, and on a chain or vault-key mismatch
|
|
143
|
+
* that would let this node corrupt cluster state.
|
|
144
|
+
*
|
|
145
|
+
* @opts
|
|
146
|
+
* nodeId: string, // required; stable identity
|
|
147
|
+
* role: "leader"|"follower",
|
|
148
|
+
* leaseTtl: number, // ms; default 30000, min 10000
|
|
149
|
+
* heartbeatInterval: number, // ms; default 10000, min 1000
|
|
150
|
+
* endpoint: string, // routable URL of THIS node
|
|
151
|
+
* allowedProtocols: number, // safeUrl.ALLOW_HTTP_TLS by default
|
|
152
|
+
* provider: object, // custom election provider
|
|
153
|
+
* externalDbBackend: object, // required when no custom provider
|
|
154
|
+
* dialect: "postgres"|"sqlite"|"mysql",
|
|
155
|
+
* onTransition: function (event),
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* await b.cluster.init({
|
|
159
|
+
* nodeId: "api-01",
|
|
160
|
+
* role: "leader",
|
|
161
|
+
* leaseTtl: 30000,
|
|
162
|
+
* heartbeatInterval: 10000,
|
|
163
|
+
* endpoint: "https://api-01.example.internal:8443",
|
|
164
|
+
* externalDbBackend: b.externalDb.backend("primary"),
|
|
165
|
+
* dialect: "postgres",
|
|
166
|
+
* onTransition: function (event) {
|
|
167
|
+
* // event.kind ∈ { "lease-acquired", "lease-lost", "lease-released" }
|
|
168
|
+
* console.log("cluster transition:", event.kind, event.fencingToken);
|
|
169
|
+
* },
|
|
170
|
+
* });
|
|
171
|
+
* // → undefined (heartbeat now running)
|
|
172
|
+
*/
|
|
134
173
|
async function init(opts) {
|
|
135
174
|
if (initialized) {
|
|
136
175
|
throw _err("ALREADY_INITIALIZED", "cluster.init() called twice", true);
|
|
@@ -527,12 +566,54 @@ async function _heartbeat() {
|
|
|
527
566
|
|
|
528
567
|
// ---- public sync surface ----
|
|
529
568
|
|
|
569
|
+
/**
|
|
570
|
+
* @primitive b.cluster.isLeader
|
|
571
|
+
* @signature b.cluster.isLeader()
|
|
572
|
+
* @since 0.4.0
|
|
573
|
+
* @status stable
|
|
574
|
+
* @related b.cluster.requireLeader, b.cluster.fencingToken, b.cluster.currentLeader
|
|
575
|
+
*
|
|
576
|
+
* Synchronous leader check. Returns `true` when this node currently
|
|
577
|
+
* holds a non-expired lease, OR when `b.cluster.init` was never called
|
|
578
|
+
* (single-node permanent-leader fallback). Returns `false` after a
|
|
579
|
+
* graceful `shutdown()`, after lease loss, or while a follower is
|
|
580
|
+
* waiting for its first lease. Cheap; safe to call on every request to
|
|
581
|
+
* branch leader-only work (scheduled jobs, cache warmers, write-side
|
|
582
|
+
* sweeps).
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* if (b.cluster.isLeader()) {
|
|
586
|
+
* // Run scheduled tick on the leader only.
|
|
587
|
+
* await runHourlyRollup();
|
|
588
|
+
* }
|
|
589
|
+
* // → undefined
|
|
590
|
+
*/
|
|
530
591
|
function isLeader() {
|
|
531
592
|
if (terminated) return false; // post-shutdown: never leader
|
|
532
593
|
if (!initialized) return true; // never-initialized: permanent leader
|
|
533
594
|
return !!lease && Date.now() < lease.expiresAt;
|
|
534
595
|
}
|
|
535
596
|
|
|
597
|
+
/**
|
|
598
|
+
* @primitive b.cluster.isClusterMode
|
|
599
|
+
* @signature b.cluster.isClusterMode()
|
|
600
|
+
* @since 0.4.0
|
|
601
|
+
* @status stable
|
|
602
|
+
* @related b.cluster.init, b.cluster.externalDbBackend
|
|
603
|
+
*
|
|
604
|
+
* Returns `true` when `b.cluster.init` has been called AND an
|
|
605
|
+
* externalDbBackend is wired — i.e. framework state (audit, consent,
|
|
606
|
+
* fencing-tokenized writes) should route to the shared external DB.
|
|
607
|
+
* Returns `false` in single-node fallback or when a custom provider
|
|
608
|
+
* was supplied without an externalDbBackend; in that case the operator
|
|
609
|
+
* owns write-dispatch.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* if (b.cluster.isClusterMode()) {
|
|
613
|
+
* console.log("framework state lives on", b.cluster.externalDbBackend());
|
|
614
|
+
* }
|
|
615
|
+
* // → undefined
|
|
616
|
+
*/
|
|
536
617
|
// Has cluster.init been called with a real configuration? Used by
|
|
537
618
|
// write-dispatch code (audit, consent, …) to decide whether framework
|
|
538
619
|
// state should go to local SQLite or external-db.
|
|
@@ -540,31 +621,146 @@ function isClusterMode() {
|
|
|
540
621
|
return initialized && !!configuredExternalDbBackend;
|
|
541
622
|
}
|
|
542
623
|
|
|
624
|
+
/**
|
|
625
|
+
* @primitive b.cluster.externalDbBackend
|
|
626
|
+
* @signature b.cluster.externalDbBackend()
|
|
627
|
+
* @since 0.4.0
|
|
628
|
+
* @status stable
|
|
629
|
+
* @related b.cluster.init, b.cluster.dialect, b.cluster.isClusterMode
|
|
630
|
+
*
|
|
631
|
+
* Returns the externalDb backend handle wired at init, or `null` in
|
|
632
|
+
* single-node fallback / when a custom provider was supplied without
|
|
633
|
+
* one. Internal write-dispatch code (audit, consent, fencing-tokenized
|
|
634
|
+
* primitives) calls this to route framework state to the shared
|
|
635
|
+
* backend; operator code rarely needs it directly.
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* var backend = b.cluster.externalDbBackend();
|
|
639
|
+
* if (backend) {
|
|
640
|
+
* // Framework state lands on the shared cluster DB.
|
|
641
|
+
* }
|
|
642
|
+
* // → undefined
|
|
643
|
+
*/
|
|
543
644
|
function externalDbBackend() {
|
|
544
645
|
return configuredExternalDbBackend;
|
|
545
646
|
}
|
|
546
647
|
|
|
648
|
+
/**
|
|
649
|
+
* @primitive b.cluster.dialect
|
|
650
|
+
* @signature b.cluster.dialect()
|
|
651
|
+
* @since 0.4.0
|
|
652
|
+
* @status stable
|
|
653
|
+
* @related b.cluster.externalDbBackend, b.cluster.init
|
|
654
|
+
*
|
|
655
|
+
* Returns the SQL dialect string wired at init — `"postgres"`,
|
|
656
|
+
* `"sqlite"`, or `"mysql"`. Used by write-dispatch code that emits raw
|
|
657
|
+
* placeholder syntax (`$1` vs `?`) against the shared backend.
|
|
658
|
+
*
|
|
659
|
+
* @example
|
|
660
|
+
* var ph = b.cluster.dialect() === "postgres" ? "$1" : "?";
|
|
661
|
+
* // → undefined
|
|
662
|
+
*/
|
|
547
663
|
function dialect() {
|
|
548
664
|
return configuredDialect;
|
|
549
665
|
}
|
|
550
666
|
|
|
667
|
+
/**
|
|
668
|
+
* @primitive b.cluster.currentNodeId
|
|
669
|
+
* @signature b.cluster.currentNodeId()
|
|
670
|
+
* @since 0.4.0
|
|
671
|
+
* @status stable
|
|
672
|
+
* @related b.cluster.endpoint, b.cluster.currentLeader
|
|
673
|
+
*
|
|
674
|
+
* Returns this node's configured nodeId, or `"single-node-local"` in
|
|
675
|
+
* the permanent-leader fallback when init was never called. Stable
|
|
676
|
+
* across the lifetime of the process — operators use it to tag audit
|
|
677
|
+
* metadata and observability events with the node identity.
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* b.audit.safeEmit({
|
|
681
|
+
* action: "system.bootstrapped",
|
|
682
|
+
* actor: { systemNode: b.cluster.currentNodeId() },
|
|
683
|
+
* outcome: "success",
|
|
684
|
+
* });
|
|
685
|
+
* // → undefined
|
|
686
|
+
*/
|
|
551
687
|
function currentNodeId() {
|
|
552
688
|
return initialized ? nodeId : "single-node-local";
|
|
553
689
|
}
|
|
554
690
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
691
|
+
/**
|
|
692
|
+
* @primitive b.cluster.endpoint
|
|
693
|
+
* @signature b.cluster.endpoint()
|
|
694
|
+
* @since 0.7.30
|
|
695
|
+
* @status stable
|
|
696
|
+
* @related b.cluster.discoveryHandler, b.cluster.currentLeader
|
|
697
|
+
*
|
|
698
|
+
* This node's routable endpoint URL — the value supplied as
|
|
699
|
+
* `opts.endpoint` to `b.cluster.init`. Returns `null` when not
|
|
700
|
+
* configured or in single-node fallback. External observers wanting
|
|
701
|
+
* to learn the leader's URL should call `discoveryHandler()` /
|
|
702
|
+
* `currentLeader()` instead; this getter is for the local node's own
|
|
703
|
+
* self-identity.
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* var here = b.cluster.endpoint();
|
|
707
|
+
* // → "https://api-01.example.internal:8443"
|
|
708
|
+
*/
|
|
559
709
|
function endpoint() {
|
|
560
710
|
return configuredEndpoint;
|
|
561
711
|
}
|
|
562
712
|
|
|
713
|
+
/**
|
|
714
|
+
* @primitive b.cluster.fencingToken
|
|
715
|
+
* @signature b.cluster.fencingToken()
|
|
716
|
+
* @since 0.4.0
|
|
717
|
+
* @status stable
|
|
718
|
+
* @compliance soc2
|
|
719
|
+
* @related b.cluster.isLeader, b.cluster.currentLeader
|
|
720
|
+
*
|
|
721
|
+
* Current monotonic fencing token for this node's lease. Increments
|
|
722
|
+
* with every successful acquisition; a stale leader's token is
|
|
723
|
+
* strictly less than the new leader's, and the audit-tip CHECK
|
|
724
|
+
* constraint refuses inserts carrying a stale token. Returns `0` when
|
|
725
|
+
* no lease is held (follower, between leases, single-node fallback).
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* var token = b.cluster.fencingToken();
|
|
729
|
+
* // → 42
|
|
730
|
+
*/
|
|
563
731
|
function fencingToken() {
|
|
564
732
|
if (!initialized) return 0;
|
|
565
733
|
return lease ? lease.fencingToken : 0;
|
|
566
734
|
}
|
|
567
735
|
|
|
736
|
+
/**
|
|
737
|
+
* @primitive b.cluster.requireLeader
|
|
738
|
+
* @signature b.cluster.requireLeader()
|
|
739
|
+
* @since 0.4.0
|
|
740
|
+
* @status stable
|
|
741
|
+
* @related b.cluster.isLeader, b.cluster.currentLeader
|
|
742
|
+
*
|
|
743
|
+
* Throws `NotLeaderError` (statusCode 503) when this node is not the
|
|
744
|
+
* current leader. Use at the top of write-side handlers so a follower
|
|
745
|
+
* receiving a misrouted request rejects fast instead of producing a
|
|
746
|
+
* downstream fencing-token rejection. Single-node deployments where
|
|
747
|
+
* init was never called short-circuit through `isLeader() === true`
|
|
748
|
+
* and never throw.
|
|
749
|
+
*
|
|
750
|
+
* @example
|
|
751
|
+
* try {
|
|
752
|
+
* b.cluster.requireLeader();
|
|
753
|
+
* await runHourlyRollup();
|
|
754
|
+
* } catch (e) {
|
|
755
|
+
* if (e.isNotLeaderError) {
|
|
756
|
+
* // Operator's load balancer should retry on the leader.
|
|
757
|
+
* res.writeHead(503).end();
|
|
758
|
+
* return;
|
|
759
|
+
* }
|
|
760
|
+
* throw e;
|
|
761
|
+
* }
|
|
762
|
+
* // → undefined
|
|
763
|
+
*/
|
|
568
764
|
function requireLeader() {
|
|
569
765
|
if (!isLeader()) {
|
|
570
766
|
throw new NotLeaderError(
|
|
@@ -574,6 +770,28 @@ function requireLeader() {
|
|
|
574
770
|
}
|
|
575
771
|
}
|
|
576
772
|
|
|
773
|
+
/**
|
|
774
|
+
* @primitive b.cluster.currentLeader
|
|
775
|
+
* @signature b.cluster.currentLeader()
|
|
776
|
+
* @since 0.4.0
|
|
777
|
+
* @status stable
|
|
778
|
+
* @related b.cluster.discoveryHandler, b.cluster.endpoint, b.cluster.isLeader
|
|
779
|
+
*
|
|
780
|
+
* Async snapshot of the cluster's current leader. Returns
|
|
781
|
+
* `{ nodeId, leaseExpiresAt, fencingToken, endpoint }` when a leader
|
|
782
|
+
* holds a non-expired lease, or `null` when no node currently holds
|
|
783
|
+
* the lease (election in progress, DB unreachable, lease expired).
|
|
784
|
+
* In single-node fallback, returns the synthetic
|
|
785
|
+
* `{ nodeId: "single-node-local", leaseExpiresAt: Infinity, ... }`
|
|
786
|
+
* record so callers don't need a second branch.
|
|
787
|
+
*
|
|
788
|
+
* @example
|
|
789
|
+
* var leader = await b.cluster.currentLeader();
|
|
790
|
+
* if (leader && leader.endpoint) {
|
|
791
|
+
* console.log("forward write to", leader.endpoint);
|
|
792
|
+
* }
|
|
793
|
+
* // → undefined
|
|
794
|
+
*/
|
|
577
795
|
async function currentLeader() {
|
|
578
796
|
if (!initialized) {
|
|
579
797
|
return {
|
|
@@ -605,6 +823,31 @@ async function currentLeader() {
|
|
|
605
823
|
// Handler is method-agnostic so it works behind any HTTP probe shape
|
|
606
824
|
// (GET, HEAD, etc.). Cache-Control: no-store to avoid stale-leader
|
|
607
825
|
// responses pinned by a caching proxy during a takeover.
|
|
826
|
+
/**
|
|
827
|
+
* @primitive b.cluster.discoveryHandler
|
|
828
|
+
* @signature b.cluster.discoveryHandler()
|
|
829
|
+
* @since 0.7.30
|
|
830
|
+
* @status stable
|
|
831
|
+
* @related b.cluster.currentLeader, b.cluster.endpoint
|
|
832
|
+
*
|
|
833
|
+
* Returns an HTTP `(req, res)` handler suitable for mounting on any
|
|
834
|
+
* route (e.g. `/cluster/leader`). Replies 200 JSON with
|
|
835
|
+
* `{ leader, self }` when a leader holds the lease, 503 JSON with
|
|
836
|
+
* `{ leader: null, self }` when no leader exists or the DB is
|
|
837
|
+
* unreachable. Method-agnostic; emits `Cache-Control: no-store` so
|
|
838
|
+
* caching proxies don't pin a stale leader during a takeover. No auth
|
|
839
|
+
* — intended for infrastructure inside the trust boundary (load
|
|
840
|
+
* balancers, healthchecks, dashboards). Operators exposing the
|
|
841
|
+
* endpoint externally should layer auth via their own middleware.
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* var leaderProbe = b.cluster.discoveryHandler();
|
|
845
|
+
* server.on("request", function (req, res) {
|
|
846
|
+
* if (req.url === "/cluster/leader") return leaderProbe(req, res);
|
|
847
|
+
* // ... rest of routing
|
|
848
|
+
* });
|
|
849
|
+
* // → undefined
|
|
850
|
+
*/
|
|
608
851
|
function discoveryHandler() {
|
|
609
852
|
return async function (req, res) {
|
|
610
853
|
var selfInfo = {
|
|
@@ -644,6 +887,31 @@ function discoveryHandler() {
|
|
|
644
887
|
};
|
|
645
888
|
}
|
|
646
889
|
|
|
890
|
+
/**
|
|
891
|
+
* @primitive b.cluster.onTransition
|
|
892
|
+
* @signature b.cluster.onTransition(handler)
|
|
893
|
+
* @since 0.4.0
|
|
894
|
+
* @status stable
|
|
895
|
+
* @related b.cluster.init, b.cluster.shutdown
|
|
896
|
+
*
|
|
897
|
+
* Register a callback fired on every cluster role transition. Event
|
|
898
|
+
* shape: `{ kind, nodeId, at, fencingToken? }` where `kind` is one of
|
|
899
|
+
* `"lease-acquired"`, `"lease-lost"`, `"lease-released"`. Multiple
|
|
900
|
+
* handlers can be registered; each runs in registration order and
|
|
901
|
+
* a throwing handler is logged but doesn't break the chain. Throws
|
|
902
|
+
* synchronously when `handler` is not a function.
|
|
903
|
+
*
|
|
904
|
+
* @example
|
|
905
|
+
* b.cluster.onTransition(function (event) {
|
|
906
|
+
* b.audit.safeEmit({
|
|
907
|
+
* action: "system.cluster_transition",
|
|
908
|
+
* actor: { systemNode: event.nodeId },
|
|
909
|
+
* outcome: "success",
|
|
910
|
+
* metadata: { kind: event.kind, fencingToken: event.fencingToken },
|
|
911
|
+
* });
|
|
912
|
+
* });
|
|
913
|
+
* // → undefined
|
|
914
|
+
*/
|
|
647
915
|
function onTransition(handler) {
|
|
648
916
|
if (typeof handler !== "function") {
|
|
649
917
|
throw _err("INVALID_HANDLER", "onTransition expects a function", true);
|
|
@@ -651,6 +919,29 @@ function onTransition(handler) {
|
|
|
651
919
|
transitionHandlers.push(handler);
|
|
652
920
|
}
|
|
653
921
|
|
|
922
|
+
/**
|
|
923
|
+
* @primitive b.cluster.shutdown
|
|
924
|
+
* @signature b.cluster.shutdown()
|
|
925
|
+
* @since 0.4.0
|
|
926
|
+
* @status stable
|
|
927
|
+
* @related b.cluster.init, b.cluster.onTransition
|
|
928
|
+
*
|
|
929
|
+
* Graceful cluster exit. Stops the heartbeat, releases the lease via
|
|
930
|
+
* the provider so the next election round can fire immediately
|
|
931
|
+
* (instead of waiting for `leaseTtl` to expire), emits a
|
|
932
|
+
* `lease-released` transition, and resets internal state. Idempotent
|
|
933
|
+
* when init was never called. After shutdown, `isLeader()` returns
|
|
934
|
+
* `false` permanently for this process; a fresh `init()` is required
|
|
935
|
+
* to participate again. Wire into the framework's appShutdown hook so
|
|
936
|
+
* SIGTERM frees the lease before the new replica boots.
|
|
937
|
+
*
|
|
938
|
+
* @example
|
|
939
|
+
* process.on("SIGTERM", async function () {
|
|
940
|
+
* await b.cluster.shutdown();
|
|
941
|
+
* process.exit(0);
|
|
942
|
+
* });
|
|
943
|
+
* // → undefined
|
|
944
|
+
*/
|
|
654
945
|
async function shutdown() {
|
|
655
946
|
if (!initialized) return;
|
|
656
947
|
if (heartbeatTimer) {
|
package/lib/codepoint-class.js
CHANGED
|
@@ -86,6 +86,69 @@ var NULL_RE_G = new RegExp(hex4(0x0000), "g");
|
|
|
86
86
|
var NULL_BYTE = fromCp(0x0000);
|
|
87
87
|
var BOM_CHAR = fromCp(0xFEFF);
|
|
88
88
|
|
|
89
|
+
// Unicode script-range catalog for IDN-homograph / mixed-script
|
|
90
|
+
// confusable detection (UTS #39). Used by guard-domain, guard-email,
|
|
91
|
+
// safe-url IDN host-label classification, and any future caller that
|
|
92
|
+
// needs "is this label entirely one writing system?". Centralizing the
|
|
93
|
+
// table keeps the codepoint definitions in one place — adding a script
|
|
94
|
+
// is a single edit.
|
|
95
|
+
var SCRIPT_RANGES = {
|
|
96
|
+
latin: [[0x0041, 0x005A], [0x0061, 0x007A],
|
|
97
|
+
[0x00C0, 0x024F], [0x1E00, 0x1EFF]], // allow:raw-byte-literal — Unicode script ranges
|
|
98
|
+
cyrillic: [[0x0400, 0x04FF], [0x0500, 0x052F]], // allow:raw-byte-literal — Unicode Cyrillic + Cyrillic Supplement
|
|
99
|
+
greek: [[0x0370, 0x03FF], [0x1F00, 0x1FFF]], // allow:raw-byte-literal — Unicode Greek + Greek Extended
|
|
100
|
+
armenian: [[0x0530, 0x058F]], // allow:raw-byte-literal — Unicode Armenian
|
|
101
|
+
cherokee: [[0x13A0, 0x13FF], [0xAB70, 0xABBF]], // allow:raw-byte-literal — Unicode Cherokee + Cherokee Supplement
|
|
102
|
+
han: [[0x4E00, 0x9FFF]], // allow:raw-byte-literal — CJK Unified Ideographs
|
|
103
|
+
hiragana: [[0x3040, 0x309F]], // allow:raw-byte-literal — Hiragana
|
|
104
|
+
katakana: [[0x30A0, 0x30FF]], // allow:raw-byte-literal — Katakana
|
|
105
|
+
hangul: [[0xAC00, 0xD7AF]], // allow:raw-byte-literal — Hangul Syllables
|
|
106
|
+
arabic: [[0x0600, 0x06FF]], // allow:raw-byte-literal — Arabic
|
|
107
|
+
hebrew: [[0x0590, 0x05FF]], // allow:raw-byte-literal — Hebrew
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// scriptFor(cp) — returns the script-name string for a codepoint, or
|
|
111
|
+
// null when the codepoint is in a script not in the catalog (digits,
|
|
112
|
+
// punctuation, symbols, etc. are not script-classifying).
|
|
113
|
+
function scriptFor(cp) {
|
|
114
|
+
var keys = Object.keys(SCRIPT_RANGES);
|
|
115
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
116
|
+
var ranges = SCRIPT_RANGES[keys[i]];
|
|
117
|
+
for (var j = 0; j < ranges.length; j += 1) {
|
|
118
|
+
if (cp >= ranges[j][0] && cp <= ranges[j][1]) return keys[i];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// detectMixedScripts(label, allowedScripts?) — returns null when the
|
|
125
|
+
// label is single-script (or every script appears in the optional
|
|
126
|
+
// allowedScripts allowlist), or an array of the detected script names
|
|
127
|
+
// when the label mixes scripts (homograph attack shape — Cyrillic 'а'
|
|
128
|
+
// inside an otherwise-Latin label, etc.). The result is the FULL set
|
|
129
|
+
// of scripts seen; callers decide refuse / audit / strip.
|
|
130
|
+
//
|
|
131
|
+
// allowedScripts: an array of script names the caller treats as
|
|
132
|
+
// acceptable; when supplied, a label whose every script is on the list
|
|
133
|
+
// returns null even if multiple scripts appear (legitimate mixed-
|
|
134
|
+
// script content like an English word inside a Japanese label).
|
|
135
|
+
function detectMixedScripts(label, allowedScripts) {
|
|
136
|
+
if (typeof label !== "string" || label.length === 0) return null;
|
|
137
|
+
var seen = {};
|
|
138
|
+
for (var i = 0; i < label.length; i += 1) {
|
|
139
|
+
var script = scriptFor(label.charCodeAt(i));
|
|
140
|
+
if (script === null) continue;
|
|
141
|
+
seen[script] = true;
|
|
142
|
+
}
|
|
143
|
+
var scripts = Object.keys(seen);
|
|
144
|
+
if (scripts.length <= 1) return null;
|
|
145
|
+
if (!allowedScripts) return scripts;
|
|
146
|
+
for (var k = 0; k < scripts.length; k += 1) {
|
|
147
|
+
if (allowedScripts.indexOf(scripts[k]) === -1) return scripts;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
89
152
|
// detectCharThreats — returns an array of issue objects for character-
|
|
90
153
|
// class threats (bidi / null / C0-control) per the opts policy. Emits
|
|
91
154
|
// at most one issue per class. Used by guard-* primitives' detection
|
|
@@ -193,4 +256,7 @@ module.exports = {
|
|
|
193
256
|
applyCharStripPolicies: applyCharStripPolicies,
|
|
194
257
|
assertNoCharThreats: assertNoCharThreats,
|
|
195
258
|
detectCharThreats: detectCharThreats,
|
|
259
|
+
SCRIPT_RANGES: SCRIPT_RANGES,
|
|
260
|
+
scriptFor: scriptFor,
|
|
261
|
+
detectMixedScripts: detectMixedScripts,
|
|
196
262
|
};
|