@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
@@ -1405,11 +1405,13 @@ var ActionRuntime = class {
1405
1405
  * Used to locate the return-path channel for dispatching results back to the action origin.
1406
1406
  * Returns `undefined` if no handler matches (score > 0 required, i.e. at least id must match).
1407
1407
  *
1408
- * A handler that currently holds the origin's *live* connection always wins over a mere coordinate
1409
- * matchso with several duplex acceptors (e.g. WS + WebRTC) a result/push routes back over the carrier
1410
- * the client actually connected on, never a same-coordinate sibling that lacks the socket. Only when no
1411
- * handler owns a live connection do we fall back to the plain best-coordinate-score pick (the
1412
- * single-acceptor and connector-only cases, unchanged).
1408
+ * A handler that currently holds the origin's *live* connection always wins, regardless of its
1409
+ * coordinate score owning the live socket bound to the origin's exact coordinate (set from the
1410
+ * handshake) is a strictly more precise match than any env-level `peerClient` score. This lets one
1411
+ * server accept clients of *several* envs over a single acceptor (a multi-role Durable Object): the
1412
+ * result/push routes back over the carrier the client actually connected on even when the handler's
1413
+ * `clientEnv` is unset or names a different env. Only when no handler owns a live connection do we fall
1414
+ * back to the plain best-coordinate-score pick (the offline-return and connector-only cases).
1413
1415
  */
1414
1416
  getReturnHandlerForOrigin(originClient) {
1415
1417
  if (originClient.envId === "_unset_") return void 0;
@@ -1424,12 +1426,12 @@ var ActionRuntime = class {
1424
1426
  bestScore = score;
1425
1427
  bestHandler = handler;
1426
1428
  }
1427
- if (score > bestOwnedScore && handler.ownsLiveConnectionFor(originClient)) {
1429
+ if (handler.ownsLiveConnectionFor(originClient) && score > bestOwnedScore) {
1428
1430
  bestOwnedScore = score;
1429
1431
  bestOwnedHandler = handler;
1430
1432
  }
1431
1433
  }
1432
- if (bestOwnedHandler != null && bestOwnedScore > 0) return bestOwnedHandler;
1434
+ if (bestOwnedHandler != null) return bestOwnedHandler;
1433
1435
  return bestScore > 0 ? bestHandler : void 0;
1434
1436
  }
