@cotal-ai/core 0.69.0 → 0.70.1

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.
Files changed (91) hide show
  1. package/dist/auth-provider.d.ts +9 -2
  2. package/dist/auth-provider.d.ts.map +1 -1
  3. package/dist/auth-provider.js +10 -0
  4. package/dist/auth-provider.js.map +1 -1
  5. package/dist/channels.js +1 -1
  6. package/dist/channels.js.map +1 -1
  7. package/dist/connector-setup.d.ts +9 -5
  8. package/dist/connector-setup.d.ts.map +1 -1
  9. package/dist/contract-manifest.d.ts +20 -0
  10. package/dist/contract-manifest.d.ts.map +1 -0
  11. package/dist/contract-manifest.js +28 -0
  12. package/dist/contract-manifest.js.map +1 -0
  13. package/dist/endpoint-action.d.ts +3 -0
  14. package/dist/endpoint-action.d.ts.map +1 -1
  15. package/dist/endpoint-action.js +8 -6
  16. package/dist/endpoint-action.js.map +1 -1
  17. package/dist/endpoint-binding.d.ts.map +1 -1
  18. package/dist/endpoint-binding.js +5 -0
  19. package/dist/endpoint-binding.js.map +1 -1
  20. package/dist/endpoint-cluster.d.ts.map +1 -1
  21. package/dist/endpoint-cluster.js +9 -4
  22. package/dist/endpoint-cluster.js.map +1 -1
  23. package/dist/endpoint-contract-store.d.ts +2 -18
  24. package/dist/endpoint-contract-store.d.ts.map +1 -1
  25. package/dist/endpoint-contract-store.js +3 -19
  26. package/dist/endpoint-contract-store.js.map +1 -1
  27. package/dist/endpoint-envelope.d.ts +6 -2
  28. package/dist/endpoint-envelope.d.ts.map +1 -1
  29. package/dist/endpoint-envelope.js +19 -2
  30. package/dist/endpoint-envelope.js.map +1 -1
  31. package/dist/endpoint-error.d.ts +13 -0
  32. package/dist/endpoint-error.d.ts.map +1 -1
  33. package/dist/endpoint-error.js +15 -0
  34. package/dist/endpoint-error.js.map +1 -1
  35. package/dist/endpoint-invoke.d.ts +5 -2
  36. package/dist/endpoint-invoke.d.ts.map +1 -1
  37. package/dist/endpoint-invoke.js +16 -6
  38. package/dist/endpoint-invoke.js.map +1 -1
  39. package/dist/endpoint-records.d.ts +2 -1
  40. package/dist/endpoint-records.d.ts.map +1 -1
  41. package/dist/endpoint-records.js +1 -1
  42. package/dist/endpoint-records.js.map +1 -1
  43. package/dist/endpoint-serve.d.ts.map +1 -1
  44. package/dist/endpoint-serve.js +4 -3
  45. package/dist/endpoint-serve.js.map +1 -1
  46. package/dist/endpoint.d.ts +9 -4
  47. package/dist/endpoint.d.ts.map +1 -1
  48. package/dist/endpoint.js +47 -37
  49. package/dist/endpoint.js.map +1 -1
  50. package/dist/evict.d.ts.map +1 -1
  51. package/dist/evict.js +2 -1
  52. package/dist/evict.js.map +1 -1
  53. package/dist/identity.d.ts +3 -0
  54. package/dist/identity.d.ts.map +1 -1
  55. package/dist/identity.js +5 -0
  56. package/dist/identity.js.map +1 -1
  57. package/dist/kv-scan.d.ts.map +1 -1
  58. package/dist/kv-scan.js +1 -6
  59. package/dist/kv-scan.js.map +1 -1
  60. package/dist/provision.d.ts +23 -24
  61. package/dist/provision.d.ts.map +1 -1
  62. package/dist/provision.js +112 -116
  63. package/dist/provision.js.map +1 -1
  64. package/dist/remote-manager-authority.d.ts +22 -2
  65. package/dist/remote-manager-authority.d.ts.map +1 -1
  66. package/dist/remote-manager-authority.js +23 -15
  67. package/dist/remote-manager-authority.js.map +1 -1
  68. package/dist/resolve.d.ts +15 -2
  69. package/dist/resolve.d.ts.map +1 -1
  70. package/dist/resolve.js +15 -2
  71. package/dist/resolve.js.map +1 -1
  72. package/dist/run-driver-grants.d.ts.map +1 -1
  73. package/dist/run-driver-grants.js +4 -1
  74. package/dist/run-driver-grants.js.map +1 -1
  75. package/dist/run-journal.d.ts.map +1 -1
  76. package/dist/run-journal.js +17 -2
  77. package/dist/run-journal.js.map +1 -1
  78. package/dist/run-record.d.ts +6 -8
  79. package/dist/run-record.d.ts.map +1 -1
  80. package/dist/run-record.js.map +1 -1
  81. package/dist/schema-profile.d.ts +11 -0
  82. package/dist/schema-profile.d.ts.map +1 -1
  83. package/dist/schema-profile.js +13 -2
  84. package/dist/schema-profile.js.map +1 -1
  85. package/dist/secret-fs.d.ts +4 -3
  86. package/dist/secret-fs.d.ts.map +1 -1
  87. package/dist/secret-fs.js +10 -11
  88. package/dist/secret-fs.js.map +1 -1
  89. package/dist/streams.d.ts +2 -2
  90. package/dist/streams.js +2 -2
  91. package/package.json +5 -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 { createConnection } from "node:net";
