@cotal-ai/core 0.56.1 → 0.58.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.
Files changed (56) hide show
  1. package/dist/auth-provider.d.ts +13 -1
  2. package/dist/auth-provider.d.ts.map +1 -1
  3. package/dist/auth-provider.js.map +1 -1
  4. package/dist/endpoint-envelope.js +2 -2
  5. package/dist/endpoint-envelope.js.map +1 -1
  6. package/dist/endpoint-grants.d.ts +1 -1
  7. package/dist/endpoint-grants.d.ts.map +1 -1
  8. package/dist/endpoint-grants.js +9 -6
  9. package/dist/endpoint-grants.js.map +1 -1
  10. package/dist/endpoint-handle.d.ts.map +1 -1
  11. package/dist/endpoint-handle.js +5 -2
  12. package/dist/endpoint-handle.js.map +1 -1
  13. package/dist/endpoint-subjects.d.ts +6 -3
  14. package/dist/endpoint-subjects.d.ts.map +1 -1
  15. package/dist/endpoint-subjects.js +5 -5
  16. package/dist/endpoint-subjects.js.map +1 -1
  17. package/dist/endpoint-verbs.d.ts +1 -1
  18. package/dist/endpoint-verbs.d.ts.map +1 -1
  19. package/dist/endpoint-verbs.js +2 -2
  20. package/dist/endpoint-verbs.js.map +1 -1
  21. package/dist/endpoint.d.ts +27 -2
  22. package/dist/endpoint.d.ts.map +1 -1
  23. package/dist/endpoint.js +67 -8
  24. package/dist/endpoint.js.map +1 -1
  25. package/dist/kv-scan.d.ts +5 -2
  26. package/dist/kv-scan.d.ts.map +1 -1
  27. package/dist/kv-scan.js +103 -27
  28. package/dist/kv-scan.js.map +1 -1
  29. package/dist/members.d.ts +10 -0
  30. package/dist/members.d.ts.map +1 -1
  31. package/dist/members.js +28 -1
  32. package/dist/members.js.map +1 -1
  33. package/dist/membership-feed.d.ts.map +1 -1
  34. package/dist/membership-feed.js +19 -6
  35. package/dist/membership-feed.js.map +1 -1
  36. package/dist/provision.d.ts.map +1 -1
  37. package/dist/provision.js +28 -25
  38. package/dist/provision.js.map +1 -1
  39. package/dist/remote-manager-authority.d.ts +120 -1
  40. package/dist/remote-manager-authority.d.ts.map +1 -1
  41. package/dist/remote-manager-authority.js.map +1 -1
  42. package/dist/run-record.d.ts +19 -0
  43. package/dist/run-record.d.ts.map +1 -1
  44. package/dist/run-record.js +20 -0
  45. package/dist/run-record.js.map +1 -1
  46. package/dist/streams.d.ts +51 -1
  47. package/dist/streams.d.ts.map +1 -1
  48. package/dist/streams.js +214 -29
  49. package/dist/streams.js.map +1 -1
  50. package/dist/subjects.d.ts +22 -5
  51. package/dist/subjects.d.ts.map +1 -1
  52. package/dist/subjects.js +25 -6
  53. package/dist/subjects.js.map +1 -1
  54. package/dist/types.d.ts +2 -1
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +1 -1
package/dist/endpoint.js CHANGED
@@ -4,6 +4,7 @@ import { createConnection } from "node:net";
4
4
  import { connect, credsAuthenticator, headers, tokenAuthenticator, nanos, AuthorizationError, PermissionViolationError, UserAuthenticationExpiredError, NoRespondersError, RequestError, } from "@nats-io/transport-node";
5
5
  import { wsconnect } from "@nats-io/nats-core";
6
6
  import { credsClaims, credsFingerprint, credsRenewalDelayMs, idFromCreds } from "./identity.js";
7
+ import { requireBrokerFloor } from "./broker-floor.js";
7
8
  import { inspectCredHealth } from "./provision.js";
8
9
  import { parseSecretStoreIdentity, } from "./secret-store.js";
9
10
  import { resolveService, invokeCommand, submitAndFollowGoal } from "./endpoint-invoke.js";
