@replit/river 0.217.2 → 0.218.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.
@@ -5,9 +5,12 @@ import {
5
5
  import {
6
6
  ControlMessageHandshakeRequestSchema,
7
7
  ControlMessageHandshakeResponseSchema,
8
+ ControlMessageRehandshakeRequestSchema,
9
+ ControlMessageRehandshakeResponseSchema,
8
10
  HandshakeErrorCustomHandlerFatalResponseCodes,
9
11
  HandshakeErrorRetriableResponseCodes,
10
12
  OpaqueTransportMessageSchema,
13
+ RehandshakeStreamId,
11
14
  acceptedProtocolVersions,
12
15
  coerceErrorString,
13
16
  createConnectionTelemetryInfo,
@@ -20,8 +23,10 @@ import {
20
23
  handshakeResponseMessage,
21
24
  isAcceptedProtocolVersion,
22
25
  isAck,
26
+ rehandshakeRequestMessage,
27
+ rehandshakeResponseMessage,
23
28
  validationErrorToRiverErrors
24
- } from "./chunk-FWVKRK36.js";
29
+ } from "./chunk-2HK3WK7W.js";
25
30
 
26
31
  // transport/events.ts
27
32
  var ProtocolError = {
@@ -559,6 +564,8 @@ var SessionConnected = class extends IdentifiedSession {
559
564
  heartbeatHandle;
560
565
  heartbeatMissTimeout;
561
566
  isActivelyHeartbeating = false;
567
+ rehandshakeTimer;
568
+ credentialExpiry;
562
569
  updateBookkeeping(ack, seq) {
563
570
  this.sendBuffer = this.sendBuffer.filter((unacked) => unacked.seq >= ack);
564
571
  this.ack = seq + 1;
@@ -664,6 +671,61 @@ var SessionConnected = class extends IdentifiedSession {
664
671
  };
665
672
  this.send(heartbeat);
666
673
  }
674
+ /**
675
+ * Schedules the next proactive re-handshake from the credential's expiry. The
676
+ * server calls this after each (re)validation, mirroring {@link startActiveHeartbeat}:
677
+ * once armed the session drives the exchange itself — one handshake window before
678
+ * expiry it sends a re-handshake request and waits for the response, firing
679
+ * {@link SessionConnectedListeners.onRehandshakeTimeout} if none arrives in time.
680
+ * Passing `undefined` (a credential that never expires) cancels any schedule.
681
+ */
682
+ scheduleRehandshake(expiry) {
683
+ this.clearRehandshakeTimer();
684
+ this.credentialExpiry = expiry;
685
+ if (expiry === void 0) {
686
+ return;
687
+ }
688
+ const delayMs = expiry - this.options.handshakeTimeoutMs - Date.now();
689
+ this.rehandshakeTimer = setTimeout(
690
+ () => {
691
+ this.rehandshakeTimer = void 0;
692
+ this.sendRehandshakeRequest();
693
+ },
694
+ Math.max(0, delayMs)
695
+ );
696
+ }
697
+ /**
698
+ * Sends a re-handshake request immediately and arms the response deadline,
699
+ * bypassing the expiry schedule. Returns false if the request couldn't be sent.
700
+ */
701
+ requestRehandshakeNow() {
702
+ this.clearRehandshakeTimer();
703
+ return this.sendRehandshakeRequest();
704
+ }
705
+ sendRehandshakeRequest() {
706
+ const res = this.send(rehandshakeRequestMessage());
707
+ if (!res.ok) {
708
+ return false;
709
+ }
710
+ const deadlineMs = this.credentialExpiry !== void 0 ? Math.min(
711
+ this.options.handshakeTimeoutMs,
712
+ this.credentialExpiry - Date.now()
713
+ ) : this.options.handshakeTimeoutMs;
714
+ this.rehandshakeTimer = setTimeout(
715
+ () => {
716
+ this.rehandshakeTimer = void 0;
717
+ this.listeners.onRehandshakeTimeout?.();
718
+ },
719
+ Math.max(0, deadlineMs)
720
+ );
721
+ return true;
722
+ }
723
+ clearRehandshakeTimer() {
724
+ if (this.rehandshakeTimer) {
725
+ clearTimeout(this.rehandshakeTimer);
726
+ this.rehandshakeTimer = void 0;
727
+ }
728
+ }
667
729
  onMessageData = (msg) => {
668
730
  const parsedMsgRes = this.codec.fromBuffer(msg);
669
731
  if (!parsedMsgRes.ok) {
@@ -709,6 +771,10 @@ var SessionConnected = class extends IdentifiedSession {
709
771
  });
710
772
  this.updateBookkeeping(parsedMsg.ack, parsedMsg.seq);
711
773
  if (!isAck(parsedMsg.controlFlags)) {
774
+ if (parsedMsg.streamId === RehandshakeStreamId) {
775
+ this.listeners.onRehandshake(parsedMsg);
776
+ return;
777
+ }
712
778
  this.listeners.onMessage(parsedMsg);
713
779
  return;
714
780
  }
@@ -733,6 +799,7 @@ var SessionConnected = class extends IdentifiedSession {
733
799
  clearTimeout(this.heartbeatMissTimeout);
734
800
  this.heartbeatMissTimeout = void 0;
735
801
  }
802
+ this.clearRehandshakeTimer();
736
803
  }
737
804
  _handleClose() {
738
805
  super._handleClose();
@@ -1504,6 +1571,14 @@ var ClientTransport = class extends Transport {
1504
1571
  * Optional handshake options for this client.
1505
1572
  */
1506
1573
  handshakeExtensions;
1574
+ /**
1575
+ * Handshake-metadata constructions prefetched when a connection attempt begins
1576
+ * (only when {@link ClientHandshakeOptions.eager} is set), so the
1577
+ * token fetch overlaps the dial instead of following it. Keyed by the peer being
1578
+ * connected to; consumed when the handshake is sent and cleared when an attempt
1579
+ * is abandoned.
1580
+ */
1581
+ pendingHandshakeMetadata = /* @__PURE__ */ new Map();
1507
1582
  sessions;
1508
1583
  constructor(clientId, providedOptions) {
1509
1584
  super(clientId, providedOptions);
@@ -1517,6 +1592,62 @@ var ClientTransport = class extends Transport {
1517
1592
  extendHandshake(options) {
1518
1593
  this.handshakeExtensions = options;
1519
1594
  }
1595
+ handleRehandshakeMessage(message) {
1596
+ if (!Value2.Check(ControlMessageRehandshakeRequestSchema, message.payload)) {
1597
+ this.log?.warn(
1598
+ `ignoring malformed re-handshake request from ${message.from}`,
1599
+ { clientId: this.clientId, connectedTo: message.from }
1600
+ );
1601
+ return;
1602
+ }
1603
+ void this.sendRehandshake(message.from);
1604
+ }
1605
+ /**
1606
+ * Re-constructs handshake metadata via the configured handshake extension and
1607
+ * sends it back to the server so it can replace the metadata for this session.
1608
+ * Triggered by a server {@link ControlMessageRehandshakeRequestSchema}.
1609
+ */
1610
+ async sendRehandshake(to) {
1611
+ if (!this.handshakeExtensions) {
1612
+ this.log?.warn(
1613
+ `got re-handshake request from ${to} but no handshake extensions are configured, ignoring`,
1614
+ { clientId: this.clientId, connectedTo: to }
1615
+ );
1616
+ return;
1617
+ }
1618
+ const session = this.sessions.get(to);
1619
+ if (!session || session.state !== "Connected" /* Connected */) {
1620
+ return;
1621
+ }
1622
+ const loggingMetadata = session.loggingMetadata;
1623
+ const sessionId = session.id;
1624
+ let metadata;
1625
+ try {
1626
+ metadata = await this.handshakeExtensions.construct();
1627
+ } catch (err) {
1628
+ this.log?.error(
1629
+ `failed to construct re-handshake metadata for ${to}: ${coerceErrorString(
1630
+ err
1631
+ )}`,
1632
+ loggingMetadata
1633
+ );
1634
+ return;
1635
+ }
1636
+ try {
1637
+ const send = this.getSessionBoundSendFn(to, sessionId);
1638
+ send(rehandshakeResponseMessage(metadata));
1639
+ } catch (err) {
1640
+ const reason = coerceErrorString(err);
1641
+ this.log?.error(
1642
+ `failed to send re-handshake metadata to ${to}: ${reason}`,
1643
+ loggingMetadata
1644
+ );
1645
+ this.protocolError({
1646
+ type: ProtocolError.MessageSendFailure,
1647
+ message: reason
1648
+ });
1649
+ }
1650
+ }
1520
1651
  tryReconnecting(to) {
1521
1652
  const oldSession = this.sessions.get(to);
1522
1653
  if (!this.options.enableTransparentSessionReconnects && oldSession) {
@@ -1558,6 +1689,10 @@ var ClientTransport = class extends Transport {
1558
1689
  this.createSession(session);
1559
1690
  return session;
1560
1691
  }
1692
+ deleteSession(session, options) {
1693
+ this.pendingHandshakeMetadata.delete(session.to);
1694
+ super.deleteSession(session, options);
1695
+ }
1561
1696
  // listeners
1562
1697
  onConnectingFailed(session) {
1563
1698
  const noConnectionSession = super.onConnectingFailed(session);
@@ -1711,6 +1846,9 @@ var ClientTransport = class extends Transport {
1711
1846
  onMessage: (msg2) => {
1712
1847
  this.handleMsg(msg2);
1713
1848
  },
1849
+ onRehandshake: (msg2) => {
1850
+ this.handleRehandshakeMessage(msg2);
1851
+ },
1714
1852
  onInvalidMessage: (reason) => {
1715
1853
  this.log?.error(`invalid message: ${reason}`, {
1716
1854
  ...connectedSession.loggingMetadata,
@@ -1828,6 +1966,12 @@ var ClientTransport = class extends Transport {
1828
1966
  }
1829
1967
  }
1830
1968
  );
1969
+ if (this.handshakeExtensions?.eager) {
1970
+ this.pendingHandshakeMetadata.set(
1971
+ session.to,
1972
+ this.constructHandshakeMetadata()
1973
+ );
1974
+ }
1831
1975
  const connectingSession = ClientSessionStateGraph.transition.BackingOffToConnecting(
1832
1976
  session,
1833
1977
  connPromise,
@@ -1875,24 +2019,39 @@ var ClientTransport = class extends Transport {
1875
2019
  );
1876
2020
  this.updateSession(connectingSession);
1877
2021
  }
2022
+ /**
2023
+ * Constructs handshake metadata via the configured handshake extension, capturing
2024
+ * a failure as a value so a prefetched result can be awaited without rejecting.
2025
+ */
2026
+ async constructHandshakeMetadata() {
2027
+ if (!this.handshakeExtensions) {
2028
+ return { ok: true, metadata: void 0 };
2029
+ }
2030
+ try {
2031
+ return { ok: true, metadata: await this.handshakeExtensions.construct() };
2032
+ } catch (err) {
2033
+ return { ok: false, reason: coerceErrorString(err) };
2034
+ }
2035
+ }
1878
2036
  async sendHandshake(session) {
1879
2037
  let metadata = void 0;
1880
2038
  if (this.handshakeExtensions) {
1881
- try {
1882
- metadata = await this.handshakeExtensions.construct();
1883
- } catch (err) {
1884
- const errStr = coerceErrorString(err);
2039
+ const pending = this.pendingHandshakeMetadata.get(session.to);
2040
+ this.pendingHandshakeMetadata.delete(session.to);
2041
+ const result = await (pending ?? this.constructHandshakeMetadata());
2042
+ if (!result.ok) {
1885
2043
  this.log?.error(
1886
- `failed to construct handshake metadata for session to ${session.to}: ${errStr}`,
2044
+ `failed to construct handshake metadata for session to ${session.to}: ${result.reason}`,
1887
2045
  session.loggingMetadata
1888
2046
  );
1889
2047
  this.protocolError({
1890
2048
  type: ProtocolError.HandshakeFailed,
1891
- message: `failed to construct handshake metadata: ${errStr}`
2049
+ message: `failed to construct handshake metadata: ${result.reason}`
1892
2050
  });
1893
2051
  this.deleteSession(session, { unhealthy: true });
1894
2052
  return;
1895
2053
  }
2054
+ metadata = result.metadata;
1896
2055
  }
1897
2056
  if (session._isConsumed) {
1898
2057
  return;
@@ -1972,6 +2131,134 @@ var ServerTransport = class extends Transport {
1972
2131
  this.sessionHandshakeMetadata.delete(session.to);
1973
2132
  super.deleteSession(session, options);
1974
2133
  }
2134
+ /**
2135
+ * Asks the connected client to re-handshake — re-construct and resend its
2136
+ * handshake metadata (e.g. a refreshed token). Returns false if there is no
2137
+ * live connection to send the request over.
2138
+ *
2139
+ * On a successful re-handshake the stored metadata is replaced and observed by
2140
+ * subsequent procedure calls. If the client does not respond with metadata that
2141
+ * re-validates before the response deadline — the shorter of
2142
+ * {@link SessionOptions.handshakeTimeoutMs} and the credential's remaining
2143
+ * lifetime — the session is torn down.
2144
+ */
2145
+ requestRehandshake(to) {
2146
+ const session = this.sessions.get(to);
2147
+ if (!session || session.state !== "Connected" /* Connected */) {
2148
+ return false;
2149
+ }
2150
+ return session.requestRehandshakeNow();
2151
+ }
2152
+ /**
2153
+ * Stores freshly validated handshake metadata for a client and hands the
2154
+ * credential's expiry to the session, which schedules and runs the next
2155
+ * re-handshake itself. Called on every successful (re)handshake, so the schedule
2156
+ * perpetuates itself and survives transparent reconnects.
2157
+ */
2158
+ storeSessionMetadata(session, parsed) {
2159
+ this.sessionHandshakeMetadata.set(session.to, parsed);
2160
+ if (session.state === "Connected" /* Connected */) {
2161
+ session.scheduleRehandshake(
2162
+ this.handshakeExtensions?.expiry?.(parsed)?.getTime()
2163
+ );
2164
+ }
2165
+ }
2166
+ handleRehandshakeMessage(message) {
2167
+ if (!Value3.Check(ControlMessageRehandshakeResponseSchema, message.payload)) {
2168
+ const session = this.sessions.get(message.from);
2169
+ if (session) {
2170
+ this.teardownForFailedRehandshake(
2171
+ session,
2172
+ "received malformed re-handshake control message"
2173
+ );
2174
+ }
2175
+ return;
2176
+ }
2177
+ void this.onRehandshakeResponse(message.from, message.payload.metadata);
2178
+ }
2179
+ /**
2180
+ * Re-validates handshake metadata sent by the client during a re-handshake and
2181
+ * replaces the stored metadata on success. Any failure (malformed metadata,
2182
+ * rejection, or a thrown validator) tears the session down.
2183
+ */
2184
+ async onRehandshakeResponse(from, metadata) {
2185
+ const handshakeExtensions = this.handshakeExtensions;
2186
+ if (!handshakeExtensions) {
2187
+ return;
2188
+ }
2189
+ const session = this.sessions.get(from);
2190
+ if (!session) {
2191
+ return;
2192
+ }
2193
+ if (!Value3.Check(handshakeExtensions.schema, metadata)) {
2194
+ this.teardownForFailedRehandshake(
2195
+ session,
2196
+ "received malformed handshake metadata during re-handshake"
2197
+ );
2198
+ return;
2199
+ }
2200
+ const previousParsedMetadata = this.sessionHandshakeMetadata.get(from);
2201
+ let parsedMetadataOrFailureCode;
2202
+ try {
2203
+ parsedMetadataOrFailureCode = await handshakeExtensions.validate(
2204
+ metadata,
2205
+ previousParsedMetadata,
2206
+ from
2207
+ );
2208
+ } catch (err) {
2209
+ this.teardownForFailedRehandshake(
2210
+ session,
2211
+ `handshake validation threw during re-handshake: ${coerceErrorString(
2212
+ err
2213
+ )}`
2214
+ );
2215
+ return;
2216
+ }
2217
+ if (Value3.Check(
2218
+ HandshakeErrorCustomHandlerFatalResponseCodes,
2219
+ parsedMetadataOrFailureCode
2220
+ )) {
2221
+ this.teardownForFailedRehandshake(
2222
+ session,
2223
+ "re-handshake metadata rejected by handshake handler"
2224
+ );
2225
+ return;
2226
+ }
2227
+ if (this.sessions.get(from) !== session) {
2228
+ return;
2229
+ }
2230
+ this.storeSessionMetadata(
2231
+ session,
2232
+ parsedMetadataOrFailureCode
2233
+ );
2234
+ this.log?.info(`re-handshake from ${from} ok`, {
2235
+ ...session.loggingMetadata,
2236
+ connectedTo: from
2237
+ });
2238
+ }
2239
+ /**
2240
+ * Tears down a session whose re-handshake failed (rejected, malformed, timed
2241
+ * out, or a thrown validator). No-ops if {@link session} is no longer the live
2242
+ * session for its peer — a transparent reconnect keeps the same id, so callers
2243
+ * reaching here after an async gap can't accidentally close the session that
2244
+ * replaced it.
2245
+ */
2246
+ teardownForFailedRehandshake(session, reason) {
2247
+ if (this.sessions.get(session.to) !== session) {
2248
+ return;
2249
+ }
2250
+ const to = session.to;
2251
+ this.log?.warn(`tearing down session to ${to}: ${reason}`, {
2252
+ ...session.loggingMetadata,
2253
+ connectedTo: to
2254
+ });
2255
+ this.protocolError({
2256
+ type: ProtocolError.HandshakeFailed,
2257
+ code: "REJECTED_BY_CUSTOM_HANDLER",
2258
+ message: reason
2259
+ });
2260
+ this.deleteSession(session, { unhealthy: true });
2261
+ }
1975
2262
  handleConnection(conn) {
1976
2263
  if (this.getStatus() !== "open") return;
1977
2264
  this.log?.info(`new incoming connection`, {
@@ -2317,6 +2604,15 @@ var ServerTransport = class extends Transport {
2317
2604
  onMessage: (msg2) => {
2318
2605
  this.handleMsg(msg2);
2319
2606
  },
2607
+ onRehandshake: (msg2) => {
2608
+ this.handleRehandshakeMessage(msg2);
2609
+ },
2610
+ onRehandshakeTimeout: () => {
2611
+ this.teardownForFailedRehandshake(
2612
+ connectedSession,
2613
+ "re-handshake timed out"
2614
+ );
2615
+ },
2320
2616
  onInvalidMessage: (reason) => {
2321
2617
  this.log?.error(`invalid message: ${reason}`, {
2322
2618
  ...connectedSession.loggingMetadata,
@@ -2346,7 +2642,7 @@ var ServerTransport = class extends Transport {
2346
2642
  if (!bufferSendRes.ok) {
2347
2643
  return;
2348
2644
  }
2349
- this.sessionHandshakeMetadata.set(connectedSession.to, parsedMetadata);
2645
+ this.storeSessionMetadata(connectedSession, parsedMetadata);
2350
2646
  if (oldSession) {
2351
2647
  this.updateSession(connectedSession);
2352
2648
  } else {
@@ -2497,4 +2793,4 @@ export {
2497
2793
  WebSocketConnection,
2498
2794
  CodecMessageAdapter
2499
2795
  };
2500
- //# sourceMappingURL=chunk-YTV5S3DJ.js.map
2796
+ //# sourceMappingURL=chunk-PWNG6VBS.js.map