@replit/river 0.217.1 → 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-JFFRB3SS.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) {
@@ -673,6 +735,12 @@ var SessionConnected = class extends IdentifiedSession {
673
735
  return;
674
736
  }
675
737
  const parsedMsg = parsedMsgRes.value;
738
+ if (parsedMsg.from !== this.to) {
739
+ this.listeners.onInvalidMessage(
740
+ `received message with 'from' (${parsedMsg.from}) that does not match the session peer (${this.to})`
741
+ );
742
+ return;
743
+ }
676
744
  if (parsedMsg.seq !== this.ack) {
677
745
  if (parsedMsg.seq < this.ack) {
678
746
  this.log?.debug(
@@ -703,6 +771,10 @@ var SessionConnected = class extends IdentifiedSession {
703
771
  });
704
772
  this.updateBookkeeping(parsedMsg.ack, parsedMsg.seq);
705
773
  if (!isAck(parsedMsg.controlFlags)) {
774
+ if (parsedMsg.streamId === RehandshakeStreamId) {
775
+ this.listeners.onRehandshake(parsedMsg);
776
+ return;
777
+ }
706
778
  this.listeners.onMessage(parsedMsg);
707
779
  return;
708
780
  }
@@ -727,6 +799,7 @@ var SessionConnected = class extends IdentifiedSession {
727
799
  clearTimeout(this.heartbeatMissTimeout);
728
800
  this.heartbeatMissTimeout = void 0;
729
801
  }
802
+ this.clearRehandshakeTimer();
730
803
  }
731
804
  _handleClose() {
732
805
  super._handleClose();
@@ -1498,6 +1571,14 @@ var ClientTransport = class extends Transport {
1498
1571
  * Optional handshake options for this client.
1499
1572
  */
1500
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();
1501
1582
  sessions;
1502
1583
  constructor(clientId, providedOptions) {
1503
1584
  super(clientId, providedOptions);
@@ -1511,6 +1592,62 @@ var ClientTransport = class extends Transport {
1511
1592
  extendHandshake(options) {
1512
1593
  this.handshakeExtensions = options;
1513
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
+ }
1514
1651
  tryReconnecting(to) {
1515
1652
  const oldSession = this.sessions.get(to);
1516
1653
  if (!this.options.enableTransparentSessionReconnects && oldSession) {
@@ -1552,6 +1689,10 @@ var ClientTransport = class extends Transport {
1552
1689
  this.createSession(session);
1553
1690
  return session;
1554
1691
  }
1692
+ deleteSession(session, options) {
1693
+ this.pendingHandshakeMetadata.delete(session.to);
1694
+ super.deleteSession(session, options);
1695
+ }
1555
1696
  // listeners
1556
1697
  onConnectingFailed(session) {
1557
1698
  const noConnectionSession = super.onConnectingFailed(session);
@@ -1705,6 +1846,9 @@ var ClientTransport = class extends Transport {
1705
1846
  onMessage: (msg2) => {
1706
1847
  this.handleMsg(msg2);
1707
1848
  },
1849
+ onRehandshake: (msg2) => {
1850
+ this.handleRehandshakeMessage(msg2);
1851
+ },
1708
1852
  onInvalidMessage: (reason) => {
1709
1853
  this.log?.error(`invalid message: ${reason}`, {
1710
1854
  ...connectedSession.loggingMetadata,
@@ -1822,6 +1966,12 @@ var ClientTransport = class extends Transport {
1822
1966
  }
1823
1967
  }
1824
1968
  );
1969
+ if (this.handshakeExtensions?.eager) {
1970
+ this.pendingHandshakeMetadata.set(
1971
+ session.to,
1972
+ this.constructHandshakeMetadata()
1973
+ );
1974
+ }
1825
1975
  const connectingSession = ClientSessionStateGraph.transition.BackingOffToConnecting(
1826
1976
  session,
1827
1977
  connPromise,
@@ -1869,24 +2019,39 @@ var ClientTransport = class extends Transport {
1869
2019
  );
1870
2020
  this.updateSession(connectingSession);
1871
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
+ }
1872
2036
  async sendHandshake(session) {
1873
2037
  let metadata = void 0;
1874
2038
  if (this.handshakeExtensions) {
1875
- try {
1876
- metadata = await this.handshakeExtensions.construct();
1877
- } catch (err) {
1878
- 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) {
1879
2043
  this.log?.error(
1880
- `failed to construct handshake metadata for session to ${session.to}: ${errStr}`,
2044
+ `failed to construct handshake metadata for session to ${session.to}: ${result.reason}`,
1881
2045
  session.loggingMetadata
1882
2046
  );
1883
2047
  this.protocolError({
1884
2048
  type: ProtocolError.HandshakeFailed,
1885
- message: `failed to construct handshake metadata: ${errStr}`
2049
+ message: `failed to construct handshake metadata: ${result.reason}`
1886
2050
  });
1887
2051
  this.deleteSession(session, { unhealthy: true });
1888
2052
  return;
1889
2053
  }
2054
+ metadata = result.metadata;
1890
2055
  }
1891
2056
  if (session._isConsumed) {
1892
2057
  return;
@@ -1966,6 +2131,134 @@ var ServerTransport = class extends Transport {
1966
2131
  this.sessionHandshakeMetadata.delete(session.to);
1967
2132
  super.deleteSession(session, options);
1968
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
+ }
1969
2262
  handleConnection(conn) {
1970
2263
  if (this.getStatus() !== "open") return;
1971
2264
  this.log?.info(`new incoming connection`, {
@@ -2129,7 +2422,8 @@ var ServerTransport = class extends Transport {
2129
2422
  try {
2130
2423
  parsedMetadataOrFailureCode = await this.handshakeExtensions.validate(
2131
2424
  msg.payload.metadata,
2132
- previousParsedMetadata
2425
+ previousParsedMetadata,
2426
+ msg.from
2133
2427
  );
2134
2428
  } catch (err) {
2135
2429
  this.rejectHandshakeRequest(
@@ -2310,6 +2604,15 @@ var ServerTransport = class extends Transport {
2310
2604
  onMessage: (msg2) => {
2311
2605
  this.handleMsg(msg2);
2312
2606
  },
2607
+ onRehandshake: (msg2) => {
2608
+ this.handleRehandshakeMessage(msg2);
2609
+ },
2610
+ onRehandshakeTimeout: () => {
2611
+ this.teardownForFailedRehandshake(
2612
+ connectedSession,
2613
+ "re-handshake timed out"
2614
+ );
2615
+ },
2313
2616
  onInvalidMessage: (reason) => {
2314
2617
  this.log?.error(`invalid message: ${reason}`, {
2315
2618
  ...connectedSession.loggingMetadata,
@@ -2339,7 +2642,7 @@ var ServerTransport = class extends Transport {
2339
2642
  if (!bufferSendRes.ok) {
2340
2643
  return;
2341
2644
  }
2342
- this.sessionHandshakeMetadata.set(connectedSession.to, parsedMetadata);
2645
+ this.storeSessionMetadata(connectedSession, parsedMetadata);
2343
2646
  if (oldSession) {
2344
2647
  this.updateSession(connectedSession);
2345
2648
  } else {
@@ -2490,4 +2793,4 @@ export {
2490
2793
  WebSocketConnection,
2491
2794
  CodecMessageAdapter
2492
2795
  };
2493
- //# sourceMappingURL=chunk-VK3VJZGG.js.map
2796
+ //# sourceMappingURL=chunk-PWNG6VBS.js.map