4
- import { connect, credsAuthenticator, headers, tokenAuthenticator, nanos, AuthorizationError, PermissionViolationError, UserAuthenticationExpiredError, NoRespondersError, RequestError, } from "@nats-io/transport-node";
4
+ import { connect, credsAuthenticator, headers, tokenAuthenticator, nanos, AuthorizationError, PermissionViolationError, UserAuthenticationExpiredError, NoRespondersError, RequestError, TimeoutError, ClosedConnectionError, } from "@nats-io/transport-node";
5
5
  import { wsconnect } from "@nats-io/nats-core";
6
6
  import { parseLivenessAnswer, responderFromLease, responderFromProbe, isLivenessPlane, LIVENESS_PLANES, } from "./liveness.js";
7
7
  import { credsClaims, credsFingerprint, credsRenewalDelayMs, idFromCreds } from "./identity.js";
@@ -9,7 +9,7 @@ import { requireBrokerFloor } from "./broker-floor.js";
9
9
  import { inspectCredHealth } from "./provision.js";
10
10
  import { parseSecretStoreIdentity, } from "./secret-store.js";
11
11
  import { BIND_SPLIT_REISSUES, resolveService, invokeCommand, submitAndFollowGoal } from "./endpoint-invoke.js";
12
- import { EpEnvelopeError, respondedButUnbound, replyRefusedBeforeEffect, EP_BIND_REFUSED } from "./endpoint-envelope.js";
12
+ import { EpEnvelopeError, respondedButUnbound, replyRefusedBeforeEffect, replyTargetUnmapped, EP_BIND_REFUSED } from "./endpoint-envelope.js";
13
13
  import { isRepeatSafeCommand } from "./endpoint-grants.js";
14
14
  import { assertIdToken, assertGeneration } from "./endpoint-subjects.js";
15
15
  import { readAcceptedRow } from "./issued-authority.js";