1435
1437
  resetRuntime() {
@@ -1732,6 +1734,16 @@ function createInMemoryTofuVerifyKeyResolver() {
1732
1734
  * Storage-backed trust-on-first-use resolver: pins survive process restarts / Durable Object eviction
1733
1735
  * (e.g. back it with `createDurableObjectStorageAdapter`). Same policy as the in-memory variant — trust
1734
1736
  * + pin the first verify key per client identity, reject a different one thereafter.
1737
+ *
1738
+ * Fail-closed by construction: a thrown storage read (`getJson`) or first-pin write (`updateJsonWithDef`)
1739
+ * propagates out of `resolve`, which makes `onProve` reject the handshake — a storage error can never be
1740
+ * mistaken for "first use" and silently trusted. (A genuine `undefined`/absent read is the only path to
1741
+ * a fresh pin; the underlying adapters never coerce a thrown read to `undefined`.) Keep it that way: do
1742
+ * not wrap the storage calls in a `try/catch` that swallows the error.
1743
+ *
1744
+ * On an *eventually-consistent* store a stale "absent" read re-pins the **same** verify key the client
1745
+ * just presented (it is signature-verified before this runs), so the worst case is a harmless re-write,
1746
+ * never a weakened trust decision. Cross-isolate-strong pinning still wants a strongly-consistent store.
1735
1747
  */
1736
1748
  function createStorageTofuVerifyKeyResolver(storageAdapter) {
1737
1749
  const storage = (0, _nice_code_util.createTypedStorage)({ storageAdapter });
@@ -1785,6 +1797,13 @@ function createClientHandshake(config) {
1785
1797
  bindVerifyKeysIntoDerivation: true
1786
1798
  } : {}
1787
1799
  });
1800
+ const encryptionKeyMaterial = wantsEncryption && welcome.exchangePublicKey != null ? {
1801
+ verifyPublicKey: welcome.verifyPublicKey,
1802
+ exchangePublicKey: welcome.exchangePublicKey,
1803
+ saltString: sessionSalt(clientNonce, welcome.serverNonce),
1804
+ infoString: handshakeInfo(dictionaryVersion),
1805
+ bindVerifyKeysIntoDerivation: true
1806
+ } : void 0;
1788
1807
  const challenge = buildHandshakeChallenge({
1789
1808
  securityLevel,
1790
1809
  dictionaryVersion,
@@ -1800,7 +1819,8 @@ function createClientHandshake(config) {
1800
1819
  pending = {
1801
1820
  linkedServerId,
1802
1821
  server: welcome.server,
1803
- challenge
1822
+ challenge,
1823
+ encryptionKeyMaterial
1804
1824
  };
1805
1825
  return {
1806
1826
  t: "prove",
@@ -1819,7 +1839,8 @@ function createClientHandshake(config) {
1819
1839
  return {
1820
1840
  linkedClientId: pending.linkedServerId,
1821
1841
  remote: pending.server,
1822
- securityLevel
1842
+ securityLevel,
1843
+ encryptionKeyMaterial: pending.encryptionKeyMaterial
1823
1844
  };
1824
1845
  }
1825
1846
  };
@@ -1914,6 +1935,16 @@ function createServerHandshake(config) {
1914
1935
  /** The completed handshake result once `onProve` has accepted, else `undefined`. */
1915
1936
  getResult() {
1916
1937
  return result;
1938
+ },
1939
+ exportPending() {
1940
+ return pending;
1941
+ },
1942
+ async restorePending(restored) {
1943
+ pending = restored;
1944
+ await link.linkClient({
1945
+ linkedClientId: restored.linkedClientId,
1946
+ verifyPublicKey: restored.clientVerifyKey
1947
+ });
1917
1948
  }
1918
1949
  };
1919
1950
  }
@@ -1944,14 +1975,16 @@ function parseJsonActionFrame(message) {
1944
1975
  const ENCRYPTED_ENVELOPE_LENGTH = 2;
1945
1976
  /**
1946
1977
  * Build the encrypt/decrypt transform for a connection whose handshake settled on the `encrypted`
1947
- * level. Keyed by the link + `linkedClientId`, so it reuses the cached shared AES-GCM key.
1978
+ * level. The shared key is derived once from {@link IActionFrameCryptoConfig.keyMaterial} and captured
1979
+ * for this connection alone, decoupling it from the link's shared key cache.
1948
1980
  */
1949
- function createActionFrameCrypto({ link, linkedClientId }) {
1981
+ function createActionFrameCrypto({ link, keyMaterial }) {
1982
+ const keyPromise = link.deriveSharedAesGcmKey(keyMaterial);
1950
1983
  return {
1951
1984
  async encryptFrame(frame) {
1952
- const { nonce, ciphertext } = await link.encryptBytesForLinkedClient({
1953
- linkedClientId,
1954
- dataToEncrypt: frame
1985
+ const { nonce, ciphertext } = await (0, _nice_code_util.encryptBytesWithAesGcmKey)({
1986
+ dataToEncrypt: frame,
1987
+ aesGcmKey: await keyPromise
1955
1988
  });
1956
1989
  return (0, msgpackr.pack)([nonce, ciphertext]);
1957
1990
  },
@@ -1961,12 +1994,12 @@ function createActionFrameCrypto({ link, linkedClientId }) {
1961
1994
  if (!Array.isArray(envelope) || envelope.length !== ENCRYPTED_ENVELOPE_LENGTH) throw new Error("[ws-crypto] malformed encrypted frame envelope");
1962
1995
  const [nonce, ciphertext] = envelope;
1963
1996
  if (!(nonce instanceof Uint8Array) || !(ciphertext instanceof Uint8Array)) throw new Error("[ws-crypto] malformed encrypted frame fields");
1964
- return await link.decryptBytesFromLinkedClient({
1965
- linkedClientId,
1997
+ return await (0, _nice_code_util.decryptBytesWithAesGcmKey)({
1966
1998
  dataToDecrypt: {
1967
1999
  nonce,
1968
2000
  ciphertext
1969
- }
2001
+ },
2002
+ aesGcmKey: await keyPromise
1970
2003
  });
1971
2004
  }
1972
2005
  };
@@ -2077,9 +2110,9 @@ var AcceptorSecureSession = class {
2077
2110
  _complete(result) {
2078
2111
  this._authed = true;
2079
2112
  this._handshake = void 0;
2080
- if (result.securityLevel === "encrypted") this._pipe = this._buildPipe(createActionFrameCrypto({
2113
+ if (result.securityLevel === "encrypted" && result.encryptionKeyMaterial != null) this._pipe = this._buildPipe(createActionFrameCrypto({
2081
2114
  link: this.config.link,
2082
- linkedClientId: result.linkedClientId
2115
+ keyMaterial: result.encryptionKeyMaterial
2083
2116
  }));
2084
2117
  this.config.onAuthenticated({
2085
2118
  client: new RuntimeCoordinate(result.remote),
@@ -2098,17 +2131,10 @@ var AcceptorSecureSession = class {
2098
2131
  this._authed = true;
2099
2132
  if (state.securityLevel !== "encrypted" || state.keyMaterial == null) return;
2100
2133
  const { link } = this.config;
2101
- const { linkedClientId, keyMaterial } = state;
2102
- const cryptoReady = link.initialize().then(() => link.linkClient({
2103
- linkedClientId,
2104
- verifyPublicKey: keyMaterial.verifyPublicKey,
2105
- exchangePublicKey: keyMaterial.exchangePublicKey,
2106
- saltString: keyMaterial.saltString,
2107
- infoString: keyMaterial.infoString,
2108
- bindVerifyKeysIntoDerivation: keyMaterial.bindVerifyKeysIntoDerivation
2109
- })).then(() => createActionFrameCrypto({
2134
+ const { keyMaterial } = state;
2135
+ const cryptoReady = link.initialize().then(() => createActionFrameCrypto({
2110
2136
  link,
2111
- linkedClientId
2137
+ keyMaterial
2112
2138
  }));
2113
2139
  cryptoReady.catch((err) => console.error("[ws-server] failed to restore encrypted session", err));
2114
2140
  this._pipe = this._buildPipe(cryptoReady);
@@ -2172,7 +2198,7 @@ var AcceptorHandler = class extends PeerLinkHandler {
2172
2198
  _codecByConn = /* @__PURE__ */ new Map();
2173
2199
  _sessionByConn = /* @__PURE__ */ new Map();
2174
2200
  constructor(options) {
2175
- super(options.clientEnv);
2201
+ super(options.clientEnv ?? RuntimeCoordinate.unknown);
2176
2202
  this._formatMessage = options.formatMessage;
2177
2203
  this._createFormatMessage = options.createFormatMessage;
2178
2204
  this._send = options.send;
@@ -2887,11 +2913,9 @@ async function runConnectorExchangeHandshake(carrier, secure) {
2887
2913
  dictionaryVersion: secure.dictionaryVersion,
2888
2914
  securityLevel: secure.securityLevel
2889
2915
  });
2890
- const hsid = (0, nanoid.nanoid)();
2891
2916
  const hello = await handshake.createHello();
2892
2917
  const welcomeReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
2893
2918
  k: "hs",
2894
- hsid,
2895
2919
  m: encodeHandshakeMessage(hello)
2896
2920
  }))));
2897
2921
  if (welcomeReply?.k !== "hs") throw new Error("[exchange-handshake] expected a welcome reply");
@@ -2899,11 +2923,12 @@ async function runConnectorExchangeHandshake(carrier, secure) {
2899
2923
  if (welcome == null) throw new Error("[exchange-handshake] malformed welcome");
2900
2924
  if (welcome.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${welcome.reason}`);
2901
2925
  if (welcome.t !== "welcome") throw new Error(`[exchange-handshake] expected welcome, got ${welcome.t}`);
2926
+ if (welcomeReply.hsc == null) throw new Error("[exchange-handshake] welcome missing handshake continuation token");
2902
2927
  const prove = await handshake.onWelcome(welcome);
2903
2928
  const acceptReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
2904
2929
  k: "hs",
2905
- hsid,
2906
- m: encodeHandshakeMessage(prove)
2930
+ m: encodeHandshakeMessage(prove),
2931
+ hsc: welcomeReply.hsc
2907
2932
  }))));
2908
2933
  if (acceptReply?.k !== "hs") throw new Error("[exchange-handshake] expected an accept reply");
2909
2934
  const accept = decodeHandshakeMessage(acceptReply.m);
@@ -2912,9 +2937,9 @@ async function runConnectorExchangeHandshake(carrier, secure) {
2912
2937
  if (accept.t !== "accept") throw new Error(`[exchange-handshake] expected accept, got ${accept.t}`);
2913
2938
  if (acceptReply.t == null) throw new Error("[exchange-handshake] accept missing session token");
2914
2939
  const result = await handshake.onAccept(accept);
2915
- const crypto = result.securityLevel === "encrypted" ? createActionFrameCrypto({
2940
+ const crypto = result.securityLevel === "encrypted" && result.encryptionKeyMaterial != null ? createActionFrameCrypto({
2916
2941
  link: secure.link,
2917
- linkedClientId: result.linkedClientId
2942
+ keyMaterial: result.encryptionKeyMaterial
2918
2943
  }) : void 0;
2919
2944
  return {
2920
2945
  token: acceptReply.t,
@@ -3222,9 +3247,9 @@ async function runClientHandshake(channel, secure, nextHandshakeMessage) {
3222
3247
  if (accept.t === "reject") throw new Error(`[link-handshake] rejected by peer: ${accept.reason}`);
3223
3248
  if (accept.t !== "accept") throw new Error(`[link-handshake] expected accept, got ${accept.t}`);
3224
3249
  const result = await handshake.onAccept(accept);
3225
- return result.securityLevel === "encrypted" ? createActionFrameCrypto({
3250
+ return result.securityLevel === "encrypted" && result.encryptionKeyMaterial != null ? createActionFrameCrypto({
3226
3251
  link: secure.link,
3227
- linkedClientId: result.linkedClientId
3252
+ keyMaterial: result.encryptionKeyMaterial
3228
3253
  }) : void 0;
3229
3254
  }
3230
3255
  function buildSendMethods(ctx, pipe, disconnectListeners, abortSet) {
@@ -3378,82 +3403,154 @@ var LinkTransport = class LinkTransport extends Transport {
3378
3403
  }
3379
3404
  };
3380
3405
  //#endregion
3406
+ //#region src/ActionRuntime/Transport/SecureSession/exchangeTicketSeal.ts
3407
+ const SEALED_ENVELOPE_LENGTH = 2;
3408
+ function createExchangeTicketSealer(link, options = {}) {
3409
+ let keyPromise;
3410
+ const getKey = () => {
3411
+ if (keyPromise == null) keyPromise = link.deriveLocalSealKey({ version: options.version }).catch((err) => {
3412
+ keyPromise = void 0;
3413
+ throw err;
3414
+ });
3415
+ return keyPromise;
3416
+ };
3417
+ return {
3418
+ async seal(value, ttlMs) {
3419
+ const { nonce, ciphertext } = await (0, _nice_code_util.encryptBytesWithAesGcmKey)({
3420
+ dataToEncrypt: (0, msgpackr.pack)({
3421
+ v: value,
3422
+ exp: Date.now() + ttlMs
3423
+ }),
3424
+ aesGcmKey: await getKey()
3425
+ });
3426
+ return bytesToBase64((0, msgpackr.pack)([nonce, ciphertext]));
3427
+ },
3428
+ async open(blob) {
3429
+ try {
3430
+ const envelope = (0, msgpackr.unpack)(base64ToBytes(blob));
3431
+ if (!Array.isArray(envelope) || envelope.length !== SEALED_ENVELOPE_LENGTH) return void 0;
3432
+ const [nonce, ciphertext] = envelope;
3433
+ if (!(nonce instanceof Uint8Array) || !(ciphertext instanceof Uint8Array)) return void 0;
3434
+ const payload = (0, msgpackr.unpack)(await (0, _nice_code_util.decryptBytesWithAesGcmKey)({
3435
+ dataToDecrypt: {
3436
+ nonce,
3437
+ ciphertext
3438
+ },
3439
+ aesGcmKey: await getKey()
3440
+ }));
3441
+ if (payload == null || typeof payload.exp !== "number" || payload.exp < Date.now()) return;
3442
+ return payload.v;
3443
+ } catch {
3444
+ return;
3445
+ }
3446
+ }
3447
+ };
3448
+ }
3449
+ //#endregion
3381
3450
  //#region src/ActionRuntime/Transport/SecureSession/exchangeAcceptor.ts
3451
+ /** The default session-ticket lifetime — see {@link IExchangeAcceptorConfig.sessionTtlMs}. */
3452
+ const DEFAULT_SESSION_TTL_MS = 720 * 60 * 1e3;
3453
+ /** The handshake continuation (`hsc`) only bridges the two handshake POSTs, so it expires quickly. */
3454
+ const HANDSHAKE_CONTINUATION_TTL_MS = 120 * 1e3;
3382
3455
  const textEncoder = new TextEncoder();
3383
3456
  const textDecoder = new TextDecoder();
3384
3457
  /**
3385
3458
  * Acceptor (accept-in) side of the secure exchange protocol — the HTTP counterpart to
3386
3459
  * {@link AcceptorSecureSession}. Each POST body is one {@link decodeExchangeRequest} envelope; the
3387
- * acceptor drives the server handshake over the two `hs` POSTs (correlated by `hsid`, since stateless
3388
- * requests can't rely on channel ordering), mints a session **token** on accept, and on every later `act`
3389
- * POST resolves the session by token, decrypts the body (at `encrypted`), routes it through the runtime,
3390
- * and returns the (encrypted) result inline as the reply.
3460
+ * acceptor drives the server handshake over the two `hs` POSTs, mints a session **token** on accept, and
3461
+ * on every later `act` POST resolves the session by token, decrypts the body (at `encrypted`), routes it
3462
+ * through the runtime, and returns the (encrypted) result inline as the reply.
3391
3463
  *
3392
- * Sessions and in-flight handshakes are held in memory fine for a single-instance server. (Surviving a
3393
- * Durable-Object eviction would persist each token's `keyMaterial` and re-derive the key on a miss, the
3394
- * same primitive `AcceptorSecureSession.rehydrate` uses; left as a follow-up.)
3464
+ * **Stateless.** It holds no in-memory handshakes or sessions: the in-flight handshake `pending` is
3465
+ * sealed into the `hsc` continuation token returned on `welcome` and echoed back on `prove`, and the live
3466
+ * session is sealed into the `t` token replayed on every `act`. Both are sealed under the acceptor's own
3467
+ * persisted identity ({@link createExchangeTicketSealer}), so any isolate that loaded the same identity
3468
+ * can serve any POST — no request needs to co-locate with another (no Durable Object required just to
3469
+ * pin a handshake to one instance). A tampered, wrong-key, or expired token opens to "no valid session".
3395
3470
  */
3396
3471
  var ExchangeAcceptor = class {
3397
3472
  _security;
3398
3473
  _runtime;
3399
3474
  _allowedLevels;
3400
3475
  _noneAllowed;
3401
- _pendingHandshakes = /* @__PURE__ */ new Map();
3402
- _sessions = /* @__PURE__ */ new Map();
3476
+ _sealer;
3477
+ _sessionTtlMs;
3403
3478
  constructor(config) {
3404
3479
  this._security = config.security;
3405
3480
  this._runtime = config.runtime;
3406
3481
  this._allowedLevels = Array.isArray(config.security.securityLevel) ? config.security.securityLevel : [config.security.securityLevel];
3407
3482
  this._noneAllowed = this._allowedLevels.includes("none");
3483
+ this._sealer = createExchangeTicketSealer(config.security.link);
3484
+ this._sessionTtlMs = config.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS;
3408
3485
  }
3409
3486
  /** Process one POST body (an exchange envelope), returning the reply body to send back. */
3410
3487
  async handlePost(body) {
3411
3488
  const request = decodeExchangeRequest(body);
3412
3489
  if (request == null) return this._err("malformed exchange request");
3490
+ await this._security.link.initialize();
3413
3491
  if (request.k === "hs") return encodeExchange(await this._handleHandshake(request));
3414
3492
  return encodeExchange(await this._handleAction(request));
3415
3493
  }
3494
+ _makeHandshake() {
3495
+ const security = this._security;
3496
+ return createServerHandshake({
3497
+ link: security.link,
3498
+ localCoordinate: security.localCoordinate,
3499
+ dictionaryVersion: security.dictionaryVersion,
3500
+ securityLevel: security.securityLevel,
3501
+ verifyKeyResolver: security.verifyKeyResolver
3502
+ });
3503
+ }
3416
3504
  async _handleHandshake(request) {
3417
3505
  const message = decodeHandshakeMessage(request.m);
3418
3506
  if (message == null) return {
3419
3507
  k: "err",
3420
3508
  message: "malformed handshake message"
3421
3509
  };
3422
- const security = this._security;
3423
- await security.link.initialize();
3424
- let handshake = this._pendingHandshakes.get(request.hsid);
3425
- if (handshake == null) {
3426
- handshake = createServerHandshake({
3427
- link: security.link,
3428
- localCoordinate: security.localCoordinate,
3429
- dictionaryVersion: security.dictionaryVersion,
3430
- securityLevel: security.securityLevel,
3431
- verifyKeyResolver: security.verifyKeyResolver
3432
- });
3433
- this._pendingHandshakes.set(request.hsid, handshake);
3510
+ if (message.t === "hello") {
3511
+ const handshake = this._makeHandshake();
3512
+ const reply = await handshake.onHello(message);
3513
+ if (reply.t === "reject") return {
3514
+ k: "hs",
3515
+ m: encodeHandshakeMessage(reply)
3516
+ };
3517
+ const pending = handshake.exportPending();
3518
+ if (pending == null) return {
3519
+ k: "err",
3520
+ message: "handshake produced no continuation state"
3521
+ };
3522
+ const hsc = await this._sealer.seal(pending, HANDSHAKE_CONTINUATION_TTL_MS);
3523
+ return {
3524
+ k: "hs",
3525
+ m: encodeHandshakeMessage(reply),
3526
+ hsc
3527
+ };
3434
3528
  }
3435
- if (message.t === "hello") return {
3436
- k: "hs",
3437
- m: encodeHandshakeMessage(await handshake.onHello(message))
3438
- };
3439
3529
  if (message.t === "prove") {
3530
+ if (request.hsc == null) return {
3531
+ k: "err",
3532
+ message: "prove missing continuation token"
3533
+ };
3534
+ const pending = await this._sealer.open(request.hsc);
3535
+ if (pending == null) return {
3536
+ k: "err",
3537
+ message: "invalid or expired continuation token"
3538
+ };
3539
+ const handshake = this._makeHandshake();
3540
+ await handshake.restorePending(pending);
3440
3541
  const reply = await handshake.onProve(message);
3441
- this._pendingHandshakes.delete(request.hsid);
3442
3542
  const result = handshake.getResult();
3443
3543
  if (reply.t === "accept" && result != null) {
3444
- const token = (0, nanoid.nanoid)();
3445
- this._sessions.set(token, {
3446
- client: new RuntimeCoordinate(result.remote),
3544
+ const ticket = {
3545
+ client: result.remote,
3447
3546
  securityLevel: result.securityLevel,
3448
- crypto: result.securityLevel === "encrypted" ? createActionFrameCrypto({
3449
- link: security.link,
3450
- linkedClientId: result.linkedClientId
3451
- }) : void 0
3452
- });
3547
+ keyMaterial: result.encryptionKeyMaterial
3548
+ };
3549
+ const t = await this._sealer.seal(ticket, this._sessionTtlMs);
3453
3550
  return {
3454
3551
  k: "hs",
3455
3552
  m: encodeHandshakeMessage(reply),
3456
- t: token
3553
+ t
3457
3554
  };
3458
3555
  }
3459
3556
  return {
@@ -3467,20 +3564,26 @@ var ExchangeAcceptor = class {
3467
3564
  };
3468
3565
  }
3469
3566
  async _handleAction(request) {
3470
- let session;
3567
+ let client;
3568
+ let crypto;
3471
3569
  let candidate;
3472
3570
  if (request.t != null) {
3473
- session = this._sessions.get(request.t);
3474
- if (session == null) return {
3571
+ const ticket = await this._sealer.open(request.t);
3572
+ if (ticket == null) return {
3475
3573
  k: "err",
3476
3574
  message: "unknown or expired session token"
3477
3575
  };
3576
+ client = new RuntimeCoordinate(ticket.client);
3577
+ crypto = ticket.securityLevel === "encrypted" && ticket.keyMaterial != null ? createActionFrameCrypto({
3578
+ link: this._security.link,
3579
+ keyMaterial: ticket.keyMaterial
3580
+ }) : void 0;
3478
3581
  if ("c" in request) {
3479
- if (session.crypto == null) return {
3582
+ if (crypto == null) return {
3480
3583
  k: "err",
3481
3584
  message: "session is not encrypted"
3482
3585
  };
3483
- const plain = await session.crypto.decryptFrame(base64ToBytes(request.c));
3586
+ const plain = await crypto.decryptFrame(base64ToBytes(request.c));
3484
3587
  candidate = JSON.parse(textDecoder.decode(plain));
3485
3588
  } else candidate = request.w;
3486
3589
  } else {
@@ -3495,11 +3598,11 @@ var ExchangeAcceptor = class {
3495
3598
  message: "malformed action wire"
3496
3599
  };
3497
3600
  const wire = candidate;
3498
- if (session != null && wire.type === "request") wire.context.originClient = session.client.toJsonObject();
3601
+ if (client != null && wire.type === "request") wire.context.originClient = client.toJsonObject();
3499
3602
  const resultWire = (await (await this._runtime.handleActionPayloadWire(wire)).waitForResultPayload()).toJsonObject();
3500
- if (session?.crypto != null) return {
3603
+ if (crypto != null && "c" in request) return {
3501
3604
  k: "act",
3502
- c: bytesToBase64(await session.crypto.encryptFrame(textEncoder.encode(JSON.stringify(resultWire))))
3605
+ c: bytesToBase64(await crypto.encryptFrame(textEncoder.encode(JSON.stringify(resultWire))))
3503
3606
  };
3504
3607
  return {
3505
3608
  k: "act",
@@ -4076,4 +4179,4 @@ Object.defineProperty(exports, "runtimeLinkId", {
4076
4179
  }
4077
4180
  });
4078
4181
 
4079
- //# sourceMappingURL=createHibernatableWsServerAdapter-BNi4k9j3.cjs.map
4182
+ //# sourceMappingURL=createHibernatableWsServerAdapter-j96U9vgo.cjs.map