@cotal-ai/core 0.85.0 → 0.86.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 (153) hide show
  1. package/dist/agent-file.d.ts.map +1 -1
  2. package/dist/agent-file.js +10 -3
  3. package/dist/agent-file.js.map +1 -1
  4. package/dist/base64url.d.ts +5 -0
  5. package/dist/base64url.d.ts.map +1 -0
  6. package/dist/base64url.js +14 -0
  7. package/dist/base64url.js.map +1 -0
  8. package/dist/browser-control.d.ts +97 -0
  9. package/dist/browser-control.d.ts.map +1 -0
  10. package/dist/browser-control.js +66 -0
  11. package/dist/browser-control.js.map +1 -0
  12. package/dist/browser-validators-build.d.ts +23 -0
  13. package/dist/browser-validators-build.d.ts.map +1 -0
  14. package/dist/browser-validators-build.js +149 -0
  15. package/dist/browser-validators-build.js.map +1 -0
  16. package/dist/canonical-json.d.ts +14 -0
  17. package/dist/canonical-json.d.ts.map +1 -0
  18. package/dist/canonical-json.js +121 -0
  19. package/dist/canonical-json.js.map +1 -0
  20. package/dist/canonical.d.ts +2 -10
  21. package/dist/canonical.d.ts.map +1 -1
  22. package/dist/canonical.js +2 -108
  23. package/dist/canonical.js.map +1 -1
  24. package/dist/checkpoint-answer.d.ts.map +1 -1
  25. package/dist/checkpoint-answer.js +3 -4
  26. package/dist/checkpoint-answer.js.map +1 -1
  27. package/dist/connector-setup.d.ts +10 -1
  28. package/dist/connector-setup.d.ts.map +1 -1
  29. package/dist/digest-id.d.ts +4 -0
  30. package/dist/digest-id.d.ts.map +1 -0
  31. package/dist/digest-id.js +7 -0
  32. package/dist/digest-id.js.map +1 -0
  33. package/dist/endpoint-action.d.ts.map +1 -1
  34. package/dist/endpoint-action.js +2 -16
  35. package/dist/endpoint-action.js.map +1 -1
  36. package/dist/endpoint-auth-digest.d.ts +7 -0
  37. package/dist/endpoint-auth-digest.d.ts.map +1 -0
  38. package/dist/endpoint-auth-digest.js +13 -0
  39. package/dist/endpoint-auth-digest.js.map +1 -0
  40. package/dist/endpoint-call.d.ts +176 -0
  41. package/dist/endpoint-call.d.ts.map +1 -0
  42. package/dist/endpoint-call.js +490 -0
  43. package/dist/endpoint-call.js.map +1 -0
  44. package/dist/endpoint-caller.d.ts +113 -0
  45. package/dist/endpoint-caller.d.ts.map +1 -0
  46. package/dist/endpoint-caller.js +260 -0
  47. package/dist/endpoint-caller.js.map +1 -0
  48. package/dist/endpoint-checkpoint.d.ts.map +1 -1
  49. package/dist/endpoint-checkpoint.js +5 -14
  50. package/dist/endpoint-checkpoint.js.map +1 -1
  51. package/dist/endpoint-envelope.d.ts +0 -6
  52. package/dist/endpoint-envelope.d.ts.map +1 -1
  53. package/dist/endpoint-envelope.js +17 -18
  54. package/dist/endpoint-envelope.js.map +1 -1
  55. package/dist/endpoint-guard.d.ts.map +1 -1
  56. package/dist/endpoint-guard.js +2 -10
  57. package/dist/endpoint-guard.js.map +1 -1
  58. package/dist/endpoint-invoke.d.ts +36 -119
  59. package/dist/endpoint-invoke.d.ts.map +1 -1
  60. package/dist/endpoint-invoke.js +56 -300
  61. package/dist/endpoint-invoke.js.map +1 -1
  62. package/dist/endpoint-publish-denial.d.ts +1 -1
  63. package/dist/endpoint-publish-denial.d.ts.map +1 -1
  64. package/dist/endpoint-publish-denial.js +1 -1
  65. package/dist/endpoint-publish-denial.js.map +1 -1
  66. package/dist/endpoint-receipt.d.ts.map +1 -1
  67. package/dist/endpoint-receipt.js +2 -16
  68. package/dist/endpoint-receipt.js.map +1 -1
  69. package/dist/endpoint-subjects.d.ts.map +1 -1
  70. package/dist/endpoint-subjects.js.map +1 -1
  71. package/dist/endpoint-traits.d.ts.map +1 -1
  72. package/dist/endpoint-traits.js +3 -20
  73. package/dist/endpoint-traits.js.map +1 -1
  74. package/dist/endpoint-verbs.d.ts +18 -151
  75. package/dist/endpoint-verbs.d.ts.map +1 -1
  76. package/dist/endpoint-verbs.js +4 -500
  77. package/dist/endpoint-verbs.js.map +1 -1
  78. package/dist/endpoint-work.d.ts.map +1 -1
  79. package/dist/endpoint-work.js +2 -17
  80. package/dist/endpoint-work.js.map +1 -1
  81. package/dist/endpoint.d.ts +39 -15
  82. package/dist/endpoint.d.ts.map +1 -1
  83. package/dist/endpoint.js +90 -42
  84. package/dist/endpoint.js.map +1 -1
  85. package/dist/index.d.ts +4 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +4 -0
  88. package/dist/index.js.map +1 -1
  89. package/dist/issued-accepted.d.ts +27 -0
  90. package/dist/issued-accepted.d.ts.map +1 -0
  91. package/dist/issued-accepted.js +62 -0
  92. package/dist/issued-accepted.js.map +1 -0
  93. package/dist/issued-authority.d.ts +3 -18
  94. package/dist/issued-authority.d.ts.map +1 -1
  95. package/dist/issued-authority.js +4 -55
  96. package/dist/issued-authority.js.map +1 -1
  97. package/dist/lifecycle-state.d.ts +3 -0
  98. package/dist/lifecycle-state.d.ts.map +1 -1
  99. package/dist/lifecycle-state.js +7 -0
  100. package/dist/lifecycle-state.js.map +1 -1
  101. package/dist/manager-caller.d.ts.map +1 -1
  102. package/dist/manager-caller.js +2 -1
  103. package/dist/manager-caller.js.map +1 -1
  104. package/dist/one-line.d.ts +6 -0
  105. package/dist/one-line.d.ts.map +1 -0
  106. package/dist/one-line.js +8 -0
  107. package/dist/one-line.js.map +1 -0
  108. package/dist/peer-label.d.ts +16 -0
  109. package/dist/peer-label.d.ts.map +1 -0
  110. package/dist/peer-label.js +15 -0
  111. package/dist/peer-label.js.map +1 -0
  112. package/dist/permission-denied.d.ts +8 -0
  113. package/dist/permission-denied.d.ts.map +1 -0
  114. package/dist/permission-denied.js +15 -0
  115. package/dist/permission-denied.js.map +1 -0
  116. package/dist/presence-text.d.ts +6 -0
  117. package/dist/presence-text.d.ts.map +1 -0
  118. package/dist/presence-text.js +25 -0
  119. package/dist/presence-text.js.map +1 -0
  120. package/dist/provision.d.ts.map +1 -1
  121. package/dist/provision.js +8 -2
  122. package/dist/provision.js.map +1 -1
  123. package/dist/resolve.d.ts +2 -13
  124. package/dist/resolve.d.ts.map +1 -1
  125. package/dist/resolve.js +2 -12
  126. package/dist/resolve.js.map +1 -1
  127. package/dist/run-driver-grants.d.ts +5 -0
  128. package/dist/run-driver-grants.d.ts.map +1 -1
  129. package/dist/run-driver-grants.js +16 -4
  130. package/dist/run-driver-grants.js.map +1 -1
  131. package/dist/run-migration.d.ts +21 -0
  132. package/dist/run-migration.d.ts.map +1 -1
  133. package/dist/run-migration.js +2 -27
  134. package/dist/run-migration.js.map +1 -1
  135. package/dist/run-notice.d.ts +20 -0
  136. package/dist/run-notice.d.ts.map +1 -1
  137. package/dist/run-notice.js +3 -28
  138. package/dist/run-notice.js.map +1 -1
  139. package/dist/schema-bounds.d.ts +24 -0
  140. package/dist/schema-bounds.d.ts.map +1 -0
  141. package/dist/schema-bounds.js +74 -0
  142. package/dist/schema-bounds.js.map +1 -0
  143. package/dist/schema-profile.d.ts +11 -24
  144. package/dist/schema-profile.d.ts.map +1 -1
  145. package/dist/schema-profile.js +16 -73
  146. package/dist/schema-profile.js.map +1 -1
  147. package/dist/subjects.d.ts.map +1 -1
  148. package/dist/subjects.js +1 -2
  149. package/dist/subjects.js.map +1 -1
  150. package/dist/turn-contract.d.ts.map +1 -1
  151. package/dist/turn-contract.js +14 -4
  152. package/dist/turn-contract.js.map +1 -1
  153. package/package.json +14 -1