@@ -363,8 +363,9 @@ export class CotalEndpoint extends EventEmitter {
363
363
  /** Active goal followers awaiting outcome, tracked so endpoint stop can cancel them immediately
364
364
  * and connection replacement / reconnect can trigger reconciliation. */
365
365
  activeGoalFollowers = new Set();
366
- /** How many calls {@link invokeService} has silently recovered from a bind refusal (§13.2) — the
367
- * class-queue splits this endpoint hit and survived.
366
+ /** How many calls {@link invokeService} has silently recovered from a bind refusal (§13.2) or a
367
+ * member holding no mapping for the target (§13.3) — the class-queue splits this endpoint hit
368
+ * and survived.
368
369
  *
369
370
  * Counted because it is recovered: handling the split is what makes it invisible, so this is the
370
371
  * only evidence the split rate exists. Always on, never behind a flag — a counter you have to
@@ -1907,8 +1908,9 @@ export class CotalEndpoint extends EventEmitter {
1907
1908
  this.emit("error", new Error(`rejected ${service} request on ${m.subject}: reply target "${m.reply ?? "(none)"}" is not under the sender's own reply subtree`));
1908
1909
  continue;
1909
1910
  }
1910
- let reply;
1911
+ let body;
1911
1912
  try {
1913
+ let reply;
1912
1914
  const req = m.json();
1913
1915
  // Authenticity guard (fail closed): control is the most privileged surface
1914
1916
  // (start/stop). The sender is encoded in the subject (ctl.<svc>.<sender>), which
@@ -1923,15 +1925,25 @@ export class CotalEndpoint extends EventEmitter {
1923
1925
  else {
1924
1926
  reply = await handler(req);
1925
1927
  }
1928
+ body = JSON.stringify(reply);
1926
1929
  }
1927
1930
  catch (e) {
1928
- reply = { ok: false, error: e.message };
1931
+ body = JSON.stringify({ ok: false, error: e.message });
1929
1932
  }
1930
1933
  try {
1931
- m.respond(JSON.stringify(reply));
1934
+ m.respond(body);
1932
1935
  }
1933
- catch {
1934
- /* no reply inbox */
1936
+ catch (e) {
1937
+ // `respond` signals a missing reply inbox by returning false; it throws when the reply exceeds the
1938
+ // broker's `max_payload`. Silence would leave the caller a bare timeout it cannot tell apart from
1939
+ // a dead service.
1940
+ const refused = { ok: false, error: `the ${service} reply is ${Buffer.byteLength(body)} bytes and was not published: ${e.message}` };
1941
+ try {
1942
+ m.respond(JSON.stringify(refused));
1943
+ }
1944
+ catch (err) {
1945
+ this.emit("error", new Error(`could not answer ${service} request on ${m.subject}: ${refused.error}, and the refusal failed too: ${err.message}`));
1946
+ }
1935
1947
  }
1936
1948
  }
1937
1949
  })().catch((e) => this.emit("error", e));