@@ -19,7 +20,7 @@ import { jetstream, jetstreamManager, AckPolicy, DeliverPolicy, JetStreamApiCode
19
20
  import {} from "@nats-io/jetstream";
20
21
  import { Kvm } from "@nats-io/kv";
21
22
  import { Bucket, KvWatchInclude } from "@nats-io/kv/internal";
22
- import { openMembersRegistry, commitMember, tombstoneMember, activateMember, readMember, listMembers, durableEligible, StaleMembershipWrite, } from "./members.js";
23
+ import { openMembersRegistry, commitMember, tombstoneMember, activateMember, readMember, listMembers, listLifecycleMemberChannels, durableEligible, StaleMembershipWrite, } from "./members.js";
23
24
  import { openAclRegistry, readAcl, readAclForAlias, AmbiguousAclAlias, commitAcl as writeAclRecord, reissueAcl as writeAclReissue } from "./acls.js";
24
25
  import { openDeliveryRegistry } from "./lease.js";
25
26
  import { openChannelRegistry, effectiveReplay, effectiveReplayWindowMs, effectiveDeliveryClass, readChannelConfig, readChannelDefaults, } from "./channels.js";
@@ -143,6 +144,8 @@ export class CotalEndpoint extends EventEmitter {
143
144
  authExpiryReconnectTimer;
144
145
  sentinelCreds;
145
146
  tls;
147
+ transportPingIntervalMs;
148
+ transportMaxPingOut;
146
149
  heartbeatMs;
147
150
  ttlMs;
148
151
  doRegister;
@@ -483,6 +486,14 @@ export class CotalEndpoint extends EventEmitter {
483
486
  this.user = opts.user;
484
487
  this.pass = opts.pass;
485
488
  this.tls = opts.tls ?? false;
489
+ for (const [label, value] of [["transportPingIntervalMs", opts.transportPingIntervalMs], ["transportMaxPingOut", opts.transportMaxPingOut]]) {
490
+ if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0))
491
+ throw new Error(`EndpointOptions.${label} must be a positive safe integer`);
492
+ }
493
+ if ((opts.transportPingIntervalMs === undefined) !== (opts.transportMaxPingOut === undefined))
494
+ throw new Error("EndpointOptions transportPingIntervalMs and transportMaxPingOut must be configured together");
495
+ this.transportPingIntervalMs = opts.transportPingIntervalMs;
496
+ this.transportMaxPingOut = opts.transportMaxPingOut;
486
497
  // No implicit channel: an endpoint reads exactly what its caller lists. Omitted means none.
487
498
  this.channels = opts.channels ?? [];
488
499
  this.heartbeatMs = opts.heartbeatMs ?? 2000;
@@ -849,6 +860,7 @@ export class CotalEndpoint extends EventEmitter {
849
860
  CotalEndpoint.assertRenewableGeneration(candidate, this.currentCreds, delay);
850
861
  this.currentCreds = candidate;
851
862
  this.armCredsRefresh(delay);
863
+ this.emit("creds-adopted", credsClaims(candidate));
852
864
  return credsClaims(candidate);
853
865
  }
854
866
  /** The 75%-of-lifetime renewal timer tick: prove + adopt + swap the LIVE connection. Preflights (it
@@ -979,6 +991,8 @@ export class CotalEndpoint extends EventEmitter {
979
991
  // sub.allow=[_INBOX_<connId>.>] it stops a peer from subscribing the wildcard inbox to sniff
980
992
  // others' DM deliveries. Set unconditionally so the prefix can never drift from the ACL.
981
993
  inboxPrefix: `_INBOX_${this.connId}`,
994
+ ...(this.transportPingIntervalMs === undefined ? {} : { pingInterval: this.transportPingIntervalMs }),
995
+ ...(this.transportMaxPingOut === undefined ? {} : { maxPingOut: this.transportMaxPingOut }),
982
996
  // The bearer rides a GETTER: nats.js re-evaluates the token authenticator per (re)connect
983
997
  // attempt, so internal reconnects present whatever refreshBearer last fetched.
984
998
  // Creds ALWAYS ride the CHECKED getter, renewed or static, so every attempt — including the
@@ -992,6 +1006,9 @@ export class CotalEndpoint extends EventEmitter {
992
1006
  // instead. Anonymous access stays reachable the only way it should be: by passing no creds.
993
1007
  ...authOpts({ token: this.token, user: this.user, pass: this.pass, creds: this.currentCreds !== undefined || this.credsSource ? () => this.credsForWire() : undefined, bearer: this.userMode ? () => this.currentBearer : undefined, sentinelCreds: this.sentinelCreds, tls: this.tls }),
994
1008
  });
1009
+ // SPEC §13.12: the control surface requires nats-server >= 2.12; this runs on every
1010
+ // fresh connection, including the reconnects the library performs on its own here.
1011
+ requireBrokerFloor(this.nc);
995
1012
  this.armAuthExpiryReconnectFence(this.nc);
996
1013
  this.watchStatus();
997
1014
  this.js = jetstream(this.nc);
@@ -1759,6 +1776,14 @@ export class CotalEndpoint extends EventEmitter {
1759
1776
  }
1760
1777
  /** Unicast: direct message to one specific instance. */
1761
1778
  async unicast(instanceId, text, opts) {
1779
+ const { msg } = await this.unicastAttributed(instanceId, text, opts);
1780
+ return msg;
1781
+ }
1782
+ /** Unicast, returning the JetStream publish ack alongside the message: the only truthful proof
1783
+ * a sender has is that the broker stored the message at a sequence, never that anyone read it. */
1784
+ async unicastAttributed(instanceId, text, opts) {
1785
+ if (!this.js)
1786
+ throw new Error(this.notLiveMsg());
1762
1787
  const msg = {
1763
1788
  id: randomUUID(),
1764
1789
  ts: Date.now(),
@@ -1774,8 +1799,11 @@ export class CotalEndpoint extends EventEmitter {
1774
1799
  const recip = parsePrincipalKey(instanceId);
1775
1800
  if (!recip)
1776
1801
  throw new Error(`unicast: "${instanceId}" is not a valid recipient principal <owner>.<actor>`);
1777
- await this.publishMsg(unicastSubject(this.space, recip.owner, recip.actor, this.owner, this.actor), msg);
1778
- return msg;
1802
+ assertPartsSerializable(msg.parts);
1803
+ // Publish DIRECTLY rather than through publishMsg, the way multicastExpecting does: this path
1804
+ // must read the ack, and publishMsg deliberately discards it.
1805
+ const ack = await this.js.publish(unicastSubject(this.space, recip.owner, recip.actor, this.owner, this.actor), JSON.stringify(msg), { msgID: msg.id });
1806
+ return { msg, ack: { seq: ack.seq, duplicate: ack.duplicate === true } };
1779
1807
  }
1780
1808
  /** Anycast: deliver to ANY one instance of a service (role) — queue-group load balancing. */
1781
1809
  async anycast(service, text, opts) {
@@ -2233,17 +2261,23 @@ export class CotalEndpoint extends EventEmitter {
2233
2261
  /** Overlay the host's live model and optional variant onto the card's display-only metadata, then
2234
2262
  * republish presence. For connectors that learn their actual selection only after launch (e.g.
2235
2263
  * Claude Code's `SessionStart` hook). The mutated card is read live by every later publish, so even
2236
- * a pre-connect call surfaces on the first presence write. */
2237
- async setCardModel(model, variant) {
2264
+ * a pre-connect call surfaces on the first presence write. `provider` is the effective provider
2265
+ * the connector reported serving the model; when omitted an existing `meta.provider` is left
2266
+ * untouched (unlike `variant`, it is never deleted, since a caller that does not know the
2267
+ * provider says nothing about whether one still applies). */
2268
+ async setCardModel(model, variant, provider) {
2238
2269
  const m = model.trim();
2239
2270
  const v = variant?.trim();
2240
- if (!m || (this.card.meta?.model === m && this.card.meta.variant === v))
2271
+ const p = provider?.trim();
2272
+ if (!m || (this.card.meta?.model === m && this.card.meta.variant === v && (p === undefined || this.card.meta?.provider === p)))
2241
2273
  return;
2242
2274
  const meta = { ...(this.card.meta ?? {}), model: m };
2243
2275
  if (v)
2244
2276
  meta.variant = v;
2245
2277
  else
2246
2278
  delete meta.variant;
2279
+ if (p)
2280
+ meta.provider = p;
2247
2281
  this.card.meta = meta;
2248
2282
  await this.publishPresence();
2249
2283
  }
@@ -3988,7 +4022,7 @@ export class CotalEndpoint extends EventEmitter {
3988
4022
  async startPlane3(aclFor, opts = {}) {
3989
4023
  if (!this.js)
3990
4024
  throw new Error("endpoint not started");
3991
- this.plane3 = { aclFor, reloadMembershipCreds: opts.reloadMembershipCreds, evictPrincipal: opts.evictPrincipal, planeConnLiveness: opts.planeConnLiveness, principalLiveness: opts.principalLiveness, reloadStoreIdentity: opts.reloadStoreIdentity };
4025
+ this.plane3 = { aclFor, reloadMembershipCreds: opts.reloadMembershipCreds, evictPrincipal: opts.evictPrincipal, planeConnLiveness: opts.planeConnLiveness, principalLiveness: opts.principalLiveness, reloadStoreIdentity: opts.reloadStoreIdentity, onDeliveryCredsAdopted: opts.onDeliveryCredsAdopted };
3992
4026
  await this.armPlane3();
3993
4027
  }
3994
4028
  /** Serve one runtime durable-membership control request (the server-side delivery daemon). The caller
@@ -4388,8 +4422,10 @@ export class CotalEndpoint extends EventEmitter {
4388
4422
  // Arm the resident wire swap ONLY now — after BOTH proofs settled, right before the reply is
4389
4423
  // returned+responded — so a slow membership proof can never let the delivery reconnect strand
4390
4424
  // this reply. Only when delivery actually adopted a new candidate (currentCreds was updated).
4391
- if (delivery.ok)
4425
+ if (delivery.ok) {
4426
+ this.plane3?.onDeliveryCredsAdopted?.();
4392
4427
  this.scheduleResidentSwap();
4428
+ }
4393
4429
  return failures.length
4394
4430
  ? { ok: false, error: failures.join("; "), data: { delivery, membership } }
4395
4431
  : { ok: true, data: { delivery, membership } };
@@ -4425,6 +4461,29 @@ export class CotalEndpoint extends EventEmitter {
4425
4461
  return { ok: false, error: e.message };
4426
4462
  }
4427
4463
  }
4464
+ if (req.op === "lifecycleMemberships") {
4465
+ // The terminal teardown's membership INVENTORY: a complete, read-only, lifecycle-exact listing
4466
+ // of one principal's durable membership rows (tombstones included) from the trusted daemon that
4467
+ // owns the members bucket. The caller deletes only the exact keys it gets back, through its
4468
+ // target-pinned deprovisioner grant; this verb deletes nothing and returns no other lifecycle.
4469
+ const principal = typeof req.args?.principal === "string" ? req.args.principal.trim() : "";
4470
+ const uid = typeof req.args?.lifecycleUid === "string" ? req.args.lifecycleUid : "";
4471
+ if (!parsePrincipalKey(principal))
4472
+ return { ok: false, error: "lifecycleMemberships: a principal (owner.actor dot-form) is required" };
4473
+ try {
4474
+ assertLifecycleToken(uid);
4475
+ }
4476
+ catch (e) {
4477
+ return { ok: false, error: `lifecycleMemberships: ${e.message}` };
4478
+ }
4479
+ try {
4480
+ const channels = await listLifecycleMemberChannels(await this.membersRegistry(), principal, uid);
4481
+ return { ok: true, data: { complete: true, channels } };
4482
+ }
4483
+ catch (e) {
4484
+ return { ok: false, error: `lifecycleMemberships: the inventory read did not complete (${e.message})` };
4485
+ }
4486
+ }
4428
4487
  if (req.op === "principalLiveness") {
4429
4488
  // The freeze-holder liveness probe (#391): the READ-ONLY half of `evictPrincipal`. A repair
4430
4489
  // that must REFUSE while the holder is alive cannot use eviction as its own precheck — that