@nice-code/action 0.24.0 → 0.26.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 (42) hide show
  1. package/README.md +106 -6
  2. package/build/{AcceptorHandler-11-QMdx2.d.mts → AcceptorHandler-CLbwu2Pa.d.mts} +179 -18
  3. package/build/{AcceptorHandler-CxD0c1BE.d.cts → AcceptorHandler-Du292dpC.d.cts} +179 -18
  4. package/build/{ActionDevtoolsCore-37JP4bOG.d.cts → ActionDevtoolsCore-DGwzONZT.d.mts} +2 -2
  5. package/build/{ActionDevtoolsCore-Cgq-go1R.d.mts → ActionDevtoolsCore-dH4K4w3B.d.cts} +2 -2
  6. package/build/advanced/index.cjs +1 -1
  7. package/build/advanced/index.d.cts +12 -101
  8. package/build/advanced/index.d.mts +12 -101
  9. package/build/advanced/index.mjs +1 -1
  10. package/build/{createHibernatableWsServerAdapter-C07RfUTH.mjs → createHibernatableWsServerAdapter-BD5n-Ev9.mjs} +186 -83
  11. package/build/createHibernatableWsServerAdapter-BD5n-Ev9.mjs.map +1 -0
  12. package/build/{createHibernatableWsServerAdapter-BNi4k9j3.cjs → createHibernatableWsServerAdapter-j96U9vgo.cjs} +185 -82
  13. package/build/createHibernatableWsServerAdapter-j96U9vgo.cjs.map +1 -0
  14. package/build/devtools/browser/index.cjs.map +1 -1
  15. package/build/devtools/browser/index.d.cts +1 -1
  16. package/build/devtools/browser/index.d.mts +1 -1
  17. package/build/devtools/browser/index.mjs.map +1 -1
  18. package/build/devtools/server/index.d.cts +1 -1
  19. package/build/devtools/server/index.d.mts +1 -1
  20. package/build/{httpAcceptorCarrier-C3S_bDkL.cjs → httpAcceptorCarrier-By0Qa__L.cjs} +2 -2
  21. package/build/httpAcceptorCarrier-By0Qa__L.cjs.map +1 -0
  22. package/build/{httpAcceptorCarrier-DPBEuewS.mjs → httpAcceptorCarrier-moSmtBxr.mjs} +2 -2
  23. package/build/httpAcceptorCarrier-moSmtBxr.mjs.map +1 -0
  24. package/build/index.cjs +6 -2
  25. package/build/index.cjs.map +1 -1
  26. package/build/index.d.cts +2 -2
  27. package/build/index.d.mts +2 -2
  28. package/build/index.mjs +3 -3
  29. package/build/index.mjs.map +1 -1
  30. package/build/platform/cloudflare/index.cjs +45 -1
  31. package/build/platform/cloudflare/index.cjs.map +1 -1
  32. package/build/platform/cloudflare/index.d.cts +40 -2
  33. package/build/platform/cloudflare/index.d.mts +40 -2
  34. package/build/platform/cloudflare/index.mjs +45 -2
  35. package/build/platform/cloudflare/index.mjs.map +1 -1
  36. package/build/react-query/index.d.cts +1 -1
  37. package/build/react-query/index.d.mts +1 -1
  38. package/package.json +5 -4
  39. package/build/createHibernatableWsServerAdapter-BNi4k9j3.cjs.map +0 -1
  40. package/build/createHibernatableWsServerAdapter-C07RfUTH.mjs.map +0 -1
  41. package/build/httpAcceptorCarrier-C3S_bDkL.cjs.map +0 -1
  42. package/build/httpAcceptorCarrier-DPBEuewS.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { A as encodeHandshakeMessage, C as EHandshakeMessageType, D as createServerHandshake, F as ConnectorHandler, I as createConnectorHandler, L as PeerLinkHandler, R as ETransportShape, T as createClientHandshake, _ as extractWirePayload, a as ExchangeAcceptor, b as createAcceptorHandler, c as decodeExchangeReply, d as Transport, f as createBinaryWireSessionFactory, g as buildActionRouteDictionary, h as assembleWireJson, i as createActionFetchHandler, k as decodeHandshakeMessage, l as decodeExchangeRequest, m as ReversePayloadType, n as ConnectionStateStore, o as LinkTransport, p as PayloadTypeToInt, r as createConnectionStateStore, s as ExchangeTransport, t as createHibernatableWsServerAdapter, u as encodeExchange, v as createSecureAcceptorHandler, x as createActionFrameCrypto, y as AcceptorHandler, z as ETransportStatus } from "../createHibernatableWsServerAdapter-C07RfUTH.mjs";
