@cotal-ai/core 0.101.6 → 0.102.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/auth-provider.d.ts +6 -3
- package/dist/auth-provider.d.ts.map +1 -1
- package/dist/auth-provider.js +6 -4
- package/dist/auth-provider.js.map +1 -1
- package/dist/endpoint.d.ts +53 -20
- package/dist/endpoint.d.ts.map +1 -1
- package/dist/endpoint.js +149 -58
- package/dist/endpoint.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/issuer-session.d.ts +33 -7
- package/dist/issuer-session.d.ts.map +1 -1
- package/dist/issuer-session.js +113 -12
- package/dist/issuer-session.js.map +1 -1
- package/dist/provision.js +1 -1
- package/dist/provision.js.map +1 -1
- package/dist/renewal.d.ts +23 -0
- package/dist/renewal.d.ts.map +1 -0
- package/dist/renewal.js +86 -0
- package/dist/renewal.js.map +1 -0
- package/dist/streams.d.ts +3 -1
- package/dist/streams.d.ts.map +1 -1
- package/dist/streams.js +10 -7
- package/dist/streams.js.map +1 -1
- package/dist/turn-contract.d.ts +13 -0
- package/dist/turn-contract.d.ts.map +1 -1
- package/dist/turn-contract.js +21 -0
- package/dist/turn-contract.js.map +1 -1
- package/package.json +13 -1
package/dist/endpoint.js
CHANGED
|
@@ -7,6 +7,7 @@ import { wsconnect } from "@nats-io/nats-core";
|
|
|
7
7
|
import { parseLivenessAnswer, responderFromLease, responderFromProbe, isLivenessPlane, LIVENESS_PLANES, } from "./liveness.js";
|
|
8
8
|
import { processRuntime } from "./process-runtime.js";
|
|
9
9
|
import { credsClaims, credsFingerprint, credsRenewalDelayMs, idFromCreds } from "./identity.js";
|
|
10
|
+
import { armRenewal, renewalDeadlineMs, jitteredBackoffMs } from "./renewal.js";
|
|
10
11
|
import { requireBrokerFloor } from "./broker-floor.js";
|
|
11
12
|
import { withDeadline } from "./deadline.js";
|
|
12
13
|
import { inspectCredHealth } from "./provision.js";
|
|
@@ -180,7 +181,15 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
180
181
|
bearerSource;
|
|
181
182
|
/** The freshest bearer — what every (re)connect attempt presents. */
|
|
182
183
|
currentBearer;
|
|
183
|
-
|
|
184
|
+
/** Cancels the armed bearer renewal or retry. */
|
|
185
|
+
cancelBearerRenewal;
|
|
186
|
+
/** Consecutive failed bearer renewals: the retry's step, and whether this run of failures has
|
|
187
|
+
* already been surfaced. */
|
|
188
|
+
bearerRenewalFailures = 0;
|
|
189
|
+
/** Whether the expired-bearer refusal has been surfaced for this outage. */
|
|
190
|
+
bearerRefusalSurfaced = false;
|
|
191
|
+
/** When the held bearer was adopted: its lifetime's start when it carries no `iat`. */
|
|
192
|
+
bearerAdoptedAtMs = 0;
|
|
184
193
|
/** Arms against the credential authenticated on the current wire. nats-core 3.4.0 discards the
|
|
185
194
|
* promise returned by its async transport-close continuation; if that continuation enters the
|
|
186
195
|
* reconnect dial loop with an expired JWT, its terminal auth error becomes an unhandled rejection.
|
|
@@ -659,11 +668,12 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
659
668
|
return;
|
|
660
669
|
this.superviseConnection();
|
|
661
670
|
}
|
|
662
|
-
/**
|
|
663
|
-
*
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
static BEARER_RETRY_MS =
|
|
671
|
+
/** The floor on a bearer renewal's delay. A token whose whole lifetime sits under it is renewed at
|
|
672
|
+
* this cadence (`expiry-renewal` mints 5s bearers). */
|
|
673
|
+
static BEARER_MIN_RENEWAL_MS = 5_000;
|
|
674
|
+
/** A failed bearer renewal retries after about this long, doubling per failure up to the cap. */
|
|
675
|
+
static BEARER_RETRY_MS = 5_000;
|
|
676
|
+
static BEARER_RETRY_CAP_MS = 60_000;
|
|
667
677
|
/**
|
|
668
678
|
* A condition the endpoint is already surviving. Node rethrows `error` when no listener is
|
|
669
679
|
* attached, so emitting retry notices on `error` killed hosts that the endpoint intended to
|
|
@@ -672,10 +682,11 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
672
682
|
emitRecoverable(err) {
|
|
673
683
|
this.emit("warning", err);
|
|
674
684
|
}
|
|
675
|
-
/** Fetch a fresh bearer from the source, pin its principal to ours, arm the next
|
|
685
|
+
/** Fetch a fresh bearer from the source, pin its principal to ours, arm the next renewal. On a
|
|
676
686
|
* fetch/principal failure: THROWS when `initial` (start() must fail loud before first connect);
|
|
677
|
-
* otherwise
|
|
678
|
-
* expiry, so a dead auth service is
|
|
687
|
+
* otherwise retries on a jittered, capped backoff, and warns once for the whole run of failures —
|
|
688
|
+
* the live connection keeps working until its current JWT expiry, so a dead auth service is
|
|
689
|
+
* loud without instantly dropping the mesh, and without a line per attempt. */
|
|
679
690
|
async refreshBearer(initial = false) {
|
|
680
691
|
try {
|
|
681
692
|
const bearer = await this.bearerSource();
|
|
@@ -698,11 +709,10 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
698
709
|
// {@link presentableCreds} (#1572).
|
|
699
710
|
if (expiryMs <= Date.now())
|
|
700
711
|
throw new Error("the bearer source returned a token that has already expired - nothing adopted");
|
|
701
|
-
// THE SAME BYTES BACK AGAIN from a source whose token
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
// nothing new (#1561).
|
|
712
|
+
// THE SAME BYTES BACK AGAIN from a source whose token is already past its renewal window:
|
|
713
|
+
// the renewal it arms is floored to 5s, and the next read returns that same token - a 5s
|
|
714
|
+
// loop against the auth service for the rest of its life, with the retry backoff bypassed
|
|
715
|
+
// because the fetch did not FAIL. It succeeded and returned nothing new (#1561).
|
|
706
716
|
//
|
|
707
717
|
// The test is byte identity, not expiry, and not advancement. `exp` carries one second of
|
|
708
718
|
// resolution, so a key rotation re-signing the same claims under a new key - and a healthy
|
|
@@ -710,30 +720,63 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
710
720
|
// `exp` has not moved. That is genuinely re-issued material, which an advancement test
|
|
711
721
|
// refuses and this one adopts. It is the question the creds path asks with
|
|
712
722
|
// `credsFingerprint`: did the source re-issue ANYTHING (#1572).
|
|
713
|
-
if (bearer === held &&
|
|
723
|
+
if (bearer === held && Date.now() >= bearerRenewalDeadlineMs(held, this.bearerAdoptedAtMs))
|
|
714
724
|
throw new Error("the bearer source re-served the token already held and it cannot carry another cycle (the auth service has not issued a fresh one) - nothing adopted");
|
|
715
725
|
}
|
|
726
|
+
if (bearer !== held)
|
|
727
|
+
this.bearerAdoptedAtMs = Date.now();
|
|
716
728
|
this.currentBearer = bearer;
|
|
717
|
-
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
this.
|
|
729
|
+
this.bearerRenewalFailures = 0;
|
|
730
|
+
// The renewal point is drawn from the middle of the token's lifetime, spaced from this
|
|
731
|
+
// process's other renewals in the space: a fixed margin before `exp` kept every endpoint
|
|
732
|
+
// issued in one second renewing, and rebinding through the callout, in one second for life.
|
|
733
|
+
const { issuedAtMs, expiresAtMs } = bearerLifetimeMs(bearer, this.bearerAdoptedAtMs);
|
|
734
|
+
this.armBearerTimer(armRenewal(this.space, issuedAtMs, expiresAtMs, () => void this.renewBearerOnTimer(), CotalEndpoint.BEARER_MIN_RENEWAL_MS));
|
|
723
735
|
}
|
|
724
736
|
catch (e) {
|
|
725
737
|
if (initial)
|
|
726
738
|
throw e;
|
|
727
|
-
|
|
728
|
-
|
|
739
|
+
if (this.bearerRenewalFailures === 0)
|
|
740
|
+
this.emitRecoverable(new Error(`bearer refresh failed (${e instanceof Error ? e.message : String(e)}) - retrying with backoff; this connection dies at its current token's expiry if the auth service stays down`));
|
|
741
|
+
// A wait the source named is kept as named: never retried inside it, nor stretched by the backoff.
|
|
742
|
+
const named = retryAfterMs(e);
|
|
743
|
+
const delay = named > 0 ? named : jitteredBackoffMs(this.bearerRenewalFailures, CotalEndpoint.BEARER_RETRY_MS, CotalEndpoint.BEARER_RETRY_CAP_MS);
|
|
744
|
+
this.bearerRenewalFailures++;
|
|
745
|
+
const timer = setTimeout(() => void this.renewBearerOnTimer(), delay);
|
|
746
|
+
timer.unref?.();
|
|
747
|
+
this.armBearerTimer(() => clearTimeout(timer));
|
|
729
748
|
}
|
|
730
749
|
}
|
|
731
|
-
|
|
750
|
+
armBearerTimer(cancel) {
|
|
751
|
+
this.cancelBearerRenewal?.();
|
|
752
|
+
this.cancelBearerRenewal = cancel;
|
|
732
753
|
if (this.stopped)
|
|
754
|
+
cancel();
|
|
755
|
+
}
|
|
756
|
+
/** The timed renewal. A new token moves the live connection onto itself at once, through the
|
|
757
|
+
* same controlled reconnect the creds path uses, so the broker never has to expire a connection
|
|
758
|
+
* this endpoint could renew. With no live connection, it ends the rebuild loop's wait instead:
|
|
759
|
+
* that loop refuses to dial an expired token and leaves the retries to this timer. */
|
|
760
|
+
async renewBearerOnTimer() {
|
|
761
|
+
const held = this.currentBearer;
|
|
762
|
+
await this.refreshBearer();
|
|
763
|
+
if (this.stopped || this.currentBearer === held)
|
|
733
764
|
return;
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
765
|
+
if (this.nc && !this.nc.isClosed())
|
|
766
|
+
await this.swapConnectionOntoFreshCreds();
|
|
767
|
+
else
|
|
768
|
+
this.kickBackoff();
|
|
769
|
+
}
|
|
770
|
+
/** The cached bearer, checked: every (re)connect attempt reads it, the reconnects nats.js runs
|
|
771
|
+
* on its own included, so none presents an expired token, which the callout could only deny
|
|
772
|
+
* after a full round trip. The bearer counterpart of {@link credsForWire}. */
|
|
773
|
+
bearerForWire() {
|
|
774
|
+
const bearer = this.currentBearer;
|
|
775
|
+
if (bearer !== undefined && bearerExpiryMs(bearer) > Date.now())
|
|
776
|
+
return bearer;
|
|
777
|
+
throw new Error(this.bearerSource
|
|
778
|
+
? "this endpoint's user bearer has expired and renewal through the auth exchange is failing - not presenting the expired token to the broker; retrying with backoff"
|
|
779
|
+
: "this endpoint's user bearer has expired and it holds no bearer source to renew it - re-authenticate and rebuild the endpoint (construct it with a bearer FUNCTION for standing renewal)");
|
|
737
780
|
}
|
|
738
781
|
/** How soon a FAILED creds refresh retries. Successful refreshes schedule by lifetime fraction
|
|
739
782
|
* (75% of iat→exp), not a fixed margin — standing creds span hours to days, bearers minutes. */
|
|
@@ -780,6 +823,12 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
780
823
|
: "this endpoint was constructed with an empty creds string - not dialing without auth material (an empty credential is not anonymous access)");
|
|
781
824
|
return CotalEndpoint.presentableCreds(this.currentCreds, { renewable: Boolean(this.credsSource) });
|
|
782
825
|
}
|
|
826
|
+
/** The credential this endpoint presents on its next connect, checked as {@link credsForWire}
|
|
827
|
+
* checks it. A second connection under the same identity reads it on every attempt, so it
|
|
828
|
+
* presents each credential this endpoint adopts and never an expired one. */
|
|
829
|
+
wireCreds() {
|
|
830
|
+
return this.credsForWire();
|
|
831
|
+
}
|
|
783
832
|
/** Refuses a fetched generation there is nothing to renew FROM, so the caller's failure posture
|
|
784
833
|
* ({@link CREDS_RETRY_MS}) applies instead of the renewal schedule (issue #1523).
|
|
785
834
|
*
|
|
@@ -850,11 +899,11 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
850
899
|
const { iat, exp } = credsClaims(creds);
|
|
851
900
|
return { iat, exp };
|
|
852
901
|
}
|
|
853
|
-
/** Swap the live connection onto the freshest cached cred with a controlled
|
|
854
|
-
* (nats.js re-evaluates the
|
|
855
|
-
* broker to close the connection at `exp`, means the wire never carries a
|
|
856
|
-
* operator never sees a spurious "authentication expired". After the
|
|
857
|
-
* the expiry fence for the credential now authenticated on the wire. */
|
|
902
|
+
/** Swap the live connection onto the freshest cached cred or bearer with a controlled
|
|
903
|
+
* `nc.reconnect()` (nats.js re-evaluates the authenticator per attempt). Swapping now, instead of
|
|
904
|
+
* waiting for the broker to close the connection at `exp`, means the wire never carries a
|
|
905
|
+
* near-dead JWT and the operator never sees a spurious "authentication expired". After the
|
|
906
|
+
* reconnect succeeds, re-arm the expiry fence for the credential now authenticated on the wire. */
|
|
858
907
|
async swapConnectionOntoFreshCreds() {
|
|
859
908
|
const nc = this.nc;
|
|
860
909
|
if (nc && !this.stopped) {
|
|
@@ -1088,14 +1137,15 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
1088
1137
|
}
|
|
1089
1138
|
async bindConnection() {
|
|
1090
1139
|
this.clearConnectionScoped();
|
|
1091
|
-
// Bearer-source endpoints fetch before the FIRST connect, and
|
|
1092
|
-
// cached token is
|
|
1093
|
-
//
|
|
1140
|
+
// Bearer-source endpoints fetch before the FIRST connect, and renew first on a rebuild whose
|
|
1141
|
+
// cached token is past its renewal window (a rebuild after a long outage would otherwise
|
|
1142
|
+
// present a dead bearer). While renewals are failing, their retry timer owns the source: a
|
|
1143
|
+
// fetch here too would add one call to the exchange per rebuild attempt.
|
|
1094
1144
|
if (this.bearerSource) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
if (
|
|
1098
|
-
await this.refreshBearer(
|
|
1145
|
+
if (!this.currentBearer)
|
|
1146
|
+
await this.refreshBearer(true);
|
|
1147
|
+
else if (this.bearerRenewalFailures === 0 && Date.now() >= bearerRenewalDeadlineMs(this.currentBearer, this.bearerAdoptedAtMs))
|
|
1148
|
+
await this.refreshBearer();
|
|
1099
1149
|
}
|
|
1100
1150
|
// Creds-source endpoints likewise fetch before the FIRST connect, and re-fetch on a rebuild
|
|
1101
1151
|
// whose cached cred is expired or inside its renewal window.
|
|
@@ -1109,13 +1159,22 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
1109
1159
|
// instantly drop a still-live mesh), which used to leave the connect below presenting an
|
|
1110
1160
|
// EXPIRED bearer: a guaranteed denial that still costs a full auth-callout round trip, repeated
|
|
1111
1161
|
// by the reestablish loop at a flat rate for as long as the process lives. Refuse here instead.
|
|
1112
|
-
// The loop's capped backoff paces the retries and re-enters this method,
|
|
1113
|
-
// starts working
|
|
1114
|
-
//
|
|
1115
|
-
if (this.userMode
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1162
|
+
// The loop's capped backoff paces the retries and re-enters this method, and a renewal that
|
|
1163
|
+
// starts working ends its wait; an endpoint with no bearer source cannot renew at all, and says
|
|
1164
|
+
// so. The refusal is surfaced once per outage: the loop retries a repeat without a notice.
|
|
1165
|
+
if (this.userMode) {
|
|
1166
|
+
try {
|
|
1167
|
+
this.bearerForWire();
|
|
1168
|
+
this.bearerRefusalSurfaced = false;
|
|
1169
|
+
}
|
|
1170
|
+
catch (e) {
|
|
1171
|
+
if (!this.bearerRefusalSurfaced) {
|
|
1172
|
+
this.bearerRefusalSurfaced = true;
|
|
1173
|
+
throw e;
|
|
1174
|
+
}
|
|
1175
|
+
throw new RepeatedRefusal(e.message);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1119
1178
|
// The CREDS refusal is NOT repeated here. It lives on the supply itself ({@link credsForWire}),
|
|
1120
1179
|
// which the authenticator below re-reads per attempt, so it covers this dial AND the reconnects
|
|
1121
1180
|
// nats.js runs on its own. Raising it early here too would only duplicate it on the one path
|
|
@@ -1139,8 +1198,9 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
1139
1198
|
...(this.transportPingIntervalMs === undefined ? {} : { pingInterval: this.transportPingIntervalMs }),
|
|
1140
1199
|
...(this.transportMaxPingOut === undefined ? {} : { maxPingOut: this.transportMaxPingOut }),
|
|
1141
1200
|
...(this.transportConnectTimeoutMs === undefined ? {} : { timeout: this.transportConnectTimeoutMs }),
|
|
1142
|
-
// The bearer rides
|
|
1143
|
-
// attempt, so internal reconnects present whatever refreshBearer last fetched
|
|
1201
|
+
// The bearer rides the CHECKED getter: nats.js re-evaluates the token authenticator per
|
|
1202
|
+
// (re)connect attempt, so internal reconnects present whatever refreshBearer last fetched,
|
|
1203
|
+
// and refuse a token that has expired since, as the creds getter does.
|
|
1144
1204
|
// Creds ALWAYS ride the CHECKED getter, renewed or static, so every attempt — including the
|
|
1145
1205
|
// reconnects nats.js performs on its own (the one the broker forces at JWT `exp`, and a dial
|
|
1146
1206
|
// loop from an earlier drop that crosses `exp` mid-retry) — re-reads a credential that has
|
|
@@ -1150,7 +1210,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
1150
1210
|
// and dialed ANONYMOUSLY, so the broker answered `Authorization Violation` and the real fault
|
|
1151
1211
|
// (an empty credential) was never named. Routing it into the checked getter fails it loud
|
|
1152
1212
|
// instead. Anonymous access stays reachable the only way it should be: by passing no creds.
|
|
1153
|
-
...authOpts({ token: this.token, user: this.user, pass: this.pass, creds: this.currentCreds !== undefined || this.credsSource ? () => this.credsForWire() : undefined, bearer: this.userMode ? () => this.
|
|
1213
|
+
...authOpts({ token: this.token, user: this.user, pass: this.pass, creds: this.currentCreds !== undefined || this.credsSource ? () => this.credsForWire() : undefined, bearer: this.userMode ? () => this.bearerForWire() : undefined, sentinelCreds: this.sentinelCreds, tls: this.tls }),
|
|
1154
1214
|
});
|
|
1155
1215
|
// SPEC §13.12: the control surface requires nats-server >= 2.12; this runs on every
|
|
1156
1216
|
// fresh connection, including the reconnects the library performs on its own here.
|
|
@@ -1609,12 +1669,13 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
1609
1669
|
* auth callout. */
|
|
1610
1670
|
static RETRY_BACKOFF_CAP_MS = 60_000;
|
|
1611
1671
|
/** The wait before the next rebuild attempt: {@link retryMs}, doubling per consecutive failure up
|
|
1612
|
-
* to {@link RETRY_BACKOFF_CAP_MS}
|
|
1613
|
-
*
|
|
1672
|
+
* to {@link RETRY_BACKOFF_CAP_MS}, then a random half of it taken off. A transient drop still
|
|
1673
|
+
* recovers within retryMs, and endpoints refused together, as a broker restart or an auth limit
|
|
1674
|
+
* refuses a host's agents, spread their retries instead of arriving at the callout at once. */
|
|
1614
1675
|
nextRetryDelayMs() {
|
|
1615
1676
|
const delay = Math.min(this.retryMs * 2 ** this.retryAttempt, CotalEndpoint.RETRY_BACKOFF_CAP_MS);
|
|
1616
1677
|
this.retryAttempt++;
|
|
1617
|
-
return delay;
|
|
1678
|
+
return Math.round(delay / 2 + Math.random() * (delay / 2));
|
|
1618
1679
|
}
|
|
1619
1680
|
/** Rebuild with backoff until it sticks or we're stopped. Interruptible: a manual
|
|
1620
1681
|
* {@link reconnect} kicks the backoff so the next attempt runs immediately instead of
|
|
@@ -1632,13 +1693,17 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
1632
1693
|
return; // success — re-armed; the supervisor re-triggers on the next terminal close
|
|
1633
1694
|
}
|
|
1634
1695
|
catch (e) {
|
|
1635
|
-
|
|
1636
|
-
|
|
1696
|
+
const delay = this.nextRetryDelayMs();
|
|
1697
|
+
// The broker never says why the auth callout refused a bearer, so a user-mode refusal is
|
|
1698
|
+
// named as one, with where the reason is, and paced like any other failure.
|
|
1699
|
+
if (!this.stopped && !(e instanceof RepeatedRefusal))
|
|
1700
|
+
this.emitRecoverable(this.userMode && isAuthRejection(e)
|
|
1701
|
+
? new Error(`the broker refused this endpoint's bearer for ${this.owner}.${this.actor} - retrying in ${Math.ceil(delay / 1000)}s; the space's auth service logs why`, { cause: e })
|
|
1702
|
+
: e);
|
|
1637
1703
|
// A warning listener may stop the endpoint. A backoff armed after that holds the process
|
|
1638
1704
|
// open for the whole delay, because the stop has already kicked the backoff it saw.
|
|
1639
1705
|
if (this.stopped)
|
|
1640
1706
|
return;
|
|
1641
|
-
const delay = this.nextRetryDelayMs();
|
|
1642
1707
|
await new Promise((resolve) => {
|
|
1643
1708
|
this.backoffResolve = resolve;
|
|
1644
1709
|
this.backoffTimer = setTimeout(resolve, delay);
|
|
@@ -1700,8 +1765,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
1700
1765
|
clearInterval(this.heartbeatTimer);
|
|
1701
1766
|
if (this.sweepTimer)
|
|
1702
1767
|
clearInterval(this.sweepTimer);
|
|
1703
|
-
|
|
1704
|
-
clearTimeout(this.bearerTimer);
|
|
1768
|
+
this.cancelBearerRenewal?.();
|
|
1705
1769
|
if (this.credsTimer)
|
|
1706
1770
|
clearTimeout(this.credsTimer);
|
|
1707
1771
|
if (this.authExpiryReconnectTimer)
|
|
@@ -2138,6 +2202,12 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
2138
2202
|
const m = await this.nc.request(reqSubject, JSON.stringify(body), { timeout: timeoutMs, noMux: true, reply });
|
|
2139
2203
|
return m.json();
|
|
2140
2204
|
}
|
|
2205
|
+
/** The caller this connection's endpoint requests ride as: the legacy triple, or the issued
|
|
2206
|
+
* caller with its discovered generation (SPEC 13.15). It names the reply rail this connection's
|
|
2207
|
+
* credential reads, which is where a manager's turn notice for it lands (SPEC 14.3). */
|
|
2208
|
+
serviceRailCaller() {
|
|
2209
|
+
return this.serviceCaller();
|
|
2210
|
+
}
|
|
2141
2211
|
/** This endpoint's v0.4 caller triple (§13.2) — the identity its minted ep-rail rows pin. The
|
|
2142
2212
|
* owner/actor principal is mode-correct by construction (static: DEV_OWNER + the connection
|
|
2143
2213
|
* identity; user mode: the bearer's callout-derived pair), and the lifecycle UID is the
|
|
@@ -7469,6 +7539,11 @@ function authOpts(a) {
|
|
|
7469
7539
|
/** The bearer's `exp` as epoch ms — what the refresh schedule keys on. A bearer without a numeric
|
|
7470
7540
|
* `exp` is structurally unusable for a refreshing endpoint (fail-loud, like the principal decode). */
|
|
7471
7541
|
function bearerExpiryMs(bearer) {
|
|
7542
|
+
return bearerLifetimeMs(bearer, 0).expiresAtMs;
|
|
7543
|
+
}
|
|
7544
|
+
/** The bearer's lifetime as epoch ms: `iat` to `exp`, starting at `adoptedAtMs` when it carries no
|
|
7545
|
+
* `iat`. */
|
|
7546
|
+
function bearerLifetimeMs(bearer, adoptedAtMs) {
|
|
7472
7547
|
const payload = bearer.split(".")[1];
|
|
7473
7548
|
if (!payload)
|
|
7474
7549
|
throw new Error("user-mode bearer is not a JWT (no payload segment)");
|
|
@@ -7481,7 +7556,17 @@ function bearerExpiryMs(bearer) {
|
|
|
7481
7556
|
}
|
|
7482
7557
|
if (typeof claims.exp !== "number")
|
|
7483
7558
|
throw new Error("user-mode bearer is missing a numeric exp claim");
|
|
7484
|
-
|
|
7559
|
+
const expiresAtMs = claims.exp * 1000;
|
|
7560
|
+
return { issuedAtMs: Math.min(typeof claims.iat === "number" ? claims.iat * 1000 : adoptedAtMs, expiresAtMs), expiresAtMs };
|
|
7561
|
+
}
|
|
7562
|
+
/** Past this point a held bearer is overdue: its renewal should already have replaced it. */
|
|
7563
|
+
function bearerRenewalDeadlineMs(bearer, adoptedAtMs) {
|
|
7564
|
+
const { issuedAtMs, expiresAtMs } = bearerLifetimeMs(bearer, adoptedAtMs);
|
|
7565
|
+
return renewalDeadlineMs(issuedAtMs, expiresAtMs);
|
|
7566
|
+
}
|
|
7567
|
+
/** A refusal already surfaced once for this outage. The rebuild loop retries it without a notice,
|
|
7568
|
+
* so an endpoint that cannot renew shows one status line rather than one per attempt. */
|
|
7569
|
+
class RepeatedRefusal extends Error {
|
|
7485
7570
|
}
|
|
7486
7571
|
/** Decode the owner+actor PRINCIPAL from a user bearer WITHOUT verifying it — the client trusts its own
|
|
7487
7572
|
* bearer only to build its subjects; the broker's minted grant (from the callout, which DOES verify the
|
|
@@ -7741,6 +7826,12 @@ function tcpDialable(server, timeoutMs) {
|
|
|
7741
7826
|
export function isAuthRejection(e) {
|
|
7742
7827
|
return e instanceof AuthorizationError || e instanceof UserAuthenticationExpiredError;
|
|
7743
7828
|
}
|
|
7829
|
+
/** The wait a credential source asked for with a numeric `retryAfterSeconds` on its error, in
|
|
7830
|
+
* milliseconds, or 0. */
|
|
7831
|
+
function retryAfterMs(e) {
|
|
7832
|
+
const seconds = e?.retryAfterSeconds;
|
|
7833
|
+
return typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : 0;
|
|
7834
|
+
}
|
|
7744
7835
|
/** Whether a NATS server is *running* at `servers`. With NO creds this is a SILENT plaintext
|
|
7745
7836
|
* liveness check ({@link tcpInfoProbe}): it reads the server's pre-auth `INFO` greeting and closes
|
|
7746
7837
|
* WITHOUT authenticating, so a live broker (open OR auth — INFO precedes auth) returns true while
|