@cotal-ai/core 0.100.2 → 0.101.0
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/dist/agent-file.d.ts +2 -1
- package/dist/agent-file.d.ts.map +1 -1
- package/dist/agent-file.js +6 -1
- package/dist/agent-file.js.map +1 -1
- package/dist/auth-provider.d.ts +3 -1
- package/dist/auth-provider.d.ts.map +1 -1
- package/dist/auth-provider.js.map +1 -1
- package/dist/backup-config.d.ts +8 -6
- package/dist/backup-config.d.ts.map +1 -1
- package/dist/backup-config.js +30 -7
- package/dist/backup-config.js.map +1 -1
- package/dist/backup.d.ts +5 -2
- package/dist/backup.d.ts.map +1 -1
- package/dist/backup.js +12 -5
- package/dist/backup.js.map +1 -1
- package/dist/channels.d.ts +5 -4
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +14 -9
- package/dist/channels.js.map +1 -1
- package/dist/command.d.ts +10 -2
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +28 -2
- package/dist/command.js.map +1 -1
- package/dist/endpoint-action.d.ts +2 -2
- package/dist/endpoint-action.d.ts.map +1 -1
- package/dist/endpoint-action.js +1 -2
- package/dist/endpoint-action.js.map +1 -1
- package/dist/endpoint-call.d.ts.map +1 -1
- package/dist/endpoint-call.js +3 -1
- package/dist/endpoint-call.js.map +1 -1
- package/dist/endpoint-serve-kv.d.ts +1 -1
- package/dist/endpoint-serve-kv.js +1 -1
- package/dist/endpoint-service.d.ts.map +1 -1
- package/dist/endpoint-service.js +3 -1
- package/dist/endpoint-service.js.map +1 -1
- package/dist/endpoint.d.ts +40 -24
- package/dist/endpoint.d.ts.map +1 -1
- package/dist/endpoint.js +122 -56
- package/dist/endpoint.js.map +1 -1
- package/dist/evict.d.ts.map +1 -1
- package/dist/evict.js +9 -8
- package/dist/evict.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jetstream-missing.d.ts +2 -0
- package/dist/jetstream-missing.d.ts.map +1 -1
- package/dist/jetstream-missing.js +5 -1
- package/dist/jetstream-missing.js.map +1 -1
- package/dist/lease.d.ts.map +1 -1
- package/dist/lease.js +4 -3
- package/dist/lease.js.map +1 -1
- package/dist/lifecycle-saga.js +1 -1
- package/dist/lifecycle-state.d.ts +2 -2
- package/dist/link.d.ts +4 -0
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +6 -0
- package/dist/link.js.map +1 -1
- package/dist/liveness.d.ts +2 -1
- package/dist/liveness.d.ts.map +1 -1
- package/dist/liveness.js +3 -2
- package/dist/liveness.js.map +1 -1
- package/dist/membership-feed.d.ts +1 -1
- package/dist/membership-feed.d.ts.map +1 -1
- package/dist/membership-feed.js +6 -5
- package/dist/membership-feed.js.map +1 -1
- package/dist/one-line.d.ts +4 -0
- package/dist/one-line.d.ts.map +1 -1
- package/dist/one-line.js +7 -0
- package/dist/one-line.js.map +1 -1
- package/dist/provision.d.ts +7 -0
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +26 -1
- package/dist/provision.js.map +1 -1
- package/dist/run-host.d.ts +31 -5
- package/dist/run-host.d.ts.map +1 -1
- package/dist/run-record.d.ts +16 -0
- package/dist/run-record.d.ts.map +1 -1
- package/dist/run-record.js +12 -0
- package/dist/run-record.js.map +1 -1
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +3 -1
- package/dist/runtime.js.map +1 -1
- package/dist/spaces.js +4 -4
- package/dist/spaces.js.map +1 -1
- package/dist/streams.d.ts +4 -1
- package/dist/streams.d.ts.map +1 -1
- package/dist/streams.js +35 -8
- package/dist/streams.js.map +1 -1
- package/dist/subjects.d.ts +1 -5
- package/dist/subjects.d.ts.map +1 -1
- package/dist/subjects.js +4 -6
- package/dist/subjects.js.map +1 -1
- package/dist/text-encoding.d.ts +6 -0
- package/dist/text-encoding.d.ts.map +1 -0
- package/dist/text-encoding.js +8 -0
- package/dist/text-encoding.js.map +1 -0
- package/package.json +1 -1
package/dist/endpoint.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
-
import { createConnection } from "node:net";
|
|
4
|
+
import { createConnection, isIP } from "node:net";
|
|
5
5
|
import { connect, credsAuthenticator, headers, tokenAuthenticator, nanos, millis, AuthorizationError, PermissionViolationError, UserAuthenticationExpiredError, NoRespondersError, RequestError, TimeoutError, ClosedConnectionError, } from "@nats-io/transport-node";
|
|
6
6
|
import { wsconnect } from "@nats-io/nats-core";
|
|
7
7
|
import { parseLivenessAnswer, responderFromLease, responderFromProbe, isLivenessPlane, LIVENESS_PLANES, } from "./liveness.js";
|
|
@@ -63,6 +63,8 @@ export const MULTI_FILTER_READ_CONCURRENCY = 4;
|
|
|
63
63
|
* minutes is what the nats.js ordered consumers it replaces carried, and it keeps a reader alive
|
|
64
64
|
* across the gap between pulls on a slow link. */
|
|
65
65
|
const HISTORY_READER_INACTIVE_MS = 5 * 60_000;
|
|
66
|
+
/** The longest delay `setTimeout` honors, 2^31-1 ms. */
|
|
67
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
66
68
|
/** A DM-only endpoint's own `dm_` durable does not exist. The endpoint never creates it. */
|
|
67
69
|
export class DmDurableMissingError extends Error {
|
|
68
70
|
stream;
|
|
@@ -898,10 +900,14 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
898
900
|
: undefined;
|
|
899
901
|
if (expiryMs === undefined || this.stopped)
|
|
900
902
|
return;
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
903
|
+
// Node fires a delay above 2^31-1 ms (about 24.8 days) after 1 ms, which would disable reconnect
|
|
904
|
+
// at once. A later expiry waits in steps of that bound, and the fence is armed again from there.
|
|
905
|
+
const delay = Math.max(0, expiryMs - Date.now() - 500);
|
|
906
|
+
this.authExpiryReconnectTimer = delay > MAX_TIMER_MS
|
|
907
|
+
? setTimeout(() => { if (this.nc === nc)
|
|
908
|
+
this.armAuthExpiryReconnectFence(nc); }, MAX_TIMER_MS)
|
|
909
|
+
: setTimeout(() => { if (this.nc === nc)
|
|
910
|
+
this.disableLibraryReconnect(nc); }, delay);
|
|
905
911
|
this.authExpiryReconnectTimer.unref?.();
|
|
906
912
|
}
|
|
907
913
|
/** The connectAndBind PRE-CONNECT fetch: pull the freshest source cred and pin it into
|
|
@@ -2131,7 +2137,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
2131
2137
|
}
|
|
2132
2138
|
/** This endpoint's v0.4 caller triple (§13.2) — the identity its minted ep-rail rows pin. The
|
|
2133
2139
|
* owner/actor principal is mode-correct by construction (static: DEV_OWNER + the connection
|
|
2134
|
-
* identity; user mode: the bearer's callout-derived pair
|
|
2140
|
+
* identity; user mode: the bearer's callout-derived pair), and the lifecycle UID is the
|
|
2135
2141
|
* launcher-supplied incarnation the rows are keyed on (ledger-consistent: the §13.1 presence
|
|
2136
2142
|
* lifecycle-proof refuses a divergent uid before any publish). */
|
|
2137
2143
|
serviceCaller() {
|
|
@@ -2480,7 +2486,13 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
2480
2486
|
this.activity = activity;
|
|
2481
2487
|
await this.publishPresence();
|
|
2482
2488
|
}
|
|
2483
|
-
|
|
2489
|
+
/** Set the status, and with `activity` the activity line too, in one presence write, so no peer sees
|
|
2490
|
+
* the new activity beside the old status. */
|
|
2491
|
+
async setStatus(status, activity) {
|
|
2492
|
+
if (activity !== undefined && activity !== this.activity)
|
|
2493
|
+
this.statusSince = this.activitySince = Date.now();
|
|
2494
|
+
if (activity !== undefined)
|
|
2495
|
+
this.activity = activity;
|
|
2484
2496
|
if (status !== this.status)
|
|
2485
2497
|
this.statusSince = Date.now();
|
|
2486
2498
|
this.status = status;
|
|
@@ -2562,7 +2574,8 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
2562
2574
|
}
|
|
2563
2575
|
/** A channel's effective replay policy read straight from the registry bucket, for a caller that
|
|
2564
2576
|
* reads it right after start, before any watch cache has caught up, or that watches nothing. A
|
|
2565
|
-
* wildcard reads the space defaults only. A space with no registry bucket has the built-in policy.
|
|
2577
|
+
* wildcard reads the space defaults only. A space with no registry bucket has the built-in policy.
|
|
2578
|
+
* `registered` says whether the channel has its own registry entry. */
|
|
2566
2579
|
async channelReplayPolicy(channel) {
|
|
2567
2580
|
if (!this.nc)
|
|
2568
2581
|
throw new Error(this.notLiveMsg());
|
|
@@ -2580,7 +2593,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
2580
2593
|
if (!isJetStreamMissing(e, JetStreamApiCodes.StreamNotFound))
|
|
2581
2594
|
throw e;
|
|
2582
2595
|
}
|
|
2583
|
-
return { replay: effectiveReplay(cfg, defaults), window: cfg?.replayWindow ?? defaults?.replayWindow ?? null };
|
|
2596
|
+
return { replay: effectiveReplay(cfg, defaults), window: cfg?.replayWindow ?? defaults?.replayWindow ?? null, registered: cfg !== undefined };
|
|
2584
2597
|
}
|
|
2585
2598
|
/** Effective delivery class for a channel (per-channel override ?? space default ?? "durable"),
|
|
2586
2599
|
* from the live watch cache — drives the non-gating delivery-health surface (only durable-class
|
|
@@ -2699,7 +2712,8 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
2699
2712
|
* message on the channel is discarded. `maxAgeMs` is the stream's age limit. Each is `null` when
|
|
2700
2713
|
* the stream sets none. `senders` counts each sender's retained messages per channel, so a sender
|
|
2701
2714
|
* at the cap marks a channel whose retained history has lost that sender's older messages while
|
|
2702
|
-
* other senders' messages from the same time remain.
|
|
2715
|
+
* other senders' messages from the same time remain. `stream` is false when the chat stream does
|
|
2716
|
+
* not exist, which holds nothing. `lastSeq` is the stream's newest sequence, 0 when it has none.
|
|
2703
2717
|
*/
|
|
2704
2718
|
async chatRetention() {
|
|
2705
2719
|
if (!this.nc)
|
|
@@ -2713,7 +2727,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
2713
2727
|
// As in listChannels: an absent CHAT stream holds nothing yet; any other failure stays loud.
|
|
2714
2728
|
if (!isJetStreamMissing(e, JetStreamApiCodes.StreamNotFound))
|
|
2715
2729
|
throw e;
|
|
2716
|
-
return { perSender: null, maxAgeMs: null, senders: [] };
|
|
2730
|
+
return { perSender: null, maxAgeMs: null, senders: [], stream: false, lastSeq: 0 };
|
|
2717
2731
|
}
|
|
2718
2732
|
const senders = [];
|
|
2719
2733
|
for (const [subject, messages] of Object.entries(info.state.subjects ?? {})) {
|
|
@@ -2727,6 +2741,8 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
2727
2741
|
perSender: typeof cap === "number" && cap > 0 ? cap : null,
|
|
2728
2742
|
maxAgeMs: typeof age === "number" && age > 0 ? Math.round(age / 1_000_000) : null,
|
|
2729
2743
|
senders: senders.sort((a, b) => a.channel.localeCompare(b.channel) || a.sender.localeCompare(b.sender)),
|
|
2744
|
+
stream: true,
|
|
2745
|
+
lastSeq: info.state.last_seq,
|
|
2730
2746
|
};
|
|
2731
2747
|
}
|
|
2732
2748
|
/** One coherent channel model for dashboards: every channel that has messages OR a registry
|
|
@@ -3163,22 +3179,13 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
3163
3179
|
* The newest `limit` chat messages across MANY channels at once, oldest-first within the page,
|
|
3164
3180
|
* each tagged with the channel the BROKER delivered it on.
|
|
3165
3181
|
*
|
|
3166
|
-
*
|
|
3167
|
-
*
|
|
3168
|
-
*
|
|
3169
|
-
* calling {@link channelHistory} once per channel and merging: each of those is a widening probe
|
|
3170
|
-
* loop, so the cost carried two multipliers (a probe loop per channel, and a fan-out across every
|
|
3171
|
-
* channel). Counted on the wire over a seeded corpus of 69 chat channels and 24 event channels at
|
|
3172
|
-
* limit 200: 2524 broker requests and about 8.0 MB transferred to return a 143,401-byte page,
|
|
3173
|
-
* against 143 requests and about 0.91 MB here. With no link cost the counts and the page size
|
|
3174
|
-
* repeat exactly across runs; the byte totals move by tens of bytes. `pnpm smoke:web-activity-read-cost` reproduces the
|
|
3175
|
-
* second column and a frozen copy of the fan-out shape; the first is that same suite run against
|
|
3176
|
-
* `544a974b7` (Cotal #1210).
|
|
3182
|
+
* The CHAT stream interleaves every channel into one sequence space. Consumers take sets of
|
|
3183
|
+
* filter subjects for the requested channels. Large filter sets are read in batches, and the
|
|
3184
|
+
* pages are merged by stream sequence to select the newest `limit` messages overall.
|
|
3177
3185
|
*
|
|
3178
3186
|
* **The broker does the filtering, so the wire carries only what is asked for.** A channel left
|
|
3179
3187
|
* out of `channels` costs nothing: its messages are never delivered, so a space whose volume is
|
|
3180
|
-
* dominated by channels the caller does not want stays cheap.
|
|
3181
|
-
* fetch" property the per-channel fan-out had, kept rather than traded away.
|
|
3188
|
+
* dominated by channels the caller does not want stays cheap.
|
|
3182
3189
|
*
|
|
3183
3190
|
* **The channel comes from the SUBJECT, never from the payload.** A message claims a `channel`
|
|
3184
3191
|
* field, and this method ignores it: the tag is derived from the subject the broker routed the
|
|
@@ -3956,7 +3963,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
3956
3963
|
/**
|
|
3957
3964
|
* Privileged: pre-create a role's shared TASK work-queue durable (auth mode), so agents
|
|
3958
3965
|
* of that role can BIND it without holding CONSUMER.CREATE on TASK_<space>. The creator
|
|
3959
|
-
* sets the filter to svc.<role
|
|
3966
|
+
* sets the filter to svc.<role>.> — agents never choose it, which stops cross-role drain.
|
|
3960
3967
|
* Idempotent per role. The caller must be permissive on TASK_<space>.
|
|
3961
3968
|
*/
|
|
3962
3969
|
async provisionTaskQueue(role) {
|
|
@@ -4090,8 +4097,8 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
4090
4097
|
async renewDeliveryLease(shardIndex, revision) {
|
|
4091
4098
|
return (await this.deliveryRegistry()).update(leaseKey(shardIndex), this.encodeLease(shardIndex, true), revision);
|
|
4092
4099
|
}
|
|
4093
|
-
/** Release the held lease on clean shutdown so a replacement daemon re-acquires immediately (
|
|
4094
|
-
*
|
|
4100
|
+
/** Release the held lease on clean shutdown so a replacement daemon re-acquires immediately (a
|
|
4101
|
+
* crash just lets the bucket TTL expire it).
|
|
4095
4102
|
*
|
|
4096
4103
|
* THE REVISION IS WHAT MAKES THIS A RELEASE RATHER THAN A DELETE. An unconditional delete removes
|
|
4097
4104
|
* whatever row is there, and by shutdown time the row is not necessarily still ours: the exit
|
|
@@ -4101,6 +4108,10 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
4101
4108
|
* swap (`previousSeq` becomes JetStream's `ExpectedLastSubjectSequence`), so a row that has moved
|
|
4102
4109
|
* on is left alone.
|
|
4103
4110
|
*
|
|
4111
|
+
* A RELEASE THAT DID NOT COMMIT REJECTS. The broker's CAS refusal and any broker failure reach the
|
|
4112
|
+
* caller, because only the caller knows whether that matters: a shutdown that must finish anyway
|
|
4113
|
+
* catches it, and a close that reports whether a successor can start learns the outcome here.
|
|
4114
|
+
*
|
|
4104
4115
|
* THE ARGUMENT IS REQUIRED, AND EXPLICITLY NULLABLE RATHER THAN OPTIONAL. `undefined` means "this
|
|
4105
4116
|
* process no longer holds a revision it can argue for", which is the takeover paths' honest
|
|
4106
4117
|
* answer and correctly releases nothing, the bucket TTL is the crash-safe authority and expires
|
|
@@ -4112,14 +4123,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
4112
4123
|
async releaseDeliveryLease(shardIndex, revision) {
|
|
4113
4124
|
if (revision === undefined)
|
|
4114
4125
|
return;
|
|
4115
|
-
|
|
4116
|
-
await (await this.deliveryRegistry()).delete(leaseKey(shardIndex), { previousSeq: revision });
|
|
4117
|
-
}
|
|
4118
|
-
catch {
|
|
4119
|
-
// Intentionally best-effort for EVERY failure: the lease TTL is the crash-safe release authority,
|
|
4120
|
-
// and clean shutdown must continue even when the broker is already gone or draining. A refused
|
|
4121
|
-
// CAS lands here too, which is the correct outcome, someone else owns the row.
|
|
4122
|
-
}
|
|
4126
|
+
await (await this.deliveryRegistry()).delete(leaseKey(shardIndex), { previousSeq: revision });
|
|
4123
4127
|
}
|
|
4124
4128
|
/** Read a shard's delivery lease (the daemon-availability signal), or `undefined` if none is live.
|
|
4125
4129
|
* READ-ONLY surface — drives Component 6's `cotal_channels` delivery-health field (an agent reads it
|
|
@@ -4339,8 +4343,8 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
4339
4343
|
await (await this.managerLeaseRegistry()).delete(MANAGER_RENEWAL_LEASE_KEY, { previousSeq: held });
|
|
4340
4344
|
}
|
|
4341
4345
|
catch {
|
|
4342
|
-
// Best-effort
|
|
4343
|
-
//
|
|
4346
|
+
// Best-effort: a moved revision means it is not ours, and a broker failure is recovered by the
|
|
4347
|
+
// bucket TTL. Shutdown must not claim deletion.
|
|
4344
4348
|
}
|
|
4345
4349
|
}
|
|
4346
4350
|
encodeDaemonRenewalLease(instanceId) {
|
|
@@ -4389,20 +4393,15 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
4389
4393
|
return { info: JSON.parse(new TextDecoder().decode(e.value)), revision: e.revision, created: e.created.getTime() };
|
|
4390
4394
|
}
|
|
4391
4395
|
/** Release THIS instance's key on clean shutdown so a same-id restart re-acquires immediately. CAS-guarded
|
|
4392
|
-
* by `revision`: if we already LOST it (renew gap) the stored revision has moved, the conditional
|
|
4393
|
-
*
|
|
4396
|
+
* by `revision`: if we already LOST it (renew gap) the stored revision has moved, and the conditional
|
|
4397
|
+
* delete is refused and rejects, like any broker failure; a shutdown that must finish anyway catches
|
|
4398
|
+
* it. Keyed per instance, so a release NEVER touches a sibling manager's key (security pin 6). */
|
|
4394
4399
|
async releaseManagerLease(instanceId, revision) {
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
await kv.delete(managerLeaseKey(instanceId), { previousSeq: revision });
|
|
4401
|
-
}
|
|
4402
|
-
catch {
|
|
4403
|
-
// Intentionally best-effort for EVERY failure: a revision mismatch means the lease is no longer
|
|
4404
|
-
// ours, while a broker failure is recovered by the bucket TTL. Shutdown must not claim deletion.
|
|
4405
|
-
}
|
|
4400
|
+
const kv = await this.managerLeaseRegistry();
|
|
4401
|
+
if (revision === undefined)
|
|
4402
|
+
await kv.delete(managerLeaseKey(instanceId));
|
|
4403
|
+
else
|
|
4404
|
+
await kv.delete(managerLeaseKey(instanceId), { previousSeq: revision });
|
|
4406
4405
|
}
|
|
4407
4406
|
/** Read a live manager liveness lease, or undefined if NONE (no manager instance holds the space). A
|
|
4408
4407
|
* presence/existence check for the CLI's `spawn -f` reuse and `waitLeaseGone`, which only need "is any
|
|
@@ -5309,7 +5308,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
5309
5308
|
: { ok: true, data: { delivery, membership } };
|
|
5310
5309
|
}
|
|
5311
5310
|
if (req.op === "evictPrincipal") {
|
|
5312
|
-
// The LIVE-EVICTION executor
|
|
5311
|
+
// The LIVE-EVICTION executor: force-drop a denied principal's connections.
|
|
5313
5312
|
// Composition-root hook because the $SYS observer/evictor creds live outside this endpoint's
|
|
5314
5313
|
// trust boundary; absent hook = a daemon build without the executor, refused loudly.
|
|
5315
5314
|
if (!this.plane3?.evictPrincipal)
|
|
@@ -5964,7 +5963,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
5964
5963
|
inactiveThresholdMs: this.inactiveThresholdMs,
|
|
5965
5964
|
});
|
|
5966
5965
|
}
|
|
5967
|
-
await this.pump(dmStream(this.space), dmDurable(this.owner, this.actor, ownUid));
|
|
5966
|
+
await this.pump(dmStream(this.space), dmDurable(this.owner, this.actor, ownUid), { inbox: true });
|
|
5968
5967
|
// Plane-3 (SPEC §8): bind + pump our per-member DELIVER durable (`dlv_<id>`) — the re-authorized
|
|
5969
5968
|
// durable-backstop channel copies the trusted reader transfers to us. No-op when it isn't present
|
|
5970
5969
|
// (open mode / un-provisioned). Auth-only feature; the pump self-guards on the durable's existence.
|
|
@@ -6008,13 +6007,15 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
6008
6007
|
/** Drive one consumer: decode and hand each message to listeners with ack control. Our own sends are
|
|
6009
6008
|
* delivered too: on the DM inbox and the role queue they were addressed to us, so none is an echo,
|
|
6010
6009
|
* and acking one unseen on the work queue would delete the only copy of the request. */
|
|
6011
|
-
async pump(stream, durable) {
|
|
6010
|
+
async pump(stream, durable, opts = {}) {
|
|
6012
6011
|
if (!this.js)
|
|
6013
6012
|
throw new Error("endpoint not started");
|
|
6014
6013
|
const consumer = await this.js.consumers.get(stream, durable);
|
|
6015
6014
|
const ackWaitMs = await ackWaitOf(consumer);
|
|
6016
6015
|
const msgs = await consumer.consume();
|
|
6017
6016
|
this.streamMsgs.push(msgs);
|
|
6017
|
+
if (opts.inbox)
|
|
6018
|
+
this.watchInbox(msgs, stream, durable);
|
|
6018
6019
|
void (async () => {
|
|
6019
6020
|
for await (const m of msgs) {
|
|
6020
6021
|
const authed = this.authenticateStreamMsg(m);
|
|
@@ -6048,6 +6049,42 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
6048
6049
|
this.emit("error", e);
|
|
6049
6050
|
});
|
|
6050
6051
|
}
|
|
6052
|
+
/** Warn once when the broker reports this endpoint's own DM inbox missing while the connection is
|
|
6053
|
+
* up, as after a store reset that the client's reconnect rode through. nats.js keeps checking and
|
|
6054
|
+
* resumes pulling if the inbox comes back, so this only reports. One report can be a passing state,
|
|
6055
|
+
* so the warning waits for a second one; the several 409s a delete answers pending pulls with count
|
|
6056
|
+
* once. A pull means the consumer was found again, which re-arms the warning. The loop ends when
|
|
6057
|
+
* the consumer is stopped, by `stop()` or by a rebuild replacing this connection. */
|
|
6058
|
+
watchInbox(msgs, stream, durable) {
|
|
6059
|
+
const boundNc = this.nc;
|
|
6060
|
+
void (async () => {
|
|
6061
|
+
let absences = 0;
|
|
6062
|
+
let deleted = false;
|
|
6063
|
+
for await (const s of msgs.status()) {
|
|
6064
|
+
if (this.stopped || this.nc !== boundNc)
|
|
6065
|
+
break;
|
|
6066
|
+
if (s.type === "next") {
|
|
6067
|
+
absences = 0;
|
|
6068
|
+
deleted = false;
|
|
6069
|
+
continue;
|
|
6070
|
+
}
|
|
6071
|
+
if (s.type === "consumer_deleted") {
|
|
6072
|
+
if (deleted)
|
|
6073
|
+
continue;
|
|
6074
|
+
deleted = true;
|
|
6075
|
+
}
|
|
6076
|
+
else if (s.type !== "consumer_not_found" && s.type !== "stream_not_found")
|
|
6077
|
+
continue;
|
|
6078
|
+
if (++absences === 2)
|
|
6079
|
+
this.emitRecoverable(new Error(`DM inbox missing: the broker reports consumer "${durable}" on stream "${stream}" missing, so direct ` +
|
|
6080
|
+
"messages to this endpoint are not delivered. The endpoint keeps checking; if the inbox does not " +
|
|
6081
|
+
"come back, end this session and start a new one"));
|
|
6082
|
+
}
|
|
6083
|
+
})().catch((e) => {
|
|
6084
|
+
if (!this.stopped)
|
|
6085
|
+
this.emit("error", e);
|
|
6086
|
+
});
|
|
6087
|
+
}
|
|
6051
6088
|
/** Decode one stream copy and apply the authenticity guard (fail closed): the sender is encoded in
|
|
6052
6089
|
* the subject, which the server policed who could publish. The payload `from` is advisory — it
|
|
6053
6090
|
* must match, and a missing `from` or an unparseable subject on a delivery is itself an anomaly.
|
|
@@ -6280,6 +6317,12 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
6280
6317
|
const jsm = await this.manager();
|
|
6281
6318
|
return (await jsm.streams.info(chatStream(this.space))).state.last_seq;
|
|
6282
6319
|
}
|
|
6320
|
+
/** The bytes this connection's account holds in JetStream file storage, as `$JS.API.INFO`
|
|
6321
|
+
* reports them. A broker's `max_file_store` cap counts every account it serves, so this is a
|
|
6322
|
+
* lower bound on what that cap measures. */
|
|
6323
|
+
async storedBytes() {
|
|
6324
|
+
return (await (await this.manager()).getAccountInfo()).storage;
|
|
6325
|
+
}
|
|
6283
6326
|
/** Phase 1 of a join — arm each channel's tail-drop watermark at the current frontier. MUST run
|
|
6284
6327
|
* BEFORE opening the core subscription so the live tail can never carry a just-joined message
|
|
6285
6328
|
* un-watermarked — which would double-emit it (live + backfill).
|
|
@@ -6919,7 +6962,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
6919
6962
|
this.applyPresence(e.key, p);
|
|
6920
6963
|
}
|
|
6921
6964
|
applyPresence(id, raw) {
|
|
6922
|
-
// Defense-in-depth
|
|
6965
|
+
// Defense-in-depth: the KV key IS the publisher's identity —
|
|
6923
6966
|
// publishPresence() writes its record under card.id — so a record whose embedded card.id disagrees
|
|
6924
6967
|
// with its bucket key is forged or corrupt. Drop it rather than surface a spoofed roster identity.
|
|
6925
6968
|
// The write-side scoping ($KV.<presenceBucket>.<own-id>) is the primary guard; this rejects a
|
|
@@ -7537,15 +7580,38 @@ export function defaultProbeTimeoutMs(servers) {
|
|
|
7537
7580
|
* `wss://host/path`), everything else through the TCP transport. The websocket dial OWNS its
|
|
7538
7581
|
* transport options: the URL scheme already decides TLS there, and the w3c transport refuses a
|
|
7539
7582
|
* `tls` block outright ("'tls' is not configurable"), so it is stripped here — at the one point
|
|
7540
|
-
* that knows which transport is dialing — rather than at every caller composing auth options.
|
|
7583
|
+
* that knows which transport is dialing — rather than at every caller composing auth options.
|
|
7584
|
+
* A TCP dial that requires TLS to one IP address names that address as the host its certificate
|
|
7585
|
+
* must cover (see {@link withDialedTlsHost}). */
|
|
7541
7586
|
export function dialerFor(servers) {
|
|
7542
7587
|
if (!wsServers(servers))
|
|
7543
|
-
return connect;
|
|
7588
|
+
return ((opts) => connect(withDialedTlsHost(servers, opts)));
|
|
7544
7589
|
return ((opts) => {
|
|
7545
7590
|
const { tls: _tls, ...rest } = (opts ?? {});
|
|
7546
7591
|
return wsconnect(rest);
|
|
7547
7592
|
});
|
|
7548
7593
|
}
|
|
7594
|
+
/** `opts` for a dial to `servers`, with the IP address it names as the host a TLS certificate must
|
|
7595
|
+
* cover when the dial requires TLS: a `tls` block, or a `tls://` address. The TCP transport upgrades
|
|
7596
|
+
* to TLS with no server name for an IP, and Node then checks the certificate against "localhost":
|
|
7597
|
+
* a certificate for the dialed address alone is refused, and one naming localhost passes for any
|
|
7598
|
+
* address. A dial that does not require TLS keeps its options, because a `tls` block refuses a
|
|
7599
|
+
* plaintext broker, and so does a list of several servers, whose one host the options cannot name. */
|
|
7600
|
+
function withDialedTlsHost(servers, opts) {
|
|
7601
|
+
const urls = servers.split(",").map((url) => url.trim()).filter(Boolean);
|
|
7602
|
+
const tls = opts?.tls;
|
|
7603
|
+
if (urls.length !== 1 || tls === null || tls === undefined && !/^tls:\/\//i.test(urls[0]))
|
|
7604
|
+
return opts;
|
|
7605
|
+
let host;
|
|
7606
|
+
try {
|
|
7607
|
+
({ host } = hostPort(urls[0]));
|
|
7608
|
+
}
|
|
7609
|
+
catch {
|
|
7610
|
+
return opts;
|
|
7611
|
+
}
|
|
7612
|
+
// Node checks an upgraded socket's certificate against `host` when no server name is set.
|
|
7613
|
+
return isIP(host) ? { ...opts, tls: { host, ...tls } } : opts;
|
|
7614
|
+
}
|
|
7549
7615
|
/** Parse a NATS server URL (`nats://host:port`, `host:port`, a bare host, or a comma list — the
|
|
7550
7616
|
* first entry wins) into a host+port for {@link tcpInfoProbe}. Defaults the port to 4222. */
|
|
7551
7617
|
function hostPort(server) {
|