1
+ import { A as encodeHandshakeMessage, C as EHandshakeMessageType, D as createServerHandshake, F as ConnectorHandler, I as createConnectorHandler, L as PeerLinkHandler, R as ETransportShape, T as createClientHandshake, _ as extractWirePayload, a as ExchangeAcceptor, b as createAcceptorHandler, c as decodeExchangeReply, d as Transport, f as createBinaryWireSessionFactory, g as buildActionRouteDictionary, h as assembleWireJson, i as createActionFetchHandler, k as decodeHandshakeMessage, l as decodeExchangeRequest, m as ReversePayloadType, n as ConnectionStateStore, o as LinkTransport, p as PayloadTypeToInt, r as createConnectionStateStore, s as ExchangeTransport, t as createHibernatableWsServerAdapter, u as encodeExchange, v as createSecureAcceptorHandler, x as createActionFrameCrypto, y as AcceptorHandler, z as ETransportStatus } from "../createHibernatableWsServerAdapter-BD5n-Ev9.mjs";
2
2
  import { pack, unpack } from "msgpackr";
3
3
  //#region src/ActionRuntime/Transport/codec/createBinaryWireAdapter.ts
4
4
  /**
@@ -3,7 +3,7 @@ import { nanoid } from "nanoid";
3
3
  import { NiceError, castNiceError, err, err_nice, isNiceErrorObject } from "@nice-code/error";
4
4
  import { extractMessageFromStandardSchema } from "@nice-code/common-errors";
5
5
  import { runtime } from "std-env";
6
- import { ClientCryptoKeyLink, createTypedStorage } from "@nice-code/util";
6
+ import { ClientCryptoKeyLink, createTypedStorage, decryptBytesWithAesGcmKey, encryptBytesWithAesGcmKey } from "@nice-code/util";
7
7
  import * as v from "valibot";
8
8
  import { pack, unpack } from "msgpackr";
9
9
  const UNSET_RUNTIME_ENV_ID = "_unset_";
@@ -1403,11 +1403,13 @@ var ActionRuntime = class {
1403
1403
  * Used to locate the return-path channel for dispatching results back to the action origin.
1404
1404
  * Returns `undefined` if no handler matches (score > 0 required, i.e. at least id must match).
1405
1405
  *
1406
- * A handler that currently holds the origin's *live* connection always wins over a mere coordinate
1407
- * matchso with several duplex acceptors (e.g. WS + WebRTC) a result/push routes back over the carrier
1408
- * the client actually connected on, never a same-coordinate sibling that lacks the socket. Only when no
1409
- * handler owns a live connection do we fall back to the plain best-coordinate-score pick (the
1410
- * single-acceptor and connector-only cases, unchanged).
1406
+ * A handler that currently holds the origin's *live* connection always wins, regardless of its
1407
+ * coordinate score owning the live socket bound to the origin's exact coordinate (set from the
1408
+ * handshake) is a strictly more precise match than any env-level `peerClient` score. This lets one
1409
+ * server accept clients of *several* envs over a single acceptor (a multi-role Durable Object): the
1410
+ * result/push routes back over the carrier the client actually connected on even when the handler's
1411
+ * `clientEnv` is unset or names a different env. Only when no handler owns a live connection do we fall
1412
+ * back to the plain best-coordinate-score pick (the offline-return and connector-only cases).
1411
1413
  */
1412
1414
  getReturnHandlerForOrigin(originClient) {
1413
1415
  if (originClient.envId === "_unset_") return void 0;
@@ -1422,12 +1424,12 @@ var ActionRuntime = class {
1422
1424
  bestScore = score;
1423
1425
  bestHandler = handler;
1424
1426
  }
1425
- if (score > bestOwnedScore && handler.ownsLiveConnectionFor(originClient)) {
1427
+ if (handler.ownsLiveConnectionFor(originClient) && score > bestOwnedScore) {
1426
1428
  bestOwnedScore = score;
1427
1429
  bestOwnedHandler = handler;
1428
1430
  }
1429
1431
  }
1430
- if (bestOwnedHandler != null && bestOwnedScore > 0) return bestOwnedHandler;
1432
+ if (bestOwnedHandler != null) return bestOwnedHandler;
1431
1433
  return bestScore > 0 ? bestHandler : void 0;
1432
1434
  }
1433
1435
  resetRuntime() {
@@ -1730,6 +1732,16 @@ function createInMemoryTofuVerifyKeyResolver() {
1730
1732
  * Storage-backed trust-on-first-use resolver: pins survive process restarts / Durable Object eviction
1731
1733
  * (e.g. back it with `createDurableObjectStorageAdapter`). Same policy as the in-memory variant — trust
1732
1734
  * + pin the first verify key per client identity, reject a different one thereafter.
1735
+ *
1736
+ * Fail-closed by construction: a thrown storage read (`getJson`) or first-pin write (`updateJsonWithDef`)
1737
+ * propagates out of `resolve`, which makes `onProve` reject the handshake — a storage error can never be
1738
+ * mistaken for "first use" and silently trusted. (A genuine `undefined`/absent read is the only path to
1739
+ * a fresh pin; the underlying adapters never coerce a thrown read to `undefined`.) Keep it that way: do
1740
+ * not wrap the storage calls in a `try/catch` that swallows the error.
1741
+ *
1742
+ * On an *eventually-consistent* store a stale "absent" read re-pins the **same** verify key the client
1743
+ * just presented (it is signature-verified before this runs), so the worst case is a harmless re-write,
1744
+ * never a weakened trust decision. Cross-isolate-strong pinning still wants a strongly-consistent store.
1733
1745
  */
1734
1746
  function createStorageTofuVerifyKeyResolver(storageAdapter) {
1735
1747
  const storage = createTypedStorage({ storageAdapter });
@@ -1783,6 +1795,13 @@ function createClientHandshake(config) {
1783
1795
  bindVerifyKeysIntoDerivation: true
1784
1796
  } : {}
1785
1797
  });
1798
+ const encryptionKeyMaterial = wantsEncryption && welcome.exchangePublicKey != null ? {
1799
+ verifyPublicKey: welcome.verifyPublicKey,
1800
+ exchangePublicKey: welcome.exchangePublicKey,
1801
+ saltString: sessionSalt(clientNonce, welcome.serverNonce),
1802
+ infoString: handshakeInfo(dictionaryVersion),
1803
+ bindVerifyKeysIntoDerivation: true
1804
+ } : void 0;
1786
1805
  const challenge = buildHandshakeChallenge({
1787
1806
  securityLevel,
1788
1807
  dictionaryVersion,
@@ -1798,7 +1817,8 @@ function createClientHandshake(config) {
1798
1817
  pending = {
1799
1818
  linkedServerId,
1800
1819
  server: welcome.server,
1801
- challenge
1820
+ challenge,
1821
+ encryptionKeyMaterial
1802
1822
  };
1803
1823
  return {
1804
1824
  t: "prove",
@@ -1817,7 +1837,8 @@ function createClientHandshake(config) {
1817
1837
  return {
1818
1838
  linkedClientId: pending.linkedServerId,
1819
1839
  remote: pending.server,
1820
- securityLevel
1840
+ securityLevel,
1841
+ encryptionKeyMaterial: pending.encryptionKeyMaterial
1821
1842
  };
1822
1843
  }
1823
1844
  };
@@ -1912,6 +1933,16 @@ function createServerHandshake(config) {
1912
1933
  /** The completed handshake result once `onProve` has accepted, else `undefined`. */
1913
1934
  getResult() {
1914
1935
  return result;
1936
+ },
1937
+ exportPending() {
1938
+ return pending;
1939
+ },
1940
+ async restorePending(restored) {
1941
+ pending = restored;
1942
+ await link.linkClient({
1943
+ linkedClientId: restored.linkedClientId,
1944
+ verifyPublicKey: restored.clientVerifyKey
1945
+ });
1915
1946
  }
1916
1947
  };
1917
1948
  }
@@ -1942,14 +1973,16 @@ function parseJsonActionFrame(message) {
1942
1973
  const ENCRYPTED_ENVELOPE_LENGTH = 2;
1943
1974
  /**
1944
1975
  * Build the encrypt/decrypt transform for a connection whose handshake settled on the `encrypted`
1945
- * level. Keyed by the link + `linkedClientId`, so it reuses the cached shared AES-GCM key.
1976
+ * level. The shared key is derived once from {@link IActionFrameCryptoConfig.keyMaterial} and captured
1977
+ * for this connection alone, decoupling it from the link's shared key cache.
1946
1978
  */
1947
- function createActionFrameCrypto({ link, linkedClientId }) {
1979
+ function createActionFrameCrypto({ link, keyMaterial }) {
1980
+ const keyPromise = link.deriveSharedAesGcmKey(keyMaterial);
1948
1981
  return {
1949
1982
  async encryptFrame(frame) {
1950
- const { nonce, ciphertext } = await link.encryptBytesForLinkedClient({
1951
- linkedClientId,
1952
- dataToEncrypt: frame
1983
+ const { nonce, ciphertext } = await encryptBytesWithAesGcmKey({
1984
+ dataToEncrypt: frame,
1985
+ aesGcmKey: await keyPromise
1953
1986
  });
1954
1987
  return pack([nonce, ciphertext]);
1955
1988
  },
@@ -1959,12 +1992,12 @@ function createActionFrameCrypto({ link, linkedClientId }) {
1959
1992
  if (!Array.isArray(envelope) || envelope.length !== ENCRYPTED_ENVELOPE_LENGTH) throw new Error("[ws-crypto] malformed encrypted frame envelope");
1960
1993
  const [nonce, ciphertext] = envelope;
1961
1994
  if (!(nonce instanceof Uint8Array) || !(ciphertext instanceof Uint8Array)) throw new Error("[ws-crypto] malformed encrypted frame fields");
1962
- return await link.decryptBytesFromLinkedClient({
1963
- linkedClientId,
1995
+ return await decryptBytesWithAesGcmKey({
1964
1996
  dataToDecrypt: {
1965
1997
  nonce,
1966
1998
  ciphertext
1967
- }
1999
+ },
2000
+ aesGcmKey: await keyPromise
1968
2001
  });
1969
2002
  }
1970
2003
  };
@@ -2075,9 +2108,9 @@ var AcceptorSecureSession = class {
2075
2108
  _complete(result) {
2076
2109
  this._authed = true;
2077
2110
  this._handshake = void 0;
2078
- if (result.securityLevel === "encrypted") this._pipe = this._buildPipe(createActionFrameCrypto({
2111
+ if (result.securityLevel === "encrypted" && result.encryptionKeyMaterial != null) this._pipe = this._buildPipe(createActionFrameCrypto({
2079
2112
  link: this.config.link,
2080
- linkedClientId: result.linkedClientId
2113
+ keyMaterial: result.encryptionKeyMaterial
2081
2114
  }));
2082
2115
  this.config.onAuthenticated({
2083
2116
  client: new RuntimeCoordinate(result.remote),
@@ -2096,17 +2129,10 @@ var AcceptorSecureSession = class {
2096
2129
  this._authed = true;
2097
2130
  if (state.securityLevel !== "encrypted" || state.keyMaterial == null) return;
2098
2131
  const { link } = this.config;
2099
- const { linkedClientId, keyMaterial } = state;
2100
- const cryptoReady = link.initialize().then(() => link.linkClient({
2101
- linkedClientId,
2102
- verifyPublicKey: keyMaterial.verifyPublicKey,
2103
- exchangePublicKey: keyMaterial.exchangePublicKey,
2104
- saltString: keyMaterial.saltString,
2105
- infoString: keyMaterial.infoString,
2106
- bindVerifyKeysIntoDerivation: keyMaterial.bindVerifyKeysIntoDerivation
2107
- })).then(() => createActionFrameCrypto({
2132
+ const { keyMaterial } = state;
2133
+ const cryptoReady = link.initialize().then(() => createActionFrameCrypto({
2108
2134
  link,
2109
- linkedClientId
2135
+ keyMaterial
2110
2136
  }));
2111
2137
  cryptoReady.catch((err) => console.error("[ws-server] failed to restore encrypted session", err));
2112
2138
  this._pipe = this._buildPipe(cryptoReady);
@@ -2170,7 +2196,7 @@ var AcceptorHandler = class extends PeerLinkHandler {
2170
2196
  _codecByConn = /* @__PURE__ */ new Map();
2171
2197
  _sessionByConn = /* @__PURE__ */ new Map();
2172
2198
  constructor(options) {
2173
- super(options.clientEnv);
2199
+ super(options.clientEnv ?? RuntimeCoordinate.unknown);
2174
2200
  this._formatMessage = options.formatMessage;
2175
2201
  this._createFormatMessage = options.createFormatMessage;
2176
2202
  this._send = options.send;
@@ -2885,11 +2911,9 @@ async function runConnectorExchangeHandshake(carrier, secure) {
2885
2911
  dictionaryVersion: secure.dictionaryVersion,
2886
2912
  securityLevel: secure.securityLevel
2887
2913
  });
2888
- const hsid = nanoid();
2889
2914
  const hello = await handshake.createHello();
2890
2915
  const welcomeReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
2891
2916
  k: "hs",
2892
- hsid,
2893
2917
  m: encodeHandshakeMessage(hello)
2894
2918
  }))));
2895
2919
  if (welcomeReply?.k !== "hs") throw new Error("[exchange-handshake] expected a welcome reply");
@@ -2897,11 +2921,12 @@ async function runConnectorExchangeHandshake(carrier, secure) {
2897
2921
  if (welcome == null) throw new Error("[exchange-handshake] malformed welcome");
2898
2922
  if (welcome.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${welcome.reason}`);
2899
2923
  if (welcome.t !== "welcome") throw new Error(`[exchange-handshake] expected welcome, got ${welcome.t}`);
2924
+ if (welcomeReply.hsc == null) throw new Error("[exchange-handshake] welcome missing handshake continuation token");
2900
2925
  const prove = await handshake.onWelcome(welcome);
2901
2926
  const acceptReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
2902
2927
  k: "hs",
2903
- hsid,
2904
- m: encodeHandshakeMessage(prove)
2928
+ m: encodeHandshakeMessage(prove),
2929
+ hsc: welcomeReply.hsc
2905
2930
  }))));
2906
2931
  if (acceptReply?.k !== "hs") throw new Error("[exchange-handshake] expected an accept reply");
2907
2932
  const accept = decodeHandshakeMessage(acceptReply.m);
@@ -2910,9 +2935,9 @@ async function runConnectorExchangeHandshake(carrier, secure) {
2910
2935
  if (accept.t !== "accept") throw new Error(`[exchange-handshake] expected accept, got ${accept.t}`);
2911
2936
  if (acceptReply.t == null) throw new Error("[exchange-handshake] accept missing session token");
2912
2937
  const result = await handshake.onAccept(accept);
2913
- const crypto = result.securityLevel === "encrypted" ? createActionFrameCrypto({
2938
+ const crypto = result.securityLevel === "encrypted" && result.encryptionKeyMaterial != null ? createActionFrameCrypto({
2914
2939
  link: secure.link,
2915
- linkedClientId: result.linkedClientId
2940
+ keyMaterial: result.encryptionKeyMaterial
2916
2941
  }) : void 0;
2917
2942
  return {
2918
2943
  token: acceptReply.t,
@@ -3220,9 +3245,9 @@ async function runClientHandshake(channel, secure, nextHandshakeMessage) {
3220
3245
  if (accept.t === "reject") throw new Error(`[link-handshake] rejected by peer: ${accept.reason}`);
3221
3246
  if (accept.t !== "accept") throw new Error(`[link-handshake] expected accept, got ${accept.t}`);
3222
3247
  const result = await handshake.onAccept(accept);
3223
- return result.securityLevel === "encrypted" ? createActionFrameCrypto({
3248
+ return result.securityLevel === "encrypted" && result.encryptionKeyMaterial != null ? createActionFrameCrypto({
3224
3249
  link: secure.link,
3225
- linkedClientId: result.linkedClientId
3250
+ keyMaterial: result.encryptionKeyMaterial
3226
3251
  }) : void 0;
3227
3252
  }
3228
3253
  function buildSendMethods(ctx, pipe, disconnectListeners, abortSet) {
@@ -3376,82 +3401,154 @@ var LinkTransport = class LinkTransport extends Transport {
3376
3401
  }
3377
3402
  };
3378
3403
  //#endregion
3404
+ //#region src/ActionRuntime/Transport/SecureSession/exchangeTicketSeal.ts
3405
+ const SEALED_ENVELOPE_LENGTH = 2;
3406
+ function createExchangeTicketSealer(link, options = {}) {
3407
+ let keyPromise;
3408
+ const getKey = () => {
3409
+ if (keyPromise == null) keyPromise = link.deriveLocalSealKey({ version: options.version }).catch((err) => {
3410
+ keyPromise = void 0;
3411
+ throw err;
3412
+ });
3413
+ return keyPromise;
3414
+ };
3415
+ return {
3416
+ async seal(value, ttlMs) {
3417
+ const { nonce, ciphertext } = await encryptBytesWithAesGcmKey({
3418
+ dataToEncrypt: pack({
3419
+ v: value,
3420
+ exp: Date.now() + ttlMs
3421
+ }),
3422
+ aesGcmKey: await getKey()
3423
+ });
3424
+ return bytesToBase64(pack([nonce, ciphertext]));
3425
+ },
3426
+ async open(blob) {
3427
+ try {
3428
+ const envelope = unpack(base64ToBytes(blob));
3429
+ if (!Array.isArray(envelope) || envelope.length !== SEALED_ENVELOPE_LENGTH) return void 0;
3430
+ const [nonce, ciphertext] = envelope;
3431
+ if (!(nonce instanceof Uint8Array) || !(ciphertext instanceof Uint8Array)) return void 0;
3432
+ const payload = unpack(await decryptBytesWithAesGcmKey({
3433
+ dataToDecrypt: {
3434
+ nonce,
3435
+ ciphertext
3436
+ },
3437
+ aesGcmKey: await getKey()
3438
+ }));
3439
+ if (payload == null || typeof payload.exp !== "number" || payload.exp < Date.now()) return;
3440
+ return payload.v;
3441
+ } catch {
3442
+ return;
3443
+ }
3444
+ }
3445
+ };
3446
+ }
3447
+ //#endregion
3379
3448
  //#region src/ActionRuntime/Transport/SecureSession/exchangeAcceptor.ts
3449
+ /** The default session-ticket lifetime — see {@link IExchangeAcceptorConfig.sessionTtlMs}. */
3450
+ const DEFAULT_SESSION_TTL_MS = 720 * 60 * 1e3;
3451
+ /** The handshake continuation (`hsc`) only bridges the two handshake POSTs, so it expires quickly. */
3452
+ const HANDSHAKE_CONTINUATION_TTL_MS = 120 * 1e3;
3380
3453
  const textEncoder = new TextEncoder();
3381
3454
  const textDecoder = new TextDecoder();
3382
3455
  /**
3383
3456
  * Acceptor (accept-in) side of the secure exchange protocol — the HTTP counterpart to
3384
3457
  * {@link AcceptorSecureSession}. Each POST body is one {@link decodeExchangeRequest} envelope; the
3385
- * acceptor drives the server handshake over the two `hs` POSTs (correlated by `hsid`, since stateless
3386
- * requests can't rely on channel ordering), mints a session **token** on accept, and on every later `act`
3387
- * POST resolves the session by token, decrypts the body (at `encrypted`), routes it through the runtime,
3388
- * and returns the (encrypted) result inline as the reply.
3458
+ * acceptor drives the server handshake over the two `hs` POSTs, mints a session **token** on accept, and
3459
+ * on every later `act` POST resolves the session by token, decrypts the body (at `encrypted`), routes it
3460
+ * through the runtime, and returns the (encrypted) result inline as the reply.
3389
3461
  *
3390
- * Sessions and in-flight handshakes are held in memory fine for a single-instance server. (Surviving a
3391
- * Durable-Object eviction would persist each token's `keyMaterial` and re-derive the key on a miss, the
3392
- * same primitive `AcceptorSecureSession.rehydrate` uses; left as a follow-up.)
3462
+ * **Stateless.** It holds no in-memory handshakes or sessions: the in-flight handshake `pending` is
3463
+ * sealed into the `hsc` continuation token returned on `welcome` and echoed back on `prove`, and the live
3464
+ * session is sealed into the `t` token replayed on every `act`. Both are sealed under the acceptor's own
3465
+ * persisted identity ({@link createExchangeTicketSealer}), so any isolate that loaded the same identity
3466
+ * can serve any POST — no request needs to co-locate with another (no Durable Object required just to
3467
+ * pin a handshake to one instance). A tampered, wrong-key, or expired token opens to "no valid session".
3393
3468
  */
3394
3469
  var ExchangeAcceptor = class {
3395
3470
  _security;
3396
3471
  _runtime;
3397
3472
  _allowedLevels;
3398
3473
  _noneAllowed;
3399
- _pendingHandshakes = /* @__PURE__ */ new Map();
3400
- _sessions = /* @__PURE__ */ new Map();
3474
+ _sealer;
3475
+ _sessionTtlMs;
3401
3476
  constructor(config) {
3402
3477
  this._security = config.security;
3403
3478
  this._runtime = config.runtime;
3404
3479
  this._allowedLevels = Array.isArray(config.security.securityLevel) ? config.security.securityLevel : [config.security.securityLevel];
3405
3480
  this._noneAllowed = this._allowedLevels.includes("none");
3481
+ this._sealer = createExchangeTicketSealer(config.security.link);
3482
+ this._sessionTtlMs = config.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS;
3406
3483
  }
3407
3484
  /** Process one POST body (an exchange envelope), returning the reply body to send back. */
3408
3485
  async handlePost(body) {
3409
3486
  const request = decodeExchangeRequest(body);
3410
3487
  if (request == null) return this._err("malformed exchange request");
3488
+ await this._security.link.initialize();
3411
3489
  if (request.k === "hs") return encodeExchange(await this._handleHandshake(request));
3412
3490
  return encodeExchange(await this._handleAction(request));
3413
3491
  }
3492
+ _makeHandshake() {
3493
+ const security = this._security;
3494
+ return createServerHandshake({
3495
+ link: security.link,
3496
+ localCoordinate: security.localCoordinate,
3497
+ dictionaryVersion: security.dictionaryVersion,
3498
+ securityLevel: security.securityLevel,
3499
+ verifyKeyResolver: security.verifyKeyResolver
3500
+ });
3501
+ }
3414
3502
  async _handleHandshake(request) {
3415
3503
  const message = decodeHandshakeMessage(request.m);
3416
3504
  if (message == null) return {
3417
3505
  k: "err",
3418
3506
  message: "malformed handshake message"
3419
3507
  };
3420
- const security = this._security;
3421
- await security.link.initialize();
3422
- let handshake = this._pendingHandshakes.get(request.hsid);
3423
- if (handshake == null) {
3424
- handshake = createServerHandshake({
3425
- link: security.link,
3426
- localCoordinate: security.localCoordinate,
3427
- dictionaryVersion: security.dictionaryVersion,
3428
- securityLevel: security.securityLevel,
3429
- verifyKeyResolver: security.verifyKeyResolver
3430
- });
3431
- this._pendingHandshakes.set(request.hsid, handshake);
3508
+ if (message.t === "hello") {
3509
+ const handshake = this._makeHandshake();
3510
+ const reply = await handshake.onHello(message);
3511
+ if (reply.t === "reject") return {
3512
+ k: "hs",
3513
+ m: encodeHandshakeMessage(reply)
3514
+ };
3515
+ const pending = handshake.exportPending();
3516
+ if (pending == null) return {
3517
+ k: "err",
3518
+ message: "handshake produced no continuation state"
3519
+ };
3520
+ const hsc = await this._sealer.seal(pending, HANDSHAKE_CONTINUATION_TTL_MS);
3521
+ return {
3522
+ k: "hs",
3523
+ m: encodeHandshakeMessage(reply),
3524
+ hsc
3525
+ };
3432
3526
  }
3433
- if (message.t === "hello") return {
3434
- k: "hs",
3435
- m: encodeHandshakeMessage(await handshake.onHello(message))
3436
- };
3437
3527
  if (message.t === "prove") {
3528
+ if (request.hsc == null) return {
3529
+ k: "err",
3530
+ message: "prove missing continuation token"
3531
+ };
3532
+ const pending = await this._sealer.open(request.hsc);
3533
+ if (pending == null) return {
3534
+ k: "err",
3535
+ message: "invalid or expired continuation token"
3536
+ };
3537
+ const handshake = this._makeHandshake();
3538
+ await handshake.restorePending(pending);
3438
3539
  const reply = await handshake.onProve(message);
3439
- this._pendingHandshakes.delete(request.hsid);
3440
3540
  const result = handshake.getResult();
3441
3541
  if (reply.t === "accept" && result != null) {
3442
- const token = nanoid();
3443
- this._sessions.set(token, {
3444
- client: new RuntimeCoordinate(result.remote),
3542
+ const ticket = {
3543
+ client: result.remote,
3445
3544
  securityLevel: result.securityLevel,
3446
- crypto: result.securityLevel === "encrypted" ? createActionFrameCrypto({
3447
- link: security.link,
3448
- linkedClientId: result.linkedClientId
3449
- }) : void 0
3450
- });
3545
+ keyMaterial: result.encryptionKeyMaterial
3546
+ };
3547
+ const t = await this._sealer.seal(ticket, this._sessionTtlMs);
3451
3548
  return {
3452
3549
  k: "hs",
3453
3550
  m: encodeHandshakeMessage(reply),
3454
- t: token
3551
+ t
3455
3552
  };
3456
3553
  }
3457
3554
  return {
@@ -3465,20 +3562,26 @@ var ExchangeAcceptor = class {
3465
3562
  };
3466
3563
  }
3467
3564
  async _handleAction(request) {
3468
- let session;
3565
+ let client;
3566
+ let crypto;
3469
3567
  let candidate;
3470
3568
  if (request.t != null) {
3471
- session = this._sessions.get(request.t);
3472
- if (session == null) return {
3569
+ const ticket = await this._sealer.open(request.t);
3570
+ if (ticket == null) return {
3473
3571
  k: "err",
3474
3572
  message: "unknown or expired session token"
3475
3573
  };
3574
+ client = new RuntimeCoordinate(ticket.client);
3575
+ crypto = ticket.securityLevel === "encrypted" && ticket.keyMaterial != null ? createActionFrameCrypto({
3576
+ link: this._security.link,
3577
+ keyMaterial: ticket.keyMaterial
3578
+ }) : void 0;
3476
3579
  if ("c" in request) {
3477
- if (session.crypto == null) return {
3580
+ if (crypto == null) return {
3478
3581
  k: "err",
3479
3582
  message: "session is not encrypted"
3480
3583
  };
3481
- const plain = await session.crypto.decryptFrame(base64ToBytes(request.c));
3584
+ const plain = await crypto.decryptFrame(base64ToBytes(request.c));
3482
3585
  candidate = JSON.parse(textDecoder.decode(plain));
3483
3586
  } else candidate = request.w;
3484
3587
  } else {
@@ -3493,11 +3596,11 @@ var ExchangeAcceptor = class {
3493
3596
  message: "malformed action wire"
3494
3597
  };
3495
3598
  const wire = candidate;
3496
- if (session != null && wire.type === "request") wire.context.originClient = session.client.toJsonObject();
3599
+ if (client != null && wire.type === "request") wire.context.originClient = client.toJsonObject();
3497
3600
  const resultWire = (await (await this._runtime.handleActionPayloadWire(wire)).waitForResultPayload()).toJsonObject();
3498
- if (session?.crypto != null) return {
3601
+ if (crypto != null && "c" in request) return {
3499
3602
  k: "act",
3500
- c: bytesToBase64(await session.crypto.encryptFrame(textEncoder.encode(JSON.stringify(resultWire))))
3603
+ c: bytesToBase64(await crypto.encryptFrame(textEncoder.encode(JSON.stringify(resultWire))))
3501
3604
  };
3502
3605
  return {
3503
3606
  k: "act",
@@ -3715,4 +3818,4 @@ function createHibernatableWsServerAdapter(options) {
3715
3818
  //#endregion
3716
3819
  export { ActionPayload_Request as $, encodeHandshakeMessage as A, EErrId_NiceTransport as B, EHandshakeMessageType as C, createServerHandshake as D, createInMemoryTofuVerifyKeyResolver as E, ConnectorHandler as F, ActionSchema as G, err_nice_external_client as H, createConnectorHandler as I, isActionPayload_Result_JsonObject as J, EActionResponseMode as K, PeerLinkHandler as L, ActionLocalHandler as M, createLocalHandler as N, createStorageTofuVerifyKeyResolver as O, ActionRuntime as P, RunningAction as Q, ETransportShape as R, decodeActionFrame as S, createClientHandshake as T, isActionPayload_Any_JsonObject as U, err_nice_transport as V, isActionPayload_Request_JsonObject as W, EErrId_NiceAction as X, isAction_Base_JsonObject as Y, err_nice_action as Z, extractWirePayload as _, ExchangeAcceptor as a, RuntimeCoordinate as at, createAcceptorHandler as b, decodeExchangeReply as c, Transport as d, ActionPayload_Result as et, createBinaryWireSessionFactory as f, buildActionRouteDictionary as g, assembleWireJson as h, createActionFetchHandler as i, ActionBase as it, runtimeLinkId as j, decodeHandshakeMessage as k, decodeExchangeRequest as l, ReversePayloadType as m, ConnectionStateStore as n, EActionProgressType as nt, LinkTransport as o, runtimeCoordinateToStringIds as ot, PayloadTypeToInt as p, actionSchema as q, createConnectionStateStore as r, ActionPayload as rt, ExchangeTransport as s, createHibernatableWsServerAdapter as t, EActionPayloadType as tt, encodeExchange as u, createSecureAcceptorHandler as v, ESecurityLevel as w, createActionFrameCrypto as x, AcceptorHandler as y, ETransportStatus as z };
3717
3820
 
3718
- //# sourceMappingURL=createHibernatableWsServerAdapter-C07RfUTH.mjs.map
3821
+ //# sourceMappingURL=createHibernatableWsServerAdapter-BD5n-Ev9.mjs.map