package/dist/endpoint.js CHANGED
@@ -29,6 +29,8 @@ import { openAclRegistry, readAcl, readAclForAlias, aclRetired, AmbiguousAclAlia
29
29
  import { openDeliveryRegistry } from "./lease.js";
30
30
  import { openChannelRegistry, effectiveReplay, effectiveReplayWindowMs, effectiveDeliveryClass, readChannelConfig, readChannelDefaults, decodeChannelEntry, } from "./channels.js";
31
31
  import { anycastSubject, assertValidRole, CHANNEL_DEFAULTS_KEY, chatStream, chatHistDurable, chatSubject, controlServiceSubject, livenessSubject, livenessServeFilter, CONTROL_DELIVERY, CONTROL_DELIVERY_ADMIN, dmStream, dmDurable, dlvStream, dlvDurable, dlvSubject, dinboxSubject, inboxStream, parseDinboxPrincipal, FANOUT_DURABLE, INBOX_READER_DURABLE, leaseKey, managerBucket, MANAGER_LEASE_KEY, MANAGER_RENEWAL_LEASE_KEY, managerLeaseKey, chatWildcard, assertValidChannel, channelInAllow, isConcreteChannel, normalizeMentions, parseSubject, isPrincipalOwnerToken, assertInboxConnId, presenceBucket, membershipBucket, MEMBERSHIP_FEED_KEY, principalKey, parsePrincipalKey, assertLifecycleToken, mintLifecycleUid, lifecycleNameKey, DEV_OWNER, spacePrefix, spaceWildcard, subjectMatches, taskStream, taskDurable, token, unicastSubject, unicastRecvFilter, } from "./subjects.js";
32
+ import { isPermissionDenied } from "./permission-denied.js";
33
+ export { isPermissionDenied };
32
34
  export const DEFAULT_SERVER = "nats://127.0.0.1:4222";
33
35
  const PLANE3_FRAME_HEADER = "Cotal-Delivery-Frame";
34
36
  /** Space joined when none is given on the CLI (the `cotal-<space>` cmux tab, etc.). */
@@ -178,6 +180,14 @@ export class CotalEndpoint extends EventEmitter {
178
180
  * live iterator. See {@link watchDeliveryLease}. */
179
181
  leaseWatchIntent;
180
182
  leaseWatchIter;
183
+ /** Consumer deletes of retired lease watches still in flight. The stop handle cannot await its
184
+ * delete, so {@link stop} and a live-epoch rebuild await these before they close the connection
185
+ * that carries them (#3966). */
186
+ leaseWatchDeletes = new Set();
187
+ /** Lease-watch binds still awaiting the broker. One that lands after {@link stop} or a rebuild
188
+ * moved the epoch retires its watch and starts that delete itself, so the same two paths wait for
189
+ * these first. Each is bounded by the JetStream request timeout. */
190
+ leaseWatchBinds = new Set();
181
191
  /** Plane-3 durable-membership registry KV — lazily opened by the privileged delivery daemon (or a
182
192
  * short-lived provisioner). */
183
193
  membersKv;
@@ -294,9 +304,10 @@ export class CotalEndpoint extends EventEmitter {
294
304
  presenceViewState = "unpopulated";
295
305
  /** A presence-watch rebind in flight (see {@link rebindStalePresenceWatch}); one at a time. */
296
306
  presenceRebind;
297
- /** Bumped by every connection-scoped teardown and by {@link stop}. A presence bind that was
298
- * awaiting the broker when the epoch moved belongs to a retired epoch: it releases the
299
- * iterator it got and installs nothing (see {@link startPresenceWatch}). */
307
+ /** Bumped by every connection-scoped teardown and by {@link stop}. A presence or lease-watch bind
308
+ * that was awaiting the broker when the epoch moved belongs to a retired epoch: it releases the
309
+ * iterator it got and installs nothing (see {@link startPresenceWatch} and
310
+ * {@link bindDeliveryLeaseWatch}). */
300
311
  presenceEpoch = 0;
301
312
  /** The current watch was bound onto a bucket with NO keys (see {@link markPresenceBucketEmpty}).
302
313
  * Such a watch cannot deliver until someone writes, so its silence is not staleness. */
@@ -1087,9 +1098,9 @@ export class CotalEndpoint extends EventEmitter {
1087
1098
  await this.startChannelWatch();
1088
1099
  }
1089
1100
  // Rebind the daemon's lease-loss watch onto the fresh connection (#1596). The intent survived
1090
- // clearConnectionScoped; a bind that completes after a stop or another rebuild found no intent
1091
- // and released itself. The replay re-delivers the row's current state, so a change that landed
1092
- // in the null window still reaches the trigger.
1101
+ // clearConnectionScoped; a bind that completes after a stop or another rebuild finds the epoch
1102
+ // moved and retires itself. The replay re-delivers the row's current state, so a change that
1103
+ // landed in the null window still reaches the trigger.
1093
1104
  if (this.leaseWatchIntent)
1094
1105
  await this.bindDeliveryLeaseWatch();
1095
1106
  // FAIL BEFORE PRESENCE (SPEC 13.1): an AUTHED endpoint that will register on the roster or
@@ -1432,7 +1443,7 @@ export class CotalEndpoint extends EventEmitter {
1432
1443
  // Manual reconnect still has a live old epoch: complete broker-consumer cleanup before close.
1433
1444
  // Terminal self-heal has an already-closed epoch: disarm retains stream/name for fresh cleanup.
1434
1445
  if (oldNc && !oldNc.isClosed())
1435
- await Promise.all([...this.membershipFeedWatches].map((watch) => watch.arm));
1446
+ await Promise.all([...[...this.membershipFeedWatches].map((watch) => watch.arm), this.settleDeliveryLeaseWatches()]);
1436
1447
  this.nc = undefined;
1437
1448
  this.js = undefined;
1438
1449
  this.jsm = undefined;
@@ -1629,6 +1640,7 @@ export class CotalEndpoint extends EventEmitter {
1629
1640
  catch {
1630
1641
  /* best-effort graceful leave */
1631
1642
  }
1643
+ await this.settleDeliveryLeaseWatches();
1632
1644
  await this.closeWithoutLibraryReconnect(this.nc);
1633
1645
  // #1356: stop() does its own teardown and never runs clearConnectionScoped, so clear here too —
1634
1646
  // AFTER the best-effort offline publish above, which would otherwise re-record a refusal on its
@@ -2397,6 +2409,18 @@ export class CotalEndpoint extends EventEmitter {
2397
2409
  this.card.meta = meta;
2398
2410
  await this.publishPresence();
2399
2411
  }
2412
+ /** Overlay the session's working directory onto the card's display-only `meta.cwd`, then
2413
+ * republish presence. For connectors that learn the directory from the harness after launch (e.g.
2414
+ * Claude Code's `SessionStart` hook) rather than from their own process. Advisory display data,
2415
+ * never a routing or access input. The pathname is kept byte for byte: a directory name may hold
2416
+ * or end in whitespace, so it is never trimmed or normalized. An empty string names no directory
2417
+ * and leaves the card unchanged. */
2418
+ async setCardCwd(cwd) {
2419
+ if (!cwd || this.card.meta?.cwd === cwd)
2420
+ return;
2421
+ this.card.meta = { ...(this.card.meta ?? {}), cwd };
2422
+ await this.publishPresence();
2423
+ }
2400
2424
  // ---- channel discovery ---------------------------------------------------
2401
2425
  /** This channel's registry config from the live local cache (undefined if unset). */
2402
2426
  getChannelConfig(channel) {
@@ -2677,8 +2701,6 @@ export class CotalEndpoint extends EventEmitter {
2677
2701
  watch.arm = watch.arm.catch(() => { }).then(async () => {
2678
2702
  await this.disarmMembershipWatch(watch);
2679
2703
  await this.deleteRetainedMembershipConsumer(watch);
2680
- if (watch.consumerStream || watch.consumerName)
2681
- return;
2682
2704
  this.finishMembershipWatchStop(watch);
2683
2705
  }).catch((err) => {
2684
2706
  // A real authorization/server failure remains loud. A terminal close is retained rather than
@@ -2772,10 +2794,10 @@ export class CotalEndpoint extends EventEmitter {
2772
2794
  const cut = subject.lastIndexOf("_") + 1;
2773
2795
  return /^\d+$/.test(subject.slice(cut)) && this.ownWatchDeletePrefixes.has(subject.slice(0, cut));
2774
2796
  }
2775
- /** Delete a history reader's own ephemeral consumer. A refused delete is the designed outcome for
2776
- * the elevated profile, which holds no stream-wide CONSUMER.DELETE (#691): the broker reaps the
2777
- * consumer at its inactive threshold. */
2778
- async deleteReaderConsumer(consumer) {
2797
+ /** Delete one of this connection's own ephemeral consumers, a history reader's or a KV watch's. A
2798
+ * refused delete is the designed outcome for a profile without a stream-wide CONSUMER.DELETE
2799
+ * (#691): the broker reaps the consumer at its inactive threshold. */
2800
+ async deleteOwnConsumer(consumer) {
2779
2801
  try {
2780
2802
  await consumer.delete();
2781
2803
  }
@@ -3185,7 +3207,7 @@ export class CotalEndpoint extends EventEmitter {
3185
3207
  throw new Error(`history: the broker reported messages on ${subjectLabel(subjects)} but delivered none - the read was cut short, not empty`);
3186
3208
  }
3187
3209
  finally {
3188
- await this.deleteReaderConsumer(consumer);
3210
+ await this.deleteOwnConsumer(consumer);
3189
3211
  }
3190
3212
  }
3191
3213
  /** The newest stream sequence matching `subject`, or 0 when the subject has no messages.
@@ -3225,7 +3247,7 @@ export class CotalEndpoint extends EventEmitter {
3225
3247
  throw new Error(`history: the broker reported messages on ${subjectLabel(subjects)} but delivered none - the read was cut short, not empty`);
3226
3248
  }
3227
3249
  finally {
3228
- await this.deleteReaderConsumer(consumer);
3250
+ await this.deleteOwnConsumer(consumer);
3229
3251
  }
3230
3252
  }
3231
3253
  /** Drain every message matching `subject` with sequence in `[start, ceiling]`, oldest-first.
@@ -3302,7 +3324,7 @@ export class CotalEndpoint extends EventEmitter {
3302
3324
  // resulting resource exhaustion would land in streamHistory's catch and read as empty history.
3303
3325
  // The observer and admin profiles may not delete (#691), so the dashboard's readers do live out
3304
3326
  // that threshold; only a profile holding the delete row reclaims them here.
3305
- await this.deleteReaderConsumer(consumer).finally(() => { this.ownWatchDeletePrefixes.delete(readerPrefix); });
3327
+ await this.deleteOwnConsumer(consumer).finally(() => { this.ownWatchDeletePrefixes.delete(readerPrefix); });
3306
3328
  }
3307
3329
  }
3308
3330
  // ---- internals -----------------------------------------------------------
@@ -3700,20 +3722,49 @@ export class CotalEndpoint extends EventEmitter {
3700
3722
  };
3701
3723
  }
3702
3724
  stopDeliveryLeaseWatch() {
3725
+ if (this.leaseWatchIter)
3726
+ this.retireDeliveryLeaseWatch(this.leaseWatchIter);
3727
+ this.leaseWatchIter = undefined;
3728
+ }
3729
+ /** Stop a lease watch and delete its ordered consumer, which the stop alone leaves on the broker
3730
+ * until its five-minute inactive threshold (#3966). A connection that closed on its own cannot
3731
+ * carry the delete, so that consumer is left to the threshold. A failed delete is a warning: the
3732
+ * broker still reaps the consumer, and the daemon's synchronous stop handle has no caller to
3733
+ * reject. */
3734
+ retireDeliveryLeaseWatch(iter) {
3703
3735
  try {
3704
- this.leaseWatchIter?.stop();
3736
+ iter.stop();
3705
3737
  }
3706
3738
  catch { /* already closed with its connection */ }
3707
- this.leaseWatchIter = undefined;
3708
- }
3709
- async bindDeliveryLeaseWatch() {
3739
+ if (!this.nc || this.nc.isClosed())
3740
+ return;
3741
+ const deleting = this.deleteOwnConsumer(kvWatchConsumer(iter)).catch((e) => this.emitRecoverable(new Error(`a retired delivery lease watch consumer was not deleted (${e.message}); the broker reaps it at its inactive threshold`)));
3742
+ this.leaseWatchDeletes.add(deleting);
3743
+ void deleting.then(() => this.leaseWatchDeletes.delete(deleting));
3744
+ }
3745
+ /** Wait for the lease-watch binds still in flight, then for every retired watch's consumer delete.
3746
+ * {@link stop} and a live-epoch rebuild run this before they close the connection those deletes
3747
+ * need (#3966). */
3748
+ async settleDeliveryLeaseWatches() {
3749
+ await Promise.allSettled(this.leaseWatchBinds);
3750
+ await Promise.all(this.leaseWatchDeletes);
3751
+ }
3752
+ /** Bind the lease watch on the current connection. A bind that lands after a newer watch replaced
3753
+ * its intent, or after a stop or connection teardown moved the epoch, retires its watch instead of
3754
+ * installing it: the epoch it was bound for is gone, and a rebuilt one binds its own. */
3755
+ bindDeliveryLeaseWatch() {
3756
+ const binding = this.bindDeliveryLeaseWatchOnce();
3757
+ const settled = () => { this.leaseWatchBinds.delete(binding); };
3758
+ this.leaseWatchBinds.add(binding);
3759
+ void binding.then(settled, settled);
3760
+ return binding;
3761
+ }
3762
+ async bindDeliveryLeaseWatchOnce() {
3710
3763
  const intent = this.leaseWatchIntent;
3764
+ const epoch = this.presenceEpoch;
3711
3765
  const iter = await (await this.deliveryRegistry()).watch({ key: leaseKey(intent.shardIndex) });
3712
- if (this.leaseWatchIntent !== intent) {
3713
- try {
3714
- iter.stop();
3715
- }
3716
- catch { /* already closed */ }
3766
+ if (this.leaseWatchIntent !== intent || epoch !== this.presenceEpoch) {
3767
+ this.retireDeliveryLeaseWatch(iter);
3717
3768
  return;
3718
3769
  }
3719
3770
  this.leaseWatchIter = iter;
@@ -6311,7 +6362,7 @@ export class CotalEndpoint extends EventEmitter {
6311
6362
  }
6312
6363
  if (!isUsablePresence(p)) {
6313
6364
  this.presenceBindingDrops++;
6314
- this.emitRecoverable(new Error(`dropped presence entry for key ${JSON.stringify(e.key)}: not a record with a string card.name, a known status and a finite ts`));
6365
+ this.emitRecoverable(new Error(`dropped presence entry for key ${JSON.stringify(e.key)}: not a record with a string card.name, a known status, a finite ts, string display fields and a condition with a string code`));
6315
6366
  return;
6316
6367
  }
6317
6368
  this.applyPresence(e.key, p);
@@ -6738,12 +6789,22 @@ const PRESENCE_STATUSES = { idle: true, waiting: true, working: true, offline: t
6738
6789
  * such as `"nope"` makes the liveness arithmetic NaN, and a JSON `1e400` parses as `Infinity`;
6739
6790
  * neither age is ever greater than the TTL, so the row would stay live after its key expired
6740
6791
  * (#2612). `card.name` is the roster sort key: a non-string throws inside the watch loop and
6741
- * ends the watch. `status` decides offline and is what roster consumers read. `card.id` is left
6742
- * to the KV-key check, which rejects anything but the key string. */
6792
+ * ends the watch. `status` decides offline and is what roster consumers read. `card.role`,
6793
+ * `activity`, `attention` and `condition.code` are printed as text, and the peer writes its own
6794
+ * row: an object such as `{"toString":null,"valueOf":null}` throws on conversion and would fail
6795
+ * every reader's roster. A present `condition` must be an object with a string `code`, which SPEC
6796
+ * §6 makes required: `null`, a bare string or an object with no code is dropped too. `card.id` is
6797
+ * left to the KV-key check, which rejects anything but the key string. */
6743
6798
  function isUsablePresence(value) {
6744
6799
  return isRecord(value) && isRecord(value.card) && typeof value.card.name === "string" &&
6800
+ isOptionalString(value.card.role) &&
6745
6801
  typeof value.status === "string" && Object.hasOwn(PRESENCE_STATUSES, value.status) &&
6746
- typeof value.ts === "number" && Number.isFinite(value.ts);
6802
+ typeof value.ts === "number" && Number.isFinite(value.ts) &&
6803
+ isOptionalString(value.activity) && isOptionalString(value.attention) &&
6804
+ (value.condition === undefined || (isRecord(value.condition) && typeof value.condition.code === "string"));
6805
+ }
6806
+ function isOptionalString(value) {
6807
+ return value === undefined || typeof value === "string";
6747
6808
  }
6748
6809
  /** Shallow-equal two per-channel-mode maps (presence dedup): a change must re-emit, so an attention
6749
6810
  * toggle isn't swallowed as a quiet heartbeat. Absent and empty compare equal. */
@@ -6837,19 +6898,6 @@ function describeStatusError(err) {
6837
6898
  }
6838
6899
  return err;
6839
6900
  }
6840
- /** True when a failure is a NATS *permission denial* — the subject is forbidden to this
6841
- * endpoint's creds — rather than a missing responder or a timeout. The two need opposite
6842
- * fixes (grant the capability vs. start/await the service), so callers (e.g. a control
6843
- * request that can't reach the manager) must tell them apart instead of defaulting to
6844
- * "service down". Unwraps a wrapped `cause` and falls back to the server's error text, since
6845
- * a denied publish can surface either as the typed error or inside a request rejection. */
6846
- export function isPermissionDenied(e) {
6847
- if (e instanceof PermissionViolationError)
6848
- return true;
6849
- if (e?.cause instanceof PermissionViolationError)
6850
- return true;
6851
- return /permissions?\s+violation/i.test(String(e?.message ?? ""));
6852
- }
6853
6901
  /** The ordered consumer behind a KV watch, which nats.js keeps on the iterator and the KV types hide. */
6854
6902
  function kvWatchConsumer(iter) {
6855
6903
  const consumer = iter._data;