@@ -2032,7 +2044,9 @@ export class CotalEndpoint extends EventEmitter {
2032
2044
  * call re-issued for any command, up to {@link BIND_SPLIT_REISSUES} times while each re-issue
2033
2045
  * is refused the same way. If a re-issue cannot be resolved, the refusal
2034
2046
  * surfaces — still saying the command did not run — naming the resolve failure as why the
2035
- * repair could not be attempted.
2047
+ * repair could not be attempted. An unpinned targeted call that reached a member holding no
2048
+ * mapping for its target ({@link replyTargetUnmapped}) is repaired the same way, and when the
2049
+ * re-issues run out, the latest such refusal is the one surfaced.
2036
2050
  * - this CLIENT caught it on the reply ({@link respondedButUnbound}: a different instance,
2037
2051
  * `failed-precondition`; the same instance at any other epoch, `expired`), which is what a
2038
2052
  * responder too old to know the field produces. A live instance received and answered it, so
@@ -2103,7 +2117,14 @@ export class CotalEndpoint extends EventEmitter {
2103
2117
  // did not run, so each re-issue is a FIRST attempt, not a second. It repeats up to the bound
2104
2118
  // the CLI uses, because the re-issue rides the same class queue and splits again at the same
2105
2119
  // rate; repairing once left a quarter of all calls in a two-manager space failing (#443).
2106
- for (let reissues = 0; reissues < BIND_SPLIT_REISSUES && r.reply.ok === false && replyRefusedBeforeEffect(r.reply.error); reissues++) {
2120
+ // A member that holds no mapping for the target also ran nothing, and on the class rail a
2121
+ // sibling may host it. A pinned call named its instance, so that refusal is its answer.
2122
+ // That member looked the target up, so when the re-issues run out its refusal is surfaced
2123
+ // in place of a trailing bind refusal, which says nothing about the target.
2124
+ const unmappedHere = (x) => opts.instanceId === undefined && replyTargetUnmapped(x.reply.error);
2125
+ let unmapped = unmappedHere(r) ? r : undefined;
2126
+ for (let reissues = 0; reissues < BIND_SPLIT_REISSUES && r.reply.ok === false
2127
+ && (replyRefusedBeforeEffect(r.reply.error) || unmappedHere(r)); reissues++) {
2107
2128
  // Counted before it is repaired: a recovery that leaves no trace takes the split rate with it.
2108
2129
  this.splitsRecovered++;
2109
2130
  // `boundTo` is the other half of `servedBy`: who the handle THOUGHT it was talking to,
@@ -2154,8 +2175,10 @@ export class CotalEndpoint extends EventEmitter {
2154
2175
  "not-executed");
2155
2176
  }
2156
2177
  r = await invokeResolved(reissueTarget, signal);
2178
+ if (unmappedHere(r))
2179
+ unmapped = r;
2157
2180
  }
2158
- return r;
2181
+ return replyRefusedBeforeEffect(r.reply.error) ? unmapped ?? r : r;
2159
2182
  }
2160
2183
  catch (e) {
2161
2184
  // DO NOT auto-retry a command a responder already ANSWERED. This path covers the responders
@@ -2762,8 +2785,8 @@ export class CotalEndpoint extends EventEmitter {
2762
2785
  watch.consumerName = undefined;
2763
2786
  }
2764
2787
  else {
2765
- const closedEpoch = err.name === "ClosedConnectionError" || /^closed connection$/i.test(err.message);
2766
- const timeout = isTimeoutError(err);
2788
+ const closedEpoch = err instanceof ClosedConnectionError;
2789
+ const timeout = err instanceof TimeoutError;
2767
2790
  const dyingEpochTimeout = timeout && (this.reconnecting || !this.nc || this.nc.isClosed());
2768
2791
  // Cleanup of an ordered consumer: a delete timeout means the broker did not answer in time,
2769
2792
  // not that the endpoint is unusable. The broker reaps an idle/ephemeral consumer anyway.
@@ -4361,9 +4384,8 @@ export class CotalEndpoint extends EventEmitter {
4361
4384
  const reachedStart = page.length <= limit;
4362
4385
  const wanted = reachedStart ? page : page.slice(-limit);
4363
4386
  // A page that cannot be SENT is not a page. The reply rides one NATS message, so `limit` alone is
4364
- // the wrong bound: 200 large messages serialize past `max_payload`, `m.respond` throws inside
4365
- // `serveControl`'s swallow, and the caller sees a bare request timeout it cannot tell apart from a
4366
- // dead daemon. Measured, not predicted: 200 x ~6 KB timed out at 5s with the message "timeout".
4387
+ // the wrong bound: 200 large messages serialize past `max_payload`, and `serveControl` can then only
4388
+ // answer with a refusal in place of the page.
4367
4389
  //
4368
4390
  // So bound by BYTES too, keeping the NEWEST that fit — which needs no new vocabulary, because
4369
4391
  // `complete: false` already means "older history remains behind this page". Trimming here is the
@@ -4705,9 +4727,7 @@ export class CotalEndpoint extends EventEmitter {
4705
4727
  probeFailureOutcome(e) {
4706
4728
  if (e instanceof AuthorizationError || e instanceof PermissionViolationError)
4707
4729
  return "refused";
4708
- const name = e?.name;
4709
- const msg = e?.message ?? "";
4710
- if (name === "TimeoutError" || /timeout/i.test(msg))
4730
+ if (e instanceof TimeoutError)
4711
4731
  return "timeout";
4712
4732
  // Anything else is a transport or client failure: it says something about our link, not about
4713
4733
  // the plane, so it is graded like a refusal rather than guessed at.
@@ -5450,7 +5470,9 @@ export class CotalEndpoint extends EventEmitter {
5450
5470
  await this.pump(taskStream(this.space), taskDurable(this.card.role));
5451
5471
  }
5452
5472
  }
5453
- /** Drive one consumer: decode, drop our own echo, and hand each message to listeners with ack control. */
5473
+ /** Drive one consumer: decode and hand each message to listeners with ack control. Our own sends are
5474
+ * delivered too: on the DM inbox and the role queue they were addressed to us, so none is an echo,
5475
+ * and acking one unseen on the work queue would delete the only copy of the request. */
5454
5476
  async pump(stream, durable) {
5455
5477
  if (!this.js)
5456
5478
  throw new Error("endpoint not started");
@@ -5484,10 +5506,6 @@ export class CotalEndpoint extends EventEmitter {
5484
5506
  `does not match subject sender ${parsed?.sender ?? "(unparseable)"}`));
5485
5507
  continue;
5486
5508
  }
5487
- if (msg.from.id === this.card.id) {
5488
- m.ack(); // our own echo — advance past it
5489
- continue;
5490
- }
5491
5509
  // No-replay + dedup (chat only): drop a message at/below this channel's join watermark
5492
5510
  // — pre-join history the New tail still carries for a *lagging* joiner (cursor behind the
5493
5511
  // frontier), and the overlap a replay backfill already delivered. Must ack, or JetStream
@@ -7043,14 +7061,6 @@ export async function isReachable(servers = DEFAULT_SERVER, opts = {}) {
7043
7061
  return e instanceof AuthorizationError || e instanceof UserAuthenticationExpiredError;
7044
7062
  }
7045
7063
  }
7046
- /** True when `err` is a dial/consumer-op timeout rather than a real refusal — the one shared test
7047
- * for "the operation ran out of its own budget", used both by {@link classifyProbeFailure} (#851:
7048
- * a probe timeout must never collapse into `unreachable`, which a TLS-required target then
7049
- * misreads as a trust failure) and by {@link Endpoint#disarmMembershipWatch}'s consumer-delete
7050
- * cleanup, which predates it. */
7051
- function isTimeoutError(err) {
7052
- return err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(err.message));
7053
- }
7054
7064
  /** Like {@link isReachable}, but distinguishes "up but won't take these creds" from "nothing there".
7055
7065
  * `spawn` needs the difference: auth-required → name the trust dir + next step; unreachable → the
7056
7066
  * mesh is down (prune the stale entry, tell the user to `cotal up`). Pass `creds` to confirm a
@@ -7116,10 +7126,10 @@ function classifyProbeFailure(e, opts) {
7116
7126
  // The broker answered but rejected these creds (so it IS up) — auth-required, not stale-auth.
7117
7127
  if (e instanceof AuthorizationError)
7118
7128
  return { ok: false, reason: "auth-required" };
7119
- // A dial that ran out of its own budget is neither a refusal nor a dead broker — it is latency.
7120
- // `e` is undefined when the tcpDialable gate refused before any connect() attempt; that path has
7121
- // no timeout to inspect and must stay `unreachable` (nothing answered at all).
7122
- if (e !== undefined && isTimeoutError(e))
7129
+ // A dial that ran out of its own budget is neither a refusal nor a dead broker — it is latency,
7130
+ // and grading it `unreachable` made a TLS-required target read as a trust failure (#851). Only
7131
+ // nats-core's TimeoutError means that: a peer's `-ERR` that mentions a timeout answered at once.
7132
+ if (e instanceof TimeoutError)
7123
7133
  return { ok: false, reason: "timeout" };
7124
7134
  return { ok: false, reason: "unreachable" };
7125
7135
  }