@nice-code/action 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -50
- package/build/{ActionDevtoolsCore-D_JvgPmz.d.mts → ActionDevtoolsCore-baIHExfj.d.mts} +2 -2
- package/build/{ActionDevtoolsCore-dV-IVPcP.d.cts → ActionDevtoolsCore-dApyYvTS.d.cts} +2 -2
- package/build/{ActionPayload.types-CnfWlkA1.d.cts → ActionPayload.types-CQM1HRw_.d.cts} +161 -194
- package/build/{ActionPayload.types-D0DM-g65.d.mts → ActionPayload.types-DXGiw1SF.d.mts} +161 -194
- package/build/devtools/browser/index.d.cts +1 -1
- package/build/devtools/browser/index.d.mts +1 -1
- package/build/devtools/server/index.d.cts +1 -1
- package/build/devtools/server/index.d.mts +1 -1
- package/build/index.cjs +1763 -1722
- package/build/index.cjs.map +1 -1
- package/build/index.d.cts +2 -2
- package/build/index.d.mts +2 -2
- package/build/index.mjs +1764 -1721
- package/build/index.mjs.map +1 -1
- package/build/platform/cloudflare/index.d.cts +1 -1
- package/build/platform/cloudflare/index.d.mts +1 -1
- package/build/react-query/index.d.cts +1 -1
- package/build/react-query/index.d.mts +1 -1
- package/package.json +4 -4
package/build/index.mjs
CHANGED
|
@@ -1454,16 +1454,16 @@ var ActionRuntime = class {
|
|
|
1454
1454
|
return this;
|
|
1455
1455
|
}
|
|
1456
1456
|
/**
|
|
1457
|
+
* @internal Low-level primitive — the public way to open a connection is `connectChannel`, which
|
|
1458
|
+
* derives routing from a channel and binds the crypto identity for you. This stays as the raw building
|
|
1459
|
+
* block it sits on (it restates domain lists by hand) and is not part of the supported surface.
|
|
1460
|
+
*
|
|
1457
1461
|
* Declare an external "backend client" in one call: build an
|
|
1458
1462
|
* {@link ConnectorHandler} for `externalCoordinate` carrying the given
|
|
1459
1463
|
* `transports`, route the listed `domains`/`actions` to it, register it (plus any
|
|
1460
1464
|
* `localHandlers` — e.g. server→client push handlers that share the same channel)
|
|
1461
1465
|
* on this runtime, and `apply()`. Returns the external handler so the caller can
|
|
1462
1466
|
* later `clearTransportCache()` it.
|
|
1463
|
-
*
|
|
1464
|
-
* Sugar over `new ConnectorHandler(...).forDomain(...)` + `addHandlers([...])`,
|
|
1465
|
-
* so a single runtime can host one handler per backend target with its transports
|
|
1466
|
-
* declared once and reused across every action routed to that backend.
|
|
1467
1467
|
*/
|
|
1468
1468
|
connectTo(externalCoordinate, options) {
|
|
1469
1469
|
const handler = new ConnectorHandler({
|
|
@@ -2967,1926 +2967,1969 @@ function createSecureAcceptorHandler(options) {
|
|
|
2967
2967
|
});
|
|
2968
2968
|
}
|
|
2969
2969
|
//#endregion
|
|
2970
|
-
//#region src/ActionRuntime/
|
|
2970
|
+
//#region src/ActionRuntime/Transport/Carrier/Carrier.types.ts
|
|
2971
2971
|
/**
|
|
2972
|
-
*
|
|
2973
|
-
*
|
|
2974
|
-
*
|
|
2975
|
-
* accepted as a legacy alias for `toAcceptor`.)
|
|
2972
|
+
* Narrow a carrier source to the exchange shape via its `shape` discriminant — the one branch the
|
|
2973
|
+
* transport factories ({@link secureTransport}, {@link plainTransport}) use to pick the duplex vs
|
|
2974
|
+
* exchange transport. A duplex source carries no `shape`, so the `else` branch is the duplex one.
|
|
2976
2975
|
*/
|
|
2977
|
-
function
|
|
2978
|
-
return
|
|
2979
|
-
toAcceptorDomains: options.toAcceptor,
|
|
2980
|
-
toConnectorDomains: options.toConnector
|
|
2981
|
-
};
|
|
2976
|
+
function isExchangeCarrierSource(carrier) {
|
|
2977
|
+
return "shape" in carrier && carrier.shape === "exchange";
|
|
2982
2978
|
}
|
|
2979
|
+
//#endregion
|
|
2980
|
+
//#region src/ActionRuntime/Transport/Transport.ts
|
|
2983
2981
|
/**
|
|
2984
|
-
*
|
|
2985
|
-
*
|
|
2986
|
-
* `
|
|
2987
|
-
*
|
|
2988
|
-
*
|
|
2989
|
-
* fallback).
|
|
2990
|
-
*
|
|
2991
|
-
* Sugar over {@link ActionRuntime.connectTo}. Returns the acceptor handler so the caller can later
|
|
2992
|
-
* `clearTransportCache()` it.
|
|
2982
|
+
* Reusable transport definition. Devs construct these (`secureTransport({ carrier: wsCarrier(url) })`,
|
|
2983
|
+
* `plainTransport({ carrier: httpCarrier(...) })`, …) and pass them to a
|
|
2984
|
+
* `ConnectorHandler`. A single
|
|
2985
|
+
* definition can be shared across multiple handlers — each handler builds its own live
|
|
2986
|
+
* {@link TransportConnection} via {@link TransportConnection._createConnection}.
|
|
2993
2987
|
*/
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
localHandlers: pushHandlers,
|
|
3000
|
-
defaultTimeout: options.defaultTimeout
|
|
3001
|
-
});
|
|
2988
|
+
var Transport = class {};
|
|
2989
|
+
//#endregion
|
|
2990
|
+
//#region src/ActionRuntime/Transport/SecureSession/exchangeProtocol.ts
|
|
2991
|
+
function encodeExchange(envelope) {
|
|
2992
|
+
return JSON.stringify(envelope);
|
|
3002
2993
|
}
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
* `toAcceptor` domains are served together with one merged, connection-aware case map (each case gets
|
|
3006
|
-
* the primed request + the originating connection, as with
|
|
3007
|
-
* {@link AcceptorHandler.forConnectionDomainCases}). The domain list is taken from the channel,
|
|
3008
|
-
* never restated. Add the returned handler to the runtime alongside the acceptor handler:
|
|
3009
|
-
* ```ts
|
|
3010
|
-
* runtime.addHandlers([acceptChannelConnections(serverHandler, channel, { … }), serverHandler]);
|
|
3011
|
-
* ```
|
|
3012
|
-
*/
|
|
3013
|
-
function acceptChannelConnections(serverHandler, channel, cases) {
|
|
3014
|
-
return serverHandler.forConnectionDomainCasesMulti(channel.toAcceptorDomains, cases);
|
|
2994
|
+
function decodeExchangeRequest(raw) {
|
|
2995
|
+
return parse(raw);
|
|
3015
2996
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
* {@link connectChannel}. It folds in the same boilerplate as {@link createSecureAcceptorHandler} (the
|
|
3019
|
-
* `ClientCryptoKeyLink` + storage-backed TOFU resolver from one `storageAdapter`, the channel's codec +
|
|
3020
|
-
* dictionary version, the `security` block from the runtime coordinate) but takes the `(runtime, channel,
|
|
3021
|
-
* options)` shape of the channel family. Pair it with {@link acceptChannelConnections} for execution:
|
|
3022
|
-
* ```ts
|
|
3023
|
-
* const acceptor = acceptChannel(runtime, gameChannel, { clientEnv, storageAdapter, send });
|
|
3024
|
-
* runtime.addHandlers([acceptChannelConnections(acceptor, gameChannel, { … }), acceptor]);
|
|
3025
|
-
* ```
|
|
3026
|
-
*/
|
|
3027
|
-
function acceptChannel(runtime, channel, options) {
|
|
3028
|
-
return createSecureAcceptorHandler({
|
|
3029
|
-
channel,
|
|
3030
|
-
runtime,
|
|
3031
|
-
clientEnv: options.clientEnv,
|
|
3032
|
-
storageAdapter: options.storageAdapter,
|
|
3033
|
-
link: options.link,
|
|
3034
|
-
send: options.send,
|
|
3035
|
-
securityLevel: options.securityLevel,
|
|
3036
|
-
verifyKeyResolver: options.verifyKeyResolver,
|
|
3037
|
-
defaultTimeout: options.defaultTimeout
|
|
3038
|
-
});
|
|
2997
|
+
function decodeExchangeReply(raw) {
|
|
2998
|
+
return parse(raw);
|
|
3039
2999
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
*/
|
|
3046
|
-
const PayloadTypeToInt = {
|
|
3047
|
-
["request"]: 0,
|
|
3048
|
-
["result"]: 1,
|
|
3049
|
-
["progress"]: 2
|
|
3050
|
-
};
|
|
3051
|
-
const ReversePayloadType = [
|
|
3052
|
-
"request",
|
|
3053
|
-
"result",
|
|
3054
|
-
"progress"
|
|
3055
|
-
];
|
|
3056
|
-
/**
|
|
3057
|
-
* Build the positional `domain:id` ↔ integer dictionary. Both ends of a channel MUST build it from
|
|
3058
|
-
* the same domains in the same order — the mapping is positional, so a mismatch routes to the wrong
|
|
3059
|
-
* action. Add new transported domains to the end of the list.
|
|
3060
|
-
*/
|
|
3061
|
-
function buildActionRouteDictionary(domains) {
|
|
3062
|
-
const routeToInt = /* @__PURE__ */ new Map();
|
|
3063
|
-
const intToRoute = [];
|
|
3064
|
-
for (const dom of domains) for (const actionId of Object.keys(dom.actionSchema)) {
|
|
3065
|
-
const routeKey = `${dom.domain}:${actionId}`;
|
|
3066
|
-
if (routeToInt.has(routeKey)) continue;
|
|
3067
|
-
routeToInt.set(routeKey, intToRoute.length);
|
|
3068
|
-
intToRoute.push({
|
|
3069
|
-
domain: dom.domain,
|
|
3070
|
-
id: actionId,
|
|
3071
|
-
allDomains: dom.allDomains
|
|
3072
|
-
});
|
|
3000
|
+
function parse(raw) {
|
|
3001
|
+
try {
|
|
3002
|
+
return JSON.parse(raw);
|
|
3003
|
+
} catch {
|
|
3004
|
+
return;
|
|
3073
3005
|
}
|
|
3074
|
-
return {
|
|
3075
|
-
routeToInt,
|
|
3076
|
-
intToRoute
|
|
3077
|
-
};
|
|
3078
3006
|
}
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
if (json.type === "progress") return json.progress;
|
|
3007
|
+
function bytesToBase64(bytes) {
|
|
3008
|
+
let binary = "";
|
|
3009
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
3010
|
+
return btoa(binary);
|
|
3084
3011
|
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
...base,
|
|
3107
|
-
type: "result",
|
|
3108
|
-
result: payloadData,
|
|
3109
|
-
outputHash: ""
|
|
3012
|
+
function base64ToBytes(base64) {
|
|
3013
|
+
const binary = atob(base64);
|
|
3014
|
+
const bytes = new Uint8Array(binary.length);
|
|
3015
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
3016
|
+
return bytes;
|
|
3017
|
+
}
|
|
3018
|
+
//#endregion
|
|
3019
|
+
//#region src/ActionRuntime/Transport/SecureSession/establishExchangeSession.ts
|
|
3020
|
+
const textEncoder$1 = new TextEncoder();
|
|
3021
|
+
const textDecoder$1 = new TextDecoder();
|
|
3022
|
+
/** Plain path (no handshake/token): every action rides a bare `act` envelope, plaintext both ways. */
|
|
3023
|
+
function finalizePlainExchangeMethods(ctx) {
|
|
3024
|
+
return buildExchangeMethods(ctx, {});
|
|
3025
|
+
}
|
|
3026
|
+
/** Secure path: run the handshake (two exchanges) once at bring-up, then reuse the token + crypto. */
|
|
3027
|
+
async function finalizeSecureExchangeMethods(ctx) {
|
|
3028
|
+
return buildExchangeMethods(ctx, await runConnectorExchangeHandshake(ctx.carrier, ctx.secure));
|
|
3029
|
+
}
|
|
3030
|
+
function buildExchangeMethods(ctx, state) {
|
|
3031
|
+
const sendActionData = (inputs) => {
|
|
3032
|
+
runExchange(ctx.carrier, state, inputs).catch((err) => inputs.runningAction._abort(err));
|
|
3110
3033
|
};
|
|
3111
3034
|
return {
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
progress: payloadData
|
|
3035
|
+
sendActionData,
|
|
3036
|
+
updateRunConfig: ctx.updateRunConfig
|
|
3115
3037
|
};
|
|
3116
3038
|
}
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
};
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
const
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
}
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
for (const [key, entry] of map) {
|
|
3152
|
-
if (now - entry.time <= ttlMs) break;
|
|
3153
|
-
map.delete(key);
|
|
3039
|
+
async function runExchange(carrier, state, inputs) {
|
|
3040
|
+
const { action, runningAction, timeout } = inputs;
|
|
3041
|
+
const ac = new AbortController();
|
|
3042
|
+
let timedOut = false;
|
|
3043
|
+
const timeoutId = setTimeout(() => {
|
|
3044
|
+
timedOut = true;
|
|
3045
|
+
ac.abort();
|
|
3046
|
+
}, timeout);
|
|
3047
|
+
const unsubscribe = runningAction.addUpdateListeners([(update) => {
|
|
3048
|
+
if (update.type === "finished") {
|
|
3049
|
+
clearTimeout(timeoutId);
|
|
3050
|
+
ac.abort();
|
|
3051
|
+
}
|
|
3052
|
+
}]);
|
|
3053
|
+
try {
|
|
3054
|
+
const request = await buildRequestEnvelope(state, action);
|
|
3055
|
+
const replyRaw = await carrier.exchange(encodeExchange(request), { signal: ac.signal });
|
|
3056
|
+
if (action.type !== "request") return;
|
|
3057
|
+
const reply = decodeExchangeReply(asString(replyRaw));
|
|
3058
|
+
if (reply == null) throw err_nice_transport.fromId("invalid_action_response", { actionId: action.id });
|
|
3059
|
+
if (reply.k === "err") throw err_nice_transport.fromId("send_failed", {
|
|
3060
|
+
actionState: action.type,
|
|
3061
|
+
actionId: action.id,
|
|
3062
|
+
message: reply.message
|
|
3063
|
+
});
|
|
3064
|
+
const wire = await extractReplyWire(state, reply);
|
|
3065
|
+
if (wire == null || !isActionPayload_Result_JsonObject(wire)) throw err_nice_transport.fromId("invalid_action_response", { actionId: action.id });
|
|
3066
|
+
runningAction._completeWithResult(action._domain.hydrateResultPayload(wire));
|
|
3067
|
+
} catch (err) {
|
|
3068
|
+
if (timedOut) throw err_nice_transport.fromId("timeout", { timeout });
|
|
3069
|
+
throw err;
|
|
3070
|
+
} finally {
|
|
3071
|
+
clearTimeout(timeoutId);
|
|
3072
|
+
unsubscribe();
|
|
3154
3073
|
}
|
|
3155
3074
|
}
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
* gets its own correlation + identity state.
|
|
3161
|
-
*
|
|
3162
|
-
* On top of everything {@link createBinaryWireAdapter} drops, a session also drops:
|
|
3163
|
-
* - **`cuid`** — replaced by a per-connection integer correlation id. The initiator maps it to its
|
|
3164
|
-
* real cuid; the responder echoes it; each side reconstructs the cuid from its own map. Correlation
|
|
3165
|
-
* only needs to be unique per socket, so a counter suffices.
|
|
3166
|
-
* - **`originClient` after the first request** — the first request each side sends carries its
|
|
3167
|
-
* identity; the peer remembers it and injects it into later frames. Replies omit it entirely (a
|
|
3168
|
-
* reply carries the initiator's own origin, which the initiator already knows).
|
|
3169
|
-
*
|
|
3170
|
-
* Both ends MUST build the factory from the same domains in the same order (positional dictionary).
|
|
3171
|
-
* Text frames still return `undefined` from `incoming`, so JSON clients remain interoperable.
|
|
3172
|
-
*
|
|
3173
|
-
* Hibernation note: after a server connection is evicted its session resets, so a still-connected
|
|
3174
|
-
* client (whose session persists) will keep omitting `originClient`. The server must therefore restore
|
|
3175
|
-
* the connection→client binding from its own store (see `AcceptorHandler.rehydrateConnection`) and
|
|
3176
|
-
* inject `originClient` from there — the session alone can't recover it.
|
|
3177
|
-
*/
|
|
3178
|
-
function createBinaryWireSessionFactory(domains, options) {
|
|
3179
|
-
const { routeToInt, intToRoute } = buildActionRouteDictionary(domains);
|
|
3180
|
-
const unknownIdentity = RuntimeCoordinate.unknown.toJsonObject();
|
|
3181
|
-
const ttlMs = options?.correlationTtlMs ?? DEFAULT_CORRELATION_TTL_MS;
|
|
3182
|
-
return () => {
|
|
3183
|
-
let outCounter = 0;
|
|
3184
|
-
const corrToCuid = /* @__PURE__ */ new Map();
|
|
3185
|
-
const cuidToCorr = /* @__PURE__ */ new Map();
|
|
3186
|
-
let selfIdentity;
|
|
3187
|
-
let peerIdentity;
|
|
3075
|
+
async function buildRequestEnvelope(state, action) {
|
|
3076
|
+
const wire = action.toJsonObject();
|
|
3077
|
+
if (state.crypto != null) {
|
|
3078
|
+
const ciphertext = await state.crypto.encryptFrame(textEncoder$1.encode(JSON.stringify(wire)));
|
|
3188
3079
|
return {
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
const routeInt = routeToInt.get(routeKey);
|
|
3193
|
-
if (routeInt == null) throw new Error(`[binary-wire] Cannot pack unregistered action route: ${routeKey}`);
|
|
3194
|
-
const now = Date.now();
|
|
3195
|
-
pruneExpired(corrToCuid, now, ttlMs);
|
|
3196
|
-
pruneExpired(cuidToCorr, now, ttlMs);
|
|
3197
|
-
let corr;
|
|
3198
|
-
let wireIdentity;
|
|
3199
|
-
if (json.type === "request") {
|
|
3200
|
-
corr = outCounter++;
|
|
3201
|
-
corrToCuid.set(corr, {
|
|
3202
|
-
value: json.context.cuid,
|
|
3203
|
-
time: now
|
|
3204
|
-
});
|
|
3205
|
-
if (selfIdentity == null && isKnownIdentity(json.context.originClient)) {
|
|
3206
|
-
selfIdentity = json.context.originClient;
|
|
3207
|
-
wireIdentity = json.context.originClient;
|
|
3208
|
-
}
|
|
3209
|
-
} else {
|
|
3210
|
-
corr = cuidToCorr.get(json.context.cuid)?.value ?? -1;
|
|
3211
|
-
if (json.type === "result") cuidToCorr.delete(json.context.cuid);
|
|
3212
|
-
}
|
|
3213
|
-
const envelope = new Array(ENVELOPE_LENGTH$1);
|
|
3214
|
-
envelope[ENVELOPE$1.route] = routeInt;
|
|
3215
|
-
envelope[ENVELOPE$1.type] = PayloadTypeToInt[json.type];
|
|
3216
|
-
envelope[ENVELOPE$1.corr] = corr;
|
|
3217
|
-
envelope[ENVELOPE$1.time] = json.time;
|
|
3218
|
-
envelope[ENVELOPE$1.originClient] = wireIdentity;
|
|
3219
|
-
envelope[ENVELOPE$1.payload] = extractWirePayload(json);
|
|
3220
|
-
return pack(envelope);
|
|
3221
|
-
},
|
|
3222
|
-
incoming: (frame) => {
|
|
3223
|
-
let buffer;
|
|
3224
|
-
if (frame instanceof ArrayBuffer) buffer = new Uint8Array(frame);
|
|
3225
|
-
else if (frame instanceof Uint8Array) buffer = frame;
|
|
3226
|
-
else return;
|
|
3227
|
-
try {
|
|
3228
|
-
const envelope = unpack(buffer);
|
|
3229
|
-
if (!Array.isArray(envelope) || envelope.length !== ENVELOPE_LENGTH$1) return void 0;
|
|
3230
|
-
const routeMeta = intToRoute[envelope[ENVELOPE$1.route]];
|
|
3231
|
-
const payloadType = ReversePayloadType[envelope[ENVELOPE$1.type]];
|
|
3232
|
-
if (routeMeta == null || payloadType == null) return void 0;
|
|
3233
|
-
const now = Date.now();
|
|
3234
|
-
pruneExpired(corrToCuid, now, ttlMs);
|
|
3235
|
-
pruneExpired(cuidToCorr, now, ttlMs);
|
|
3236
|
-
const corr = envelope[ENVELOPE$1.corr];
|
|
3237
|
-
const time = envelope[ENVELOPE$1.time];
|
|
3238
|
-
const wireIdentity = envelope[ENVELOPE$1.originClient];
|
|
3239
|
-
let cuid;
|
|
3240
|
-
let originClient;
|
|
3241
|
-
if (payloadType === "request") {
|
|
3242
|
-
cuid = nanoid();
|
|
3243
|
-
cuidToCorr.set(cuid, {
|
|
3244
|
-
value: corr,
|
|
3245
|
-
time: now
|
|
3246
|
-
});
|
|
3247
|
-
if (isKnownIdentity(wireIdentity)) peerIdentity = wireIdentity;
|
|
3248
|
-
originClient = peerIdentity ?? unknownIdentity;
|
|
3249
|
-
} else {
|
|
3250
|
-
cuid = corrToCuid.get(corr)?.value ?? nanoid();
|
|
3251
|
-
if (payloadType === "result") corrToCuid.delete(corr);
|
|
3252
|
-
originClient = selfIdentity ?? unknownIdentity;
|
|
3253
|
-
}
|
|
3254
|
-
return assembleWireJson(routeMeta, payloadType, time, {
|
|
3255
|
-
cuid,
|
|
3256
|
-
timeCreated: time,
|
|
3257
|
-
routing: [],
|
|
3258
|
-
originClient
|
|
3259
|
-
}, envelope[ENVELOPE$1.payload]);
|
|
3260
|
-
} catch (e) {
|
|
3261
|
-
console.error("[binary-wire] Failed to unpack binary action session frame", e);
|
|
3262
|
-
return;
|
|
3263
|
-
}
|
|
3264
|
-
}
|
|
3080
|
+
k: "act",
|
|
3081
|
+
t: state.token,
|
|
3082
|
+
c: bytesToBase64(ciphertext)
|
|
3265
3083
|
};
|
|
3084
|
+
}
|
|
3085
|
+
return {
|
|
3086
|
+
k: "act",
|
|
3087
|
+
t: state.token,
|
|
3088
|
+
w: wire
|
|
3266
3089
|
};
|
|
3267
3090
|
}
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
*/
|
|
3275
|
-
function deriveDictionaryVersion(domains) {
|
|
3276
|
-
const { intToRoute } = buildActionRouteDictionary(domains);
|
|
3277
|
-
const signature = intToRoute.map((route) => `${route.domain}:${route.id}`).join(",");
|
|
3278
|
-
let hash = 2166136261;
|
|
3279
|
-
for (let i = 0; i < signature.length; i++) {
|
|
3280
|
-
hash ^= signature.charCodeAt(i);
|
|
3281
|
-
hash = Math.imul(hash, 16777619);
|
|
3091
|
+
async function extractReplyWire(state, reply) {
|
|
3092
|
+
if (reply.k !== "act") return void 0;
|
|
3093
|
+
if ("c" in reply) {
|
|
3094
|
+
if (state.crypto == null) return void 0;
|
|
3095
|
+
const plain = await state.crypto.decryptFrame(base64ToBytes(reply.c));
|
|
3096
|
+
return JSON.parse(textDecoder$1.decode(plain));
|
|
3282
3097
|
}
|
|
3283
|
-
return
|
|
3098
|
+
return reply.w;
|
|
3284
3099
|
}
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
* {@link connectChannel} and `acceptChannelConnections`) instead of being restated at each end. The
|
|
3293
|
-
* wire dictionary spans `[...toAcceptor, ...toConnector]` in that order; add new domains to the end of
|
|
3294
|
-
* their list to keep older peers compatible. (`domains` is still accepted as a legacy alias for
|
|
3295
|
-
* `toAcceptor`.)
|
|
3296
|
-
*/
|
|
3297
|
-
function defineSecureChannel(options) {
|
|
3298
|
-
const base = defineChannel({
|
|
3299
|
-
toAcceptor: options.toAcceptor,
|
|
3300
|
-
toConnector: options.toConnector
|
|
3100
|
+
async function runConnectorExchangeHandshake(carrier, secure) {
|
|
3101
|
+
await secure.link.initialize();
|
|
3102
|
+
const handshake = createClientHandshake({
|
|
3103
|
+
link: secure.link,
|
|
3104
|
+
localCoordinate: secure.localCoordinate,
|
|
3105
|
+
dictionaryVersion: secure.dictionaryVersion,
|
|
3106
|
+
securityLevel: secure.securityLevel
|
|
3301
3107
|
});
|
|
3302
|
-
const
|
|
3108
|
+
const hsid = nanoid();
|
|
3109
|
+
const hello = await handshake.createHello();
|
|
3110
|
+
const welcomeReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
|
|
3111
|
+
k: "hs",
|
|
3112
|
+
hsid,
|
|
3113
|
+
m: encodeHandshakeMessage(hello)
|
|
3114
|
+
}))));
|
|
3115
|
+
if (welcomeReply?.k !== "hs") throw new Error("[exchange-handshake] expected a welcome reply");
|
|
3116
|
+
const welcome = decodeHandshakeMessage(welcomeReply.m);
|
|
3117
|
+
if (welcome == null) throw new Error("[exchange-handshake] malformed welcome");
|
|
3118
|
+
if (welcome.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${welcome.reason}`);
|
|
3119
|
+
if (welcome.t !== "welcome") throw new Error(`[exchange-handshake] expected welcome, got ${welcome.t}`);
|
|
3120
|
+
const prove = await handshake.onWelcome(welcome);
|
|
3121
|
+
const acceptReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
|
|
3122
|
+
k: "hs",
|
|
3123
|
+
hsid,
|
|
3124
|
+
m: encodeHandshakeMessage(prove)
|
|
3125
|
+
}))));
|
|
3126
|
+
if (acceptReply?.k !== "hs") throw new Error("[exchange-handshake] expected an accept reply");
|
|
3127
|
+
const accept = decodeHandshakeMessage(acceptReply.m);
|
|
3128
|
+
if (accept == null) throw new Error("[exchange-handshake] malformed accept");
|
|
3129
|
+
if (accept.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${accept.reason}`);
|
|
3130
|
+
if (accept.t !== "accept") throw new Error(`[exchange-handshake] expected accept, got ${accept.t}`);
|
|
3131
|
+
if (acceptReply.t == null) throw new Error("[exchange-handshake] accept missing session token");
|
|
3132
|
+
const result = await handshake.onAccept(accept);
|
|
3133
|
+
const crypto = result.securityLevel === "encrypted" ? createActionFrameCrypto({
|
|
3134
|
+
link: secure.link,
|
|
3135
|
+
linkedClientId: result.linkedClientId
|
|
3136
|
+
}) : void 0;
|
|
3303
3137
|
return {
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
createCodec: createBinaryWireSessionFactory(allDomains, options.sessionOptions)
|
|
3138
|
+
token: acceptReply.t,
|
|
3139
|
+
crypto
|
|
3307
3140
|
};
|
|
3308
3141
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
return JSON.stringify(envelope);
|
|
3313
|
-
}
|
|
3314
|
-
function decodeExchangeRequest(raw) {
|
|
3315
|
-
return parse(raw);
|
|
3316
|
-
}
|
|
3317
|
-
function decodeExchangeReply(raw) {
|
|
3318
|
-
return parse(raw);
|
|
3319
|
-
}
|
|
3320
|
-
function parse(raw) {
|
|
3321
|
-
try {
|
|
3322
|
-
return JSON.parse(raw);
|
|
3323
|
-
} catch {
|
|
3324
|
-
return;
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
function bytesToBase64(bytes) {
|
|
3328
|
-
let binary = "";
|
|
3329
|
-
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
3330
|
-
return btoa(binary);
|
|
3142
|
+
function asString(frame) {
|
|
3143
|
+
if (typeof frame === "string") return frame;
|
|
3144
|
+
return textDecoder$1.decode(frame instanceof ArrayBuffer ? new Uint8Array(frame) : frame);
|
|
3331
3145
|
}
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3146
|
+
//#endregion
|
|
3147
|
+
//#region src/ActionRuntime/Transport/helpers/addTransportStatusMetadata.ts
|
|
3148
|
+
function addTransportStatusMetadata(transportStatus) {
|
|
3149
|
+
if (transportStatus.status === "ready") return {
|
|
3150
|
+
status: "ready",
|
|
3151
|
+
readyData: transportStatus.readyData
|
|
3152
|
+
};
|
|
3153
|
+
if (transportStatus.status === "initializing") return {
|
|
3154
|
+
status: "initializing",
|
|
3155
|
+
initializationPromise: transportStatus.initializationPromise,
|
|
3156
|
+
timeStarted: Date.now()
|
|
3157
|
+
};
|
|
3158
|
+
if (transportStatus.status === "failed") return {
|
|
3159
|
+
status: "failed",
|
|
3160
|
+
error: transportStatus.error,
|
|
3161
|
+
timeFailed: Date.now()
|
|
3162
|
+
};
|
|
3163
|
+
if (transportStatus.status === "unsupported") return { status: "unsupported" };
|
|
3164
|
+
return { status: "uninitialized" };
|
|
3337
3165
|
}
|
|
3338
3166
|
//#endregion
|
|
3339
|
-
//#region src/ActionRuntime/Transport/
|
|
3340
|
-
|
|
3341
|
-
const textDecoder$1 = new TextDecoder();
|
|
3167
|
+
//#region src/ActionRuntime/Transport/TransportConnection.ts
|
|
3168
|
+
let transportOrd = 0;
|
|
3342
3169
|
/**
|
|
3343
|
-
*
|
|
3344
|
-
*
|
|
3345
|
-
*
|
|
3346
|
-
* requests can't rely on channel ordering), mints a session **token** on accept, and on every later `act`
|
|
3347
|
-
* POST resolves the session by token, decrypts the body (at `encrypted`), routes it through the runtime,
|
|
3348
|
-
* and returns the (encrypted) result inline as the reply.
|
|
3349
|
-
*
|
|
3350
|
-
* Sessions and in-flight handshakes are held in memory — fine for a single-instance server. (Surviving a
|
|
3351
|
-
* Durable-Object eviction would persist each token's `keyMaterial` and re-derive the key on a miss, the
|
|
3352
|
-
* same primitive `AcceptorSecureSession.rehydrate` uses; left as a follow-up.)
|
|
3170
|
+
* Live, per-handler transport runtime built from a reusable {@link Transport} definition. Holds the
|
|
3171
|
+
* connection-scoped state (ordinal, initialized config, sockets / abort sets) that must not be shared
|
|
3172
|
+
* across handlers. Construct these via `definition._createConnection(...)`, never directly.
|
|
3353
3173
|
*/
|
|
3354
|
-
var
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
constructor(
|
|
3362
|
-
this.
|
|
3363
|
-
this.
|
|
3364
|
-
this.
|
|
3365
|
-
this._noneAllowed = this._allowedLevels.includes("none");
|
|
3174
|
+
var TransportConnection = class {
|
|
3175
|
+
def;
|
|
3176
|
+
transOrd = transportOrd++;
|
|
3177
|
+
type;
|
|
3178
|
+
initialized;
|
|
3179
|
+
/** Backref to the public definition that created this connection (used for devtools route info). */
|
|
3180
|
+
definition;
|
|
3181
|
+
constructor(def) {
|
|
3182
|
+
this.def = def;
|
|
3183
|
+
this.type = def.type;
|
|
3184
|
+
this.initialized = def.initialize();
|
|
3366
3185
|
}
|
|
3367
|
-
/**
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3186
|
+
/**
|
|
3187
|
+
* Devtools route info for an action routed through this live connection. Defaults to the stateless
|
|
3188
|
+
* {@link definition}'s info; connections override to enrich it from live state (e.g. the actual
|
|
3189
|
+
* resolved socket URL) when the definition couldn't resolve it on its own.
|
|
3190
|
+
*/
|
|
3191
|
+
getRouteInfo(input) {
|
|
3192
|
+
return this.definition?.getRouteInfo(input);
|
|
3373
3193
|
}
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3194
|
+
/**
|
|
3195
|
+
* Whether a `ready`-status transport still needs asynchronous bring-up before its methods exist —
|
|
3196
|
+
* awaiting the carrier to open and/or running a handshake. Default `false`: a stateless transport
|
|
3197
|
+
* (HTTP) is usable the instant `getTransport` reports `ready`, so it stays a terminal *synchronous*
|
|
3198
|
+
* fallback in {@link ConnectionTransportManager}. Stream carriers (Link/WS) override to `true`.
|
|
3199
|
+
*/
|
|
3200
|
+
_needsAsyncBringUp(_readyData) {
|
|
3201
|
+
return false;
|
|
3202
|
+
}
|
|
3203
|
+
/** Await the carrier becoming ready to send (e.g. a socket `open`). Default: nothing to await. */
|
|
3204
|
+
_awaitCarrierReady(_readyData) {
|
|
3205
|
+
return Promise.resolve();
|
|
3206
|
+
}
|
|
3207
|
+
/**
|
|
3208
|
+
* Finalize during async bring-up — may run a handshake, so it can be async. Defaults to the
|
|
3209
|
+
* synchronous {@link _finalizeTransportMethods}; secure stream carriers override to branch plain/secure.
|
|
3210
|
+
*/
|
|
3211
|
+
_finalizeReady(readyData) {
|
|
3212
|
+
return this._finalizeTransportMethods(readyData);
|
|
3213
|
+
}
|
|
3214
|
+
_getCacheKey(input) {
|
|
3215
|
+
const parts = this.initialized.getTransportCacheKey?.(input);
|
|
3216
|
+
if (parts == null) return null;
|
|
3217
|
+
return parts.join("\0");
|
|
3218
|
+
}
|
|
3219
|
+
getCacheKey(input) {
|
|
3220
|
+
const inner = this._getCacheKey(input);
|
|
3221
|
+
if (inner == null) return null;
|
|
3222
|
+
return `${this.transOrd}:${inner}`;
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Whether this transport can serve the given action right now. Consulted by the manager before
|
|
3226
|
+
* cache-key resolution and `getTransport`; a `false` result skips this transport (treated as
|
|
3227
|
+
* `unsupported`) and the manager falls through to the next in preference order. Defaults to `true`
|
|
3228
|
+
* when the transport declares no gate.
|
|
3229
|
+
*/
|
|
3230
|
+
isAvailable(input) {
|
|
3231
|
+
return this.initialized.isAvailable?.(input) ?? true;
|
|
3232
|
+
}
|
|
3233
|
+
getTransport(input) {
|
|
3234
|
+
return this._processTransportStatus(input);
|
|
3235
|
+
}
|
|
3236
|
+
_processTransportStatus(input) {
|
|
3237
|
+
const statusInfo = addTransportStatusMetadata(this.initialized.getTransport(input));
|
|
3238
|
+
if (statusInfo.status === "ready") {
|
|
3239
|
+
if (!this._needsAsyncBringUp(statusInfo.readyData)) return {
|
|
3240
|
+
status: "ready",
|
|
3241
|
+
readyData: this._finalizeTransportMethods(statusInfo.readyData)
|
|
3242
|
+
};
|
|
3417
3243
|
return {
|
|
3418
|
-
|
|
3419
|
-
|
|
3244
|
+
status: "initializing",
|
|
3245
|
+
timeStarted: Date.now(),
|
|
3246
|
+
initializationPromise: this._bringUp(statusInfo.readyData)
|
|
3420
3247
|
};
|
|
3421
3248
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
let session;
|
|
3429
|
-
let candidate;
|
|
3430
|
-
if (request.t != null) {
|
|
3431
|
-
session = this._sessions.get(request.t);
|
|
3432
|
-
if (session == null) return {
|
|
3433
|
-
k: "err",
|
|
3434
|
-
message: "unknown or expired session token"
|
|
3435
|
-
};
|
|
3436
|
-
if ("c" in request) {
|
|
3437
|
-
if (session.crypto == null) return {
|
|
3438
|
-
k: "err",
|
|
3439
|
-
message: "session is not encrypted"
|
|
3440
|
-
};
|
|
3441
|
-
const plain = await session.crypto.decryptFrame(base64ToBytes(request.c));
|
|
3442
|
-
candidate = JSON.parse(textDecoder$1.decode(plain));
|
|
3443
|
-
} else candidate = request.w;
|
|
3444
|
-
} else {
|
|
3445
|
-
if (!this._noneAllowed || "c" in request) return {
|
|
3446
|
-
k: "err",
|
|
3447
|
-
message: "missing session token"
|
|
3249
|
+
if (statusInfo.status === "initializing") {
|
|
3250
|
+
const initializationPromise = statusInfo.initializationPromise.then((result) => result.status === "ready" ? this._bringUp(result.readyData) : result);
|
|
3251
|
+
return {
|
|
3252
|
+
status: "initializing",
|
|
3253
|
+
timeStarted: statusInfo.timeStarted,
|
|
3254
|
+
initializationPromise
|
|
3448
3255
|
};
|
|
3449
|
-
candidate = request.w;
|
|
3450
3256
|
}
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
if (session != null && wire.type === "request") wire.context.originClient = session.client.toJsonObject();
|
|
3457
|
-
const resultWire = (await (await this._runtime.handleActionPayloadWire(wire)).waitForResultPayload()).toJsonObject();
|
|
3458
|
-
if (session?.crypto != null) return {
|
|
3459
|
-
k: "act",
|
|
3460
|
-
c: bytesToBase64(await session.crypto.encryptFrame(textEncoder$1.encode(JSON.stringify(resultWire))))
|
|
3461
|
-
};
|
|
3257
|
+
return statusInfo;
|
|
3258
|
+
}
|
|
3259
|
+
/** Await carrier readiness, then finalize (possibly running a handshake) into the live methods. */
|
|
3260
|
+
async _bringUp(readyData) {
|
|
3261
|
+
await this._awaitCarrierReady(readyData);
|
|
3462
3262
|
return {
|
|
3463
|
-
|
|
3464
|
-
|
|
3263
|
+
status: "ready",
|
|
3264
|
+
readyData: await this._finalizeReady(readyData)
|
|
3465
3265
|
};
|
|
3466
3266
|
}
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3267
|
+
};
|
|
3268
|
+
//#endregion
|
|
3269
|
+
//#region src/ActionRuntime/Transport/Exchange/ExchangeConnection.ts
|
|
3270
|
+
/**
|
|
3271
|
+
* Carrier-agnostic live connection for the exchange (request → single reply) shape — the HTTP
|
|
3272
|
+
* counterpart to {@link LinkConnection}. It owns only the bring-up (run the secure handshake on first
|
|
3273
|
+
* use); the request/reply lifecycle + crypto live in the shared `establishExchangeSession`.
|
|
3274
|
+
*/
|
|
3275
|
+
var ExchangeConnection = class extends TransportConnection {
|
|
3276
|
+
constructor(def) {
|
|
3277
|
+
super({
|
|
3278
|
+
...def,
|
|
3279
|
+
type: "exchange"
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
_getCacheKey(input) {
|
|
3283
|
+
return this.initialized.getTransportCacheKey?.(input).join("\0") ?? "";
|
|
3284
|
+
}
|
|
3285
|
+
_needsAsyncBringUp(data) {
|
|
3286
|
+
return data.secureChannel != null && data.secureChannel.securityLevel !== "none";
|
|
3287
|
+
}
|
|
3288
|
+
_finalizeReady(data) {
|
|
3289
|
+
const secure = data.secureChannel;
|
|
3290
|
+
if (secure != null && secure.securityLevel !== "none") return finalizeSecureExchangeMethods({
|
|
3291
|
+
...this._sessionContext(data),
|
|
3292
|
+
secure
|
|
3471
3293
|
});
|
|
3294
|
+
return this._finalizeTransportMethods(data);
|
|
3295
|
+
}
|
|
3296
|
+
_finalizeTransportMethods(data) {
|
|
3297
|
+
return finalizePlainExchangeMethods(this._sessionContext(data));
|
|
3298
|
+
}
|
|
3299
|
+
_sessionContext(data) {
|
|
3300
|
+
return {
|
|
3301
|
+
carrier: data.carrier,
|
|
3302
|
+
updateRunConfig: data.updateRunConfig,
|
|
3303
|
+
secure: data.secureChannel
|
|
3304
|
+
};
|
|
3472
3305
|
}
|
|
3473
3306
|
};
|
|
3474
3307
|
//#endregion
|
|
3475
|
-
//#region src/ActionRuntime/
|
|
3476
|
-
/** Permissive defaults — fine for a public action endpoint; override (or disable) via `cors`. */
|
|
3477
|
-
const DEFAULT_CORS_HEADERS = {
|
|
3478
|
-
"Access-Control-Allow-Origin": "*",
|
|
3479
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
3480
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
3481
|
-
"Access-Control-Max-Age": "86400"
|
|
3482
|
-
};
|
|
3308
|
+
//#region src/ActionRuntime/Transport/Exchange/ExchangeTransport.ts
|
|
3483
3309
|
/**
|
|
3484
|
-
*
|
|
3485
|
-
*
|
|
3486
|
-
*
|
|
3487
|
-
*
|
|
3488
|
-
*
|
|
3489
|
-
* It only touches web-standard `Request`/`Response`, so it stays transport-agnostic — the one
|
|
3490
|
-
* environment-specific bit (the WS upgrade) is injected via {@link IActionFetchHandlerOptions.onWebSocketUpgrade}:
|
|
3491
|
-
* ```ts
|
|
3492
|
-
* this.fetchHandler = createActionFetchHandler(this.runtime, {
|
|
3493
|
-
* onWebSocketUpgrade: () => {
|
|
3494
|
-
* const pair = new WebSocketPair();
|
|
3495
|
-
* this.ctx.acceptWebSocket(pair[1]);
|
|
3496
|
-
* return new Response(null, { status: 101, webSocket: pair[0] });
|
|
3497
|
-
* },
|
|
3498
|
-
* });
|
|
3499
|
-
* // async fetch(request) { return this.fetchHandler(request); }
|
|
3500
|
-
* ```
|
|
3310
|
+
* A carrier-agnostic exchange (request → single reply) transport: it drives nice-action's secure session
|
|
3311
|
+
* over any {@link IExchangeCarrier} (HTTP being the one built-in). The duplex counterpart is
|
|
3312
|
+
* {@link LinkTransport}; this is the no-push half — its reply rides the response to its own request, so it
|
|
3313
|
+
* can't deliver an unsolicited frame (the runtime never picks it for the return path).
|
|
3501
3314
|
*/
|
|
3502
|
-
|
|
3503
|
-
const corsHeaders = options.cors === false ? {} : options.cors ?? DEFAULT_CORS_HEADERS;
|
|
3504
|
-
const isActionPath = options.isActionPath ?? ((url) => url.pathname.endsWith("/action"));
|
|
3505
|
-
const isWebSocketPath = options.isWebSocketPath ?? ((url) => url.pathname.endsWith("/ws"));
|
|
3506
|
-
const exchangeAcceptor = options.security != null ? new ExchangeAcceptor({
|
|
3507
|
-
runtime,
|
|
3508
|
-
security: options.security
|
|
3509
|
-
}) : void 0;
|
|
3510
|
-
const withCors = (response) => {
|
|
3511
|
-
if (options.cors === false) return response;
|
|
3512
|
-
const headers = new Headers(response.headers);
|
|
3513
|
-
for (const [key, value] of Object.entries(corsHeaders)) headers.set(key, value);
|
|
3514
|
-
return new Response(response.body, {
|
|
3515
|
-
status: response.status,
|
|
3516
|
-
headers
|
|
3517
|
-
});
|
|
3518
|
-
};
|
|
3519
|
-
return async (request) => {
|
|
3520
|
-
if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 }));
|
|
3521
|
-
const url = new URL(request.url);
|
|
3522
|
-
const isWebSocketUpgrade = options.isWebSocketUpgrade ?? ((req, u) => req.headers.get("Upgrade") === "websocket" && isWebSocketPath(u));
|
|
3523
|
-
if (options.onWebSocketUpgrade != null && isWebSocketUpgrade(request, url)) return options.onWebSocketUpgrade(request, url);
|
|
3524
|
-
if (request.method === "POST" && isActionPath(url)) {
|
|
3525
|
-
if (exchangeAcceptor != null) {
|
|
3526
|
-
const reply = await exchangeAcceptor.handlePost(await request.text());
|
|
3527
|
-
return withCors(new Response(reply, {
|
|
3528
|
-
status: 200,
|
|
3529
|
-
headers: { "Content-Type": "application/json" }
|
|
3530
|
-
}));
|
|
3531
|
-
}
|
|
3532
|
-
return withCors((await (await runtime.handleActionPayloadWire(await request.json())).waitForResultPayload()).toHttpResponse({ useErrorStatus: options.useErrorStatus }));
|
|
3533
|
-
}
|
|
3534
|
-
return withCors(new Response("Not found", { status: 404 }));
|
|
3535
|
-
};
|
|
3536
|
-
}
|
|
3537
|
-
//#endregion
|
|
3538
|
-
//#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/ConnectionStateStore.ts
|
|
3539
|
-
/**
|
|
3540
|
-
* A typed per-connection state store that co-owns the app state and the acceptor handler's routing
|
|
3541
|
-
* binding in one attachment, so neither the consumer nor the handler has to hand-merge the two. Create
|
|
3542
|
-
* it through {@link createConnectionStateStore} (which also wires binding persistence and replays
|
|
3543
|
-
* surviving connections after a wake), then `get`/`set`/`clearApp` the app state directly.
|
|
3544
|
-
*
|
|
3545
|
-
* The mechanism is carrier-neutral — it only needs read/write/enumerate callbacks for the connection's
|
|
3546
|
-
* attachment — but it pays off on transports whose connections outlive process eviction (e.g. a
|
|
3547
|
-
* Durable Object's hibernatable WebSockets), which is why it lives beside the hibernation adapter.
|
|
3548
|
-
*
|
|
3549
|
-
* ```ts
|
|
3550
|
-
* const players = createConnectionStateStore(serverHandler, {
|
|
3551
|
-
* schema: vs_player,
|
|
3552
|
-
* read: (ws) => ws.deserializeAttachment(),
|
|
3553
|
-
* write: (ws, v) => ws.serializeAttachment(v),
|
|
3554
|
-
* getConnections: () => ctx.getWebSockets(),
|
|
3555
|
-
* });
|
|
3556
|
-
* players.set(ws, player); // binding is preserved automatically
|
|
3557
|
-
* const player = players.get(ws);
|
|
3558
|
-
* ```
|
|
3559
|
-
*/
|
|
3560
|
-
var ConnectionStateStore = class {
|
|
3315
|
+
var ExchangeTransport = class ExchangeTransport extends Transport {
|
|
3561
3316
|
options;
|
|
3317
|
+
type = "exchange";
|
|
3562
3318
|
constructor(options) {
|
|
3319
|
+
super();
|
|
3563
3320
|
this.options = options;
|
|
3564
3321
|
}
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
return this._readAttachment(connection).app ?? null;
|
|
3568
|
-
}
|
|
3569
|
-
/** Set the app state, preserving the runtime binding already pinned to the connection. */
|
|
3570
|
-
set(connection, app) {
|
|
3571
|
-
const existing = this._readAttachment(connection);
|
|
3572
|
-
this.options.write(connection, {
|
|
3573
|
-
app,
|
|
3574
|
-
binding: existing.binding
|
|
3575
|
-
});
|
|
3576
|
-
}
|
|
3577
|
-
/** Clear the app state but keep the binding (e.g. a spectator that stopped watching). */
|
|
3578
|
-
clearApp(connection) {
|
|
3579
|
-
const existing = this._readAttachment(connection);
|
|
3580
|
-
this.options.write(connection, { binding: existing.binding });
|
|
3581
|
-
}
|
|
3582
|
-
/** Every live connection paired with its (validated) app state — for rebuilding in-memory state after a wake. */
|
|
3583
|
-
entries() {
|
|
3584
|
-
return this.options.getConnections().map((connection) => [connection, this._readAttachment(connection).app ?? null]);
|
|
3585
|
-
}
|
|
3586
|
-
/** @internal Persist a freshly-bound connection's binding, preserving any app state already stored. */
|
|
3587
|
-
_persistBinding(connection, binding) {
|
|
3588
|
-
const existing = this._readAttachment(connection);
|
|
3589
|
-
this.options.write(connection, {
|
|
3590
|
-
app: existing.app,
|
|
3591
|
-
binding
|
|
3592
|
-
});
|
|
3593
|
-
}
|
|
3594
|
-
/** @internal The persisted binding for a connection, if any (used to replay routing after a wake). */
|
|
3595
|
-
_readBinding(connection) {
|
|
3596
|
-
return this._readAttachment(connection).binding;
|
|
3322
|
+
static create(options) {
|
|
3323
|
+
return new ExchangeTransport(options);
|
|
3597
3324
|
}
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
}
|
|
3325
|
+
_createConnection(_ctx) {
|
|
3326
|
+
const options = this.options;
|
|
3327
|
+
return new ExchangeConnection({ initialize: () => ({
|
|
3328
|
+
getTransportCacheKey: options.getTransportCacheKey,
|
|
3329
|
+
isAvailable: options.available,
|
|
3330
|
+
getTransport: (input) => ({
|
|
3331
|
+
status: "ready",
|
|
3332
|
+
readyData: {
|
|
3333
|
+
carrier: options.openCarrier(input),
|
|
3334
|
+
secureChannel: options.security,
|
|
3335
|
+
updateRunConfig: options.updateRunConfig
|
|
3336
|
+
}
|
|
3337
|
+
})
|
|
3338
|
+
}) });
|
|
3613
3339
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
return result.value;
|
|
3340
|
+
getRouteInfo(input) {
|
|
3341
|
+
if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
|
|
3342
|
+
return {
|
|
3343
|
+
carrierLabel: this.options.label ?? "exchange",
|
|
3344
|
+
summary: this.options.label ?? "exchange"
|
|
3345
|
+
};
|
|
3621
3346
|
}
|
|
3622
3347
|
};
|
|
3623
|
-
/**
|
|
3624
|
-
* Build a per-connection {@link ConnectionStateStore} bound to an {@link AcceptorHandler}: it registers
|
|
3625
|
-
* itself as the handler's connection-bound persistence callback (so bindings are written without
|
|
3626
|
-
* overwriting app state) and immediately replays every live connection's stored binding via
|
|
3627
|
-
* {@link AcceptorHandler.rehydrateConnection} — so on a transport that resumes after eviction (e.g. a
|
|
3628
|
-
* Durable Object waking from hibernation) both the app identity and the action routing come back from a
|
|
3629
|
-
* single attachment, with no storage reads and no hand-rolled merge.
|
|
3630
|
-
*
|
|
3631
|
-
* Lives outside the handler so the generic {@link AcceptorHandler} stays free of any attachment/
|
|
3632
|
-
* hibernation concern — it exposes only the neutral `setOnConnectionBound` + `rehydrateConnection`
|
|
3633
|
-
* hooks this builder drives.
|
|
3634
|
-
*/
|
|
3635
|
-
function createConnectionStateStore(handler, options) {
|
|
3636
|
-
const store = new ConnectionStateStore(options);
|
|
3637
|
-
handler.setOnConnectionBound((connection, binding) => store._persistBinding(connection, binding));
|
|
3638
|
-
for (const connection of options.getConnections()) {
|
|
3639
|
-
const binding = store._readBinding(connection);
|
|
3640
|
-
if (binding != null) handler.rehydrateConnection(connection, binding);
|
|
3641
|
-
}
|
|
3642
|
-
return store;
|
|
3643
|
-
}
|
|
3644
3348
|
//#endregion
|
|
3645
|
-
//#region src/ActionRuntime/
|
|
3349
|
+
//#region src/ActionRuntime/Transport/helpers/createUnsetTransportResolvers.ts
|
|
3350
|
+
const createUnsetTransportResolvers = (transportLabel) => ({ onIncomingActionDataJson: (json) => {
|
|
3351
|
+
console.warn(`Received incoming action JSON [${json.domain}:${json.id}] on Transport [${transportLabel}] but no incoming data listener has been set.`);
|
|
3352
|
+
} });
|
|
3353
|
+
//#endregion
|
|
3354
|
+
//#region src/ActionRuntime/Transport/SecureSession/establishLinkSession.ts
|
|
3355
|
+
const HANDSHAKE_TIMEOUT_MS = 15e3;
|
|
3356
|
+
/** Plain path (no handshake): route every inbound frame to the runtime; send without crypto. */
|
|
3357
|
+
function finalizePlainLinkMethods(ctx) {
|
|
3358
|
+
const disconnectListeners = [];
|
|
3359
|
+
const abortSet = /* @__PURE__ */ new Set();
|
|
3360
|
+
const pipe = makePipe(ctx, void 0);
|
|
3361
|
+
ctx.channel.attach({
|
|
3362
|
+
onMessage: (frame) => void handleIncomingActionFrame(ctx, pipe, frame),
|
|
3363
|
+
onClose: () => onChannelClosed(ctx, disconnectListeners, abortSet),
|
|
3364
|
+
onError: () => onChannelClosed(ctx, disconnectListeners, abortSet)
|
|
3365
|
+
});
|
|
3366
|
+
return buildSendMethods(ctx, pipe, disconnectListeners, abortSet);
|
|
3367
|
+
}
|
|
3646
3368
|
/**
|
|
3647
|
-
*
|
|
3648
|
-
*
|
|
3649
|
-
*
|
|
3650
|
-
* live connection's stored binding via `getAttachment`, so results/pushes still route after a wake.
|
|
3651
|
-
*
|
|
3652
|
-
* Layered on top of the generic {@link AcceptorHandler} — it touches only the handler's neutral
|
|
3653
|
-
* `setOnConnectionBound` / `rehydrateConnection` / `receive` / `dropConnection` surface, so no
|
|
3654
|
-
* hibernation concern leaks into the handler itself.
|
|
3655
|
-
*
|
|
3656
|
-
* Construct it once when the handler is built, then forward connection events:
|
|
3657
|
-
* ```ts
|
|
3658
|
-
* const duplex = createHibernatableWsServerAdapter({ handler, getConnections, getAttachment, setAttachment });
|
|
3659
|
-
* // webSocketMessage(ws, msg) => duplex.receive(ws, msg);
|
|
3660
|
-
* // webSocketClose/Error(ws) => duplex.drop(ws);
|
|
3661
|
-
* ```
|
|
3369
|
+
* Secure path: a single message handler feeds the handshake until it completes, then routes action
|
|
3370
|
+
* frames (decrypting at the `encrypted` level). Frames that race ahead of activation are buffered and
|
|
3371
|
+
* flushed once the handshake lands, so nothing is lost.
|
|
3662
3372
|
*/
|
|
3663
|
-
function
|
|
3664
|
-
const
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3373
|
+
async function finalizeSecureLinkMethods(ctx) {
|
|
3374
|
+
const disconnectListeners = [];
|
|
3375
|
+
const abortSet = /* @__PURE__ */ new Set();
|
|
3376
|
+
const session = {};
|
|
3377
|
+
let active = false;
|
|
3378
|
+
const handshakeQueue = [];
|
|
3379
|
+
const handshakeWaiters = [];
|
|
3380
|
+
const pendingActionFrames = [];
|
|
3381
|
+
ctx.channel.attach({
|
|
3382
|
+
onMessage: (frame) => {
|
|
3383
|
+
if (active && session.pipe != null) {
|
|
3384
|
+
handleIncomingActionFrame(ctx, session.pipe, frame);
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
if (typeof frame === "string") {
|
|
3388
|
+
const message = decodeHandshakeMessage(frame);
|
|
3389
|
+
if (message != null) {
|
|
3390
|
+
const waiter = handshakeWaiters.shift();
|
|
3391
|
+
if (waiter != null) waiter(message);
|
|
3392
|
+
else handshakeQueue.push(message);
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
pendingActionFrames.push(frame);
|
|
3397
|
+
},
|
|
3398
|
+
onClose: () => onChannelClosed(ctx, disconnectListeners, abortSet),
|
|
3399
|
+
onError: () => onChannelClosed(ctx, disconnectListeners, abortSet)
|
|
3400
|
+
});
|
|
3401
|
+
const nextHandshakeMessage = () => {
|
|
3402
|
+
const queued = handshakeQueue.shift();
|
|
3403
|
+
if (queued != null) return Promise.resolve(queued);
|
|
3404
|
+
return new Promise((resolve, reject) => {
|
|
3405
|
+
const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("[link-handshake] timed out waiting for peer reply")), HANDSHAKE_TIMEOUT_MS);
|
|
3406
|
+
handshakeWaiters.push((message) => {
|
|
3407
|
+
clearTimeout(timeout);
|
|
3408
|
+
resolve(message);
|
|
3409
|
+
});
|
|
3410
|
+
});
|
|
3673
3411
|
};
|
|
3412
|
+
const pipe = makePipe(ctx, await runClientHandshake(ctx.channel, ctx.secure, nextHandshakeMessage));
|
|
3413
|
+
session.pipe = pipe;
|
|
3414
|
+
active = true;
|
|
3415
|
+
for (const frame of pendingActionFrames) await handleIncomingActionFrame(ctx, pipe, frame);
|
|
3416
|
+
pendingActionFrames.length = 0;
|
|
3417
|
+
return buildSendMethods(ctx, pipe, disconnectListeners, abortSet);
|
|
3674
3418
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
"authenticated",
|
|
3681
|
-
"encrypted"
|
|
3682
|
-
];
|
|
3683
|
-
function serveChannel(runtime, channel, options) {
|
|
3684
|
-
const duplexCarriers = options.carriers.filter((carrier) => !isExchangeAcceptorCarrier(carrier));
|
|
3685
|
-
const exchangeCarriers = options.carriers.filter(isExchangeAcceptorCarrier);
|
|
3686
|
-
if (exchangeCarriers.length > 1) throw new Error("serveChannel: at most one exchange carrier is supported");
|
|
3687
|
-
const exchangeCarrier = exchangeCarriers[0];
|
|
3688
|
-
const singleDuplex = duplexCarriers.length === 1;
|
|
3689
|
-
if (options.connectionState != null && !singleDuplex) throw new Error("serveChannel: `connectionState` requires exactly one duplex carrier");
|
|
3690
|
-
if (options.channelCases != null && !singleDuplex) throw new Error("serveChannel: `channelCases` requires exactly one duplex carrier");
|
|
3691
|
-
const exchangeSecure = exchangeCarrier != null && (exchangeCarrier.secure ?? true);
|
|
3692
|
-
const anyDuplexSecure = duplexCarriers.some((carrier) => carrier.secure ?? true);
|
|
3693
|
-
const securityLevel = options.securityLevel ?? DEFAULT_SERVER_SECURITY_LEVELS;
|
|
3694
|
-
let secure;
|
|
3695
|
-
if (anyDuplexSecure || exchangeSecure) {
|
|
3696
|
-
const storage = options.storage;
|
|
3697
|
-
if (storage == null) throw new Error("serveChannel: a secure carrier requires `storage`. Pass it, or set `secure: false` on the carrier for a plain endpoint.");
|
|
3698
|
-
secure = {
|
|
3699
|
-
storage,
|
|
3700
|
-
link: options.link ?? new ClientCryptoKeyLink({ storageAdapter: storage }),
|
|
3701
|
-
verifyKeyResolver: options.verifyKeyResolver ?? createStorageTofuVerifyKeyResolver(storage)
|
|
3702
|
-
};
|
|
3703
|
-
}
|
|
3704
|
-
const plainRouter = (handler) => ({
|
|
3705
|
-
receive: (connection, frame) => handler.receive(connection, frame),
|
|
3706
|
-
drop: (connection) => handler.dropConnection(connection)
|
|
3419
|
+
function makePipe(ctx, crypto) {
|
|
3420
|
+
return createFrameCryptoPipe({
|
|
3421
|
+
write: (frame) => ctx.channel.send(frame),
|
|
3422
|
+
isOpen: () => ctx.channel.isOpen(),
|
|
3423
|
+
crypto
|
|
3707
3424
|
});
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
const handler = (carrier.secure ?? true) && secure != null ? acceptChannel(runtime, channel, {
|
|
3713
|
-
clientEnv: options.clientEnv,
|
|
3714
|
-
storageAdapter: secure.storage,
|
|
3715
|
-
link: secure.link,
|
|
3716
|
-
verifyKeyResolver: secure.verifyKeyResolver,
|
|
3717
|
-
securityLevel,
|
|
3718
|
-
send: carrier.send,
|
|
3719
|
-
defaultTimeout: options.defaultTimeout
|
|
3720
|
-
}) : createAcceptorHandler({
|
|
3721
|
-
clientEnv: options.clientEnv,
|
|
3722
|
-
createFormatMessage: channel.createCodec,
|
|
3723
|
-
send: carrier.send,
|
|
3724
|
-
runtime,
|
|
3725
|
-
defaultTimeout: options.defaultTimeout
|
|
3726
|
-
});
|
|
3727
|
-
const attach = carrier.attachmentStore;
|
|
3728
|
-
let router;
|
|
3729
|
-
if (attach == null) router = plainRouter(handler);
|
|
3730
|
-
else if (options.connectionState != null) {
|
|
3731
|
-
connections = createConnectionStateStore(handler, {
|
|
3732
|
-
schema: options.connectionState.schema,
|
|
3733
|
-
getConnections: attach.getConnections,
|
|
3734
|
-
read: attach.read,
|
|
3735
|
-
write: attach.write
|
|
3736
|
-
});
|
|
3737
|
-
router = plainRouter(handler);
|
|
3738
|
-
} else router = createHibernatableWsServerAdapter({
|
|
3739
|
-
handler,
|
|
3740
|
-
getConnections: attach.getConnections,
|
|
3741
|
-
getAttachment: (connection) => attach.read(connection)?.binding,
|
|
3742
|
-
setAttachment: (connection, binding) => attach.write(connection, {
|
|
3743
|
-
...asObject(attach.read(connection)),
|
|
3744
|
-
binding
|
|
3745
|
-
})
|
|
3746
|
-
});
|
|
3747
|
-
carrier._activate(router);
|
|
3748
|
-
handlers.push(handler);
|
|
3749
|
-
}
|
|
3750
|
-
runtime.addHandlers([...options.handlers ?? [], ...handlers]);
|
|
3751
|
-
if (options.channelCases != null) runtime.addHandlers([acceptChannelConnections(handlers[0], channel, options.channelCases)]);
|
|
3752
|
-
const exchangeSecurity = exchangeSecure && secure != null ? {
|
|
3425
|
+
}
|
|
3426
|
+
async function runClientHandshake(channel, secure, nextHandshakeMessage) {
|
|
3427
|
+
await secure.link.initialize();
|
|
3428
|
+
const handshake = createClientHandshake({
|
|
3753
3429
|
link: secure.link,
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
securityLevel
|
|
3758
|
-
} : void 0;
|
|
3759
|
-
const defaultIsUpgrade = (request) => request.headers.get("Upgrade") === "websocket";
|
|
3760
|
-
const upgraders = [];
|
|
3761
|
-
for (const carrier of duplexCarriers) {
|
|
3762
|
-
if (carrier.upgrade == null) continue;
|
|
3763
|
-
upgraders.push({
|
|
3764
|
-
isUpgrade: carrier.isUpgrade ?? defaultIsUpgrade,
|
|
3765
|
-
upgrade: carrier.upgrade
|
|
3766
|
-
});
|
|
3767
|
-
}
|
|
3768
|
-
const fetch = createActionFetchHandler(runtime, {
|
|
3769
|
-
cors: exchangeCarrier?.cors,
|
|
3770
|
-
onWebSocketUpgrade: upgraders.length === 0 ? void 0 : (request, url) => (upgraders.find((u) => u.isUpgrade(request, url)) ?? upgraders[0]).upgrade(request, url),
|
|
3771
|
-
isWebSocketUpgrade: upgraders.length === 0 ? void 0 : (request, url) => upgraders.some((u) => u.isUpgrade(request, url)),
|
|
3772
|
-
isActionPath: exchangeCarrier != null ? exchangeCarrier.isActionPath ?? (() => true) : () => false,
|
|
3773
|
-
security: exchangeSecurity,
|
|
3774
|
-
useErrorStatus: exchangeCarrier?.useErrorStatus
|
|
3430
|
+
localCoordinate: secure.localCoordinate,
|
|
3431
|
+
dictionaryVersion: secure.dictionaryVersion,
|
|
3432
|
+
securityLevel: secure.securityLevel
|
|
3775
3433
|
});
|
|
3776
|
-
|
|
3777
|
-
const
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
};
|
|
3789
|
-
return {
|
|
3790
|
-
handlers,
|
|
3791
|
-
fetch,
|
|
3792
|
-
duplex,
|
|
3793
|
-
pushToClient,
|
|
3794
|
-
broadcast,
|
|
3795
|
-
connections
|
|
3796
|
-
};
|
|
3434
|
+
channel.send(encodeHandshakeMessage(await handshake.createHello()));
|
|
3435
|
+
const welcome = await nextHandshakeMessage();
|
|
3436
|
+
if (welcome.t === "reject") throw new Error(`[link-handshake] rejected by peer: ${welcome.reason}`);
|
|
3437
|
+
if (welcome.t !== "welcome") throw new Error(`[link-handshake] expected welcome, got ${welcome.t}`);
|
|
3438
|
+
channel.send(encodeHandshakeMessage(await handshake.onWelcome(welcome)));
|
|
3439
|
+
const accept = await nextHandshakeMessage();
|
|
3440
|
+
if (accept.t === "reject") throw new Error(`[link-handshake] rejected by peer: ${accept.reason}`);
|
|
3441
|
+
if (accept.t !== "accept") throw new Error(`[link-handshake] expected accept, got ${accept.t}`);
|
|
3442
|
+
const result = await handshake.onAccept(accept);
|
|
3443
|
+
return result.securityLevel === "encrypted" ? createActionFrameCrypto({
|
|
3444
|
+
link: secure.link,
|
|
3445
|
+
linkedClientId: result.linkedClientId
|
|
3446
|
+
}) : void 0;
|
|
3797
3447
|
}
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
serverClose?.();
|
|
3820
|
-
});
|
|
3448
|
+
function buildSendMethods(ctx, pipe, disconnectListeners, abortSet) {
|
|
3449
|
+
const channel = ctx.channel;
|
|
3450
|
+
const sendActionData = (inputs) => {
|
|
3451
|
+
const { action, runningAction, timeout } = inputs;
|
|
3452
|
+
if (!channel.isOpen()) {
|
|
3453
|
+
if (action.type === "request") runningAction._abort(ctx.makeDisconnectError(action.id));
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
if (action.type === "request") {
|
|
3457
|
+
abortSet.add(runningAction);
|
|
3458
|
+
const timeoutId = setTimeout(() => {
|
|
3459
|
+
runningAction._abort(err_nice_transport.fromId("timeout", { timeout }));
|
|
3460
|
+
}, timeout);
|
|
3461
|
+
runningAction.addUpdateListeners([(update) => {
|
|
3462
|
+
if (update.type === "finished") {
|
|
3463
|
+
clearTimeout(timeoutId);
|
|
3464
|
+
abortSet.delete(runningAction);
|
|
3465
|
+
}
|
|
3466
|
+
}]);
|
|
3467
|
+
}
|
|
3468
|
+
pipe.send(ctx.formatMessage?.outgoing(inputs) ?? JSON.stringify(inputs.action.toJsonObject()));
|
|
3821
3469
|
};
|
|
3822
3470
|
return {
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
if (!open) return;
|
|
3828
|
-
queueMicrotask(() => serverMessage?.(frame));
|
|
3829
|
-
},
|
|
3830
|
-
attach: ({ onMessage, onClose }) => {
|
|
3831
|
-
clientMessage = onMessage;
|
|
3832
|
-
clientClose = onClose;
|
|
3833
|
-
},
|
|
3834
|
-
close: closeBoth,
|
|
3835
|
-
label: "in-memory"
|
|
3471
|
+
sendActionData,
|
|
3472
|
+
updateRunConfig: ctx.updateRunConfig,
|
|
3473
|
+
addOnDisconnectListener: (cb) => {
|
|
3474
|
+
disconnectListeners.push(cb);
|
|
3836
3475
|
},
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
close: closeBoth
|
|
3476
|
+
disconnect: () => {
|
|
3477
|
+
try {
|
|
3478
|
+
channel.close();
|
|
3479
|
+
} catch {}
|
|
3480
|
+
},
|
|
3481
|
+
sendReturnData: (payload, clients) => {
|
|
3482
|
+
const formatted = clients != null ? ctx.formatMessage?.outgoing({
|
|
3483
|
+
action: payload,
|
|
3484
|
+
...clients
|
|
3485
|
+
}) : void 0;
|
|
3486
|
+
pipe.send(formatted ?? JSON.stringify(payload.toJsonObject()));
|
|
3849
3487
|
}
|
|
3850
3488
|
};
|
|
3851
3489
|
}
|
|
3490
|
+
async function handleIncomingActionFrame(ctx, pipe, frame) {
|
|
3491
|
+
const decoded = await pipe.decryptIncoming(frame);
|
|
3492
|
+
if (decoded === void 0) return;
|
|
3493
|
+
const rawJson = decodeActionFrame(decoded, ctx.formatMessage);
|
|
3494
|
+
if (rawJson != null) ctx.resolvers.onIncomingActionDataJson(rawJson);
|
|
3495
|
+
}
|
|
3496
|
+
function onChannelClosed(ctx, disconnectListeners, abortSet) {
|
|
3497
|
+
for (const cb of disconnectListeners) cb();
|
|
3498
|
+
const error = ctx.makeDisconnectError("—");
|
|
3499
|
+
for (const ra of [...abortSet]) ra._abort(error);
|
|
3500
|
+
}
|
|
3852
3501
|
//#endregion
|
|
3853
|
-
//#region src/ActionRuntime/Transport/
|
|
3502
|
+
//#region src/ActionRuntime/Transport/Link/LinkConnection.ts
|
|
3503
|
+
/** Abort error for a closed link channel (carrier-neutral — the carrier itself isn't named). */
|
|
3504
|
+
function linkDisconnectError(actionId) {
|
|
3505
|
+
return err_nice_transport.fromId("send_failed", {
|
|
3506
|
+
actionId,
|
|
3507
|
+
actionState: "request",
|
|
3508
|
+
message: "link channel disconnected"
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3854
3511
|
/**
|
|
3855
|
-
*
|
|
3856
|
-
*
|
|
3857
|
-
*
|
|
3858
|
-
*
|
|
3512
|
+
* Carrier-agnostic live connection. It owns only the *bring-up* (open the carrier, then run the secure
|
|
3513
|
+
* session); the session itself — handshake, frame crypto, codec, send/receive — lives in the shared
|
|
3514
|
+
* {@link finalizeSecureLinkMethods}/{@link finalizePlainLinkMethods}, so a WebSocket, a WebRTC data
|
|
3515
|
+
* channel, a Bluetooth characteristic, and an in-memory pipe all run the identical secure layer.
|
|
3859
3516
|
*/
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3517
|
+
var LinkConnection = class extends TransportConnection {
|
|
3518
|
+
resolvers;
|
|
3519
|
+
constructor(def, resolvers) {
|
|
3520
|
+
super({
|
|
3521
|
+
...def,
|
|
3522
|
+
type: "duplex"
|
|
3523
|
+
});
|
|
3524
|
+
this.resolvers = resolvers ?? createUnsetTransportResolvers("link");
|
|
3525
|
+
}
|
|
3526
|
+
_getCacheKey(input) {
|
|
3527
|
+
return this.initialized.getTransportCacheKey?.(input).join("\0") ?? "";
|
|
3528
|
+
}
|
|
3529
|
+
_needsAsyncBringUp() {
|
|
3530
|
+
return true;
|
|
3531
|
+
}
|
|
3532
|
+
_awaitCarrierReady(data) {
|
|
3533
|
+
return data.channel.ready;
|
|
3534
|
+
}
|
|
3535
|
+
_finalizeReady(data) {
|
|
3536
|
+
const secure = data.secureChannel;
|
|
3537
|
+
if (secure != null && secure.securityLevel !== "none") return finalizeSecureLinkMethods({
|
|
3538
|
+
...this._sessionContext(data),
|
|
3539
|
+
secure
|
|
3540
|
+
});
|
|
3541
|
+
return this._finalizeTransportMethods(data);
|
|
3542
|
+
}
|
|
3543
|
+
_sessionContext(data) {
|
|
3544
|
+
return {
|
|
3545
|
+
channel: data.channel,
|
|
3546
|
+
resolvers: this.resolvers,
|
|
3547
|
+
formatMessage: data.formatMessage,
|
|
3548
|
+
updateRunConfig: data.updateRunConfig,
|
|
3549
|
+
makeDisconnectError: linkDisconnectError
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
_finalizeTransportMethods(data) {
|
|
3553
|
+
return finalizePlainLinkMethods(this._sessionContext(data));
|
|
3554
|
+
}
|
|
3555
|
+
};
|
|
3871
3556
|
//#endregion
|
|
3872
|
-
//#region src/ActionRuntime/Transport/
|
|
3557
|
+
//#region src/ActionRuntime/Transport/Link/LinkTransport.ts
|
|
3873
3558
|
/**
|
|
3874
|
-
*
|
|
3875
|
-
*
|
|
3876
|
-
*
|
|
3877
|
-
*
|
|
3878
|
-
* The data channel must already be created (its negotiation/signaling is the app's concern); this only
|
|
3879
|
-
* drives bytes over it. Binary frames are requested as `ArrayBuffer` so the binary session codec unpacks
|
|
3880
|
-
* them synchronously; a `Blob` (if the channel hands one back) is normalized to a buffer.
|
|
3559
|
+
* A carrier-agnostic transport: it drives nice-action's secure session + action routing over any
|
|
3560
|
+
* {@link IDuplexCarrier}. The WebSocket transport is the special case that opens a `WebSocket`;
|
|
3561
|
+
* this opens whatever `openChannel` returns, so the identical secure layer works over WebRTC, Bluetooth,
|
|
3562
|
+
* or an in-memory pipe. Reported with an overridable carrier label in the devtools (defaults to "link").
|
|
3881
3563
|
*/
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
}
|
|
3892
|
-
|
|
3893
|
-
const
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3564
|
+
var LinkTransport = class LinkTransport extends Transport {
|
|
3565
|
+
options;
|
|
3566
|
+
type = "duplex";
|
|
3567
|
+
constructor(options) {
|
|
3568
|
+
super();
|
|
3569
|
+
this.options = options;
|
|
3570
|
+
}
|
|
3571
|
+
static create(options) {
|
|
3572
|
+
return new LinkTransport(options);
|
|
3573
|
+
}
|
|
3574
|
+
_createConnection(ctx) {
|
|
3575
|
+
const options = this.options;
|
|
3576
|
+
return new LinkConnection({ initialize: () => ({
|
|
3577
|
+
getTransportCacheKey: options.getTransportCacheKey,
|
|
3578
|
+
isAvailable: options.available,
|
|
3579
|
+
getTransport: (input) => ({
|
|
3580
|
+
status: "ready",
|
|
3581
|
+
readyData: {
|
|
3582
|
+
channel: options.openChannel(input),
|
|
3583
|
+
formatMessage: options.createFormatMessage?.() ?? options.formatMessage,
|
|
3584
|
+
updateRunConfig: options.updateRunConfig,
|
|
3585
|
+
secureChannel: options.security
|
|
3586
|
+
}
|
|
3587
|
+
})
|
|
3588
|
+
}) }, ctx.resolvers);
|
|
3589
|
+
}
|
|
3590
|
+
getRouteInfo(input) {
|
|
3591
|
+
if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
|
|
3592
|
+
return {
|
|
3593
|
+
carrierLabel: this.options.label ?? "link",
|
|
3594
|
+
summary: this.options.label ?? "link"
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
//#endregion
|
|
3599
|
+
//#region src/ActionRuntime/Transport/plainTransport.ts
|
|
3600
|
+
function plainTransport(options) {
|
|
3601
|
+
const carrier = options.carrier;
|
|
3602
|
+
if (isExchangeCarrierSource(carrier)) return ExchangeTransport.create({
|
|
3603
|
+
openCarrier: carrier.open,
|
|
3604
|
+
getTransportCacheKey: carrier.getCacheKey,
|
|
3605
|
+
available: options.available,
|
|
3606
|
+
getRouteInfo: carrier.getRouteInfo,
|
|
3607
|
+
label: options.label ?? carrier.carrierLabel,
|
|
3608
|
+
updateRunConfig: options.updateRunConfig
|
|
3899
3609
|
});
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3610
|
+
return LinkTransport.create({
|
|
3611
|
+
openChannel: carrier.open,
|
|
3612
|
+
formatMessage: options.formatMessage,
|
|
3613
|
+
createFormatMessage: options.createFormatMessage,
|
|
3614
|
+
getTransportCacheKey: carrier.getCacheKey,
|
|
3615
|
+
available: options.available,
|
|
3616
|
+
getRouteInfo: carrier.getRouteInfo,
|
|
3617
|
+
label: options.label ?? carrier.carrierLabel,
|
|
3618
|
+
updateRunConfig: options.updateRunConfig
|
|
3903
3619
|
});
|
|
3904
|
-
return {
|
|
3905
|
-
ready: new Promise((resolve, reject) => {
|
|
3906
|
-
if (dc.readyState === "open") {
|
|
3907
|
-
resolve();
|
|
3908
|
-
return;
|
|
3909
|
-
}
|
|
3910
|
-
dc.addEventListener("open", () => resolve(), { once: true });
|
|
3911
|
-
dc.addEventListener("error", (event) => reject(event), { once: true });
|
|
3912
|
-
dc.addEventListener("close", () => reject(/* @__PURE__ */ new Error("RTCDataChannel closed before open")), { once: true });
|
|
3913
|
-
}),
|
|
3914
|
-
isOpen: () => dc.readyState === "open",
|
|
3915
|
-
send: (frame) => {
|
|
3916
|
-
if (typeof frame === "string" || frame instanceof ArrayBuffer) dc.send(frame);
|
|
3917
|
-
else dc.send(new Uint8Array(frame));
|
|
3918
|
-
},
|
|
3919
|
-
attach: (handlers) => {
|
|
3920
|
-
onMessage = handlers.onMessage;
|
|
3921
|
-
onClose = handlers.onClose;
|
|
3922
|
-
for (const frame of preAttach) handlers.onMessage(frame);
|
|
3923
|
-
preAttach.length = 0;
|
|
3924
|
-
},
|
|
3925
|
-
close: () => {
|
|
3926
|
-
intentional = true;
|
|
3927
|
-
try {
|
|
3928
|
-
dc.close();
|
|
3929
|
-
} catch {}
|
|
3930
|
-
},
|
|
3931
|
-
get label() {
|
|
3932
|
-
return dc.label != null && dc.label !== "" ? dc.label : void 0;
|
|
3933
|
-
}
|
|
3934
|
-
};
|
|
3935
3620
|
}
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3621
|
+
//#endregion
|
|
3622
|
+
//#region src/ActionRuntime/Transport/secureTransport.ts
|
|
3623
|
+
function secureTransport(options) {
|
|
3624
|
+
const link = options.link ?? (options.storageAdapter != null ? new ClientCryptoKeyLink({ storageAdapter: options.storageAdapter }) : void 0);
|
|
3625
|
+
if (link == null) throw new Error("secureTransport: provide `link` or `storageAdapter` for the crypto identity.");
|
|
3626
|
+
const security = {
|
|
3627
|
+
securityLevel: options.securityLevel,
|
|
3628
|
+
link,
|
|
3629
|
+
localCoordinate: options.runtime.coordinate.toJsonObject(),
|
|
3630
|
+
dictionaryVersion: options.channel.dictionaryVersion
|
|
3631
|
+
};
|
|
3632
|
+
const carrier = options.carrier;
|
|
3633
|
+
if (isExchangeCarrierSource(carrier)) return ExchangeTransport.create({
|
|
3634
|
+
openCarrier: carrier.open,
|
|
3635
|
+
getTransportCacheKey: carrier.getCacheKey,
|
|
3636
|
+
available: options.available,
|
|
3637
|
+
getRouteInfo: carrier.getRouteInfo,
|
|
3638
|
+
label: carrier.carrierLabel,
|
|
3639
|
+
security
|
|
3640
|
+
});
|
|
3641
|
+
return LinkTransport.create({
|
|
3642
|
+
openChannel: carrier.open,
|
|
3643
|
+
createFormatMessage: options.channel.createCodec,
|
|
3644
|
+
getTransportCacheKey: carrier.getCacheKey,
|
|
3645
|
+
available: options.available,
|
|
3646
|
+
getRouteInfo: carrier.getRouteInfo,
|
|
3647
|
+
label: carrier.carrierLabel,
|
|
3648
|
+
security
|
|
3649
|
+
});
|
|
3939
3650
|
}
|
|
3940
3651
|
//#endregion
|
|
3941
|
-
//#region src/ActionRuntime/
|
|
3652
|
+
//#region src/ActionRuntime/Channel/ActionChannel.ts
|
|
3942
3653
|
/**
|
|
3943
|
-
*
|
|
3944
|
-
*
|
|
3945
|
-
*
|
|
3654
|
+
* Declare a transport-agnostic channel by role. Use it for HTTP, custom transports, or as the routing
|
|
3655
|
+
* half of a richer channel. The order of each list is part of the contract for wire formats that pack
|
|
3656
|
+
* positionally (see `defineSecureChannel`) — add new domains to the end of their list. (`domains` is
|
|
3657
|
+
* accepted as a legacy alias for `toAcceptor`.)
|
|
3946
3658
|
*/
|
|
3947
|
-
function
|
|
3659
|
+
function defineChannel(options) {
|
|
3948
3660
|
return {
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
getCacheKey: options.getTransportCacheKey ?? (() => ["webrtc"]),
|
|
3952
|
-
getRouteInfo: options.getRouteInfo
|
|
3661
|
+
toAcceptorDomains: options.toAcceptor,
|
|
3662
|
+
toConnectorDomains: options.toConnector
|
|
3953
3663
|
};
|
|
3954
3664
|
}
|
|
3955
|
-
//#endregion
|
|
3956
|
-
//#region src/ActionRuntime/Transport/Carrier/duplex/ws/err_nice_transport_ws.ts
|
|
3957
|
-
let EErrId_NiceTransport_WebSocket = /* @__PURE__ */ function(EErrId_NiceTransport_WebSocket) {
|
|
3958
|
-
EErrId_NiceTransport_WebSocket["ws_disconnected"] = "ws_disconnected";
|
|
3959
|
-
EErrId_NiceTransport_WebSocket["ws_create_failed"] = "ws_create_failed";
|
|
3960
|
-
EErrId_NiceTransport_WebSocket["ws_error"] = "ws_error";
|
|
3961
|
-
return EErrId_NiceTransport_WebSocket;
|
|
3962
|
-
}({});
|
|
3963
|
-
const err_nice_transport_ws = err_nice_transport.createChildDomain({
|
|
3964
|
-
domain: "ws_transport",
|
|
3965
|
-
schema: {
|
|
3966
|
-
["ws_disconnected"]: err({ message: () => `WebSocket transport disconnected.` }),
|
|
3967
|
-
["ws_create_failed"]: err({ message: ({ originalError }) => `Failed to create WebSocket transport.${originalError ? ` Original error: ${originalError.message}` : ""}` }),
|
|
3968
|
-
["ws_error"]: err({ message: ({ originalError }) => `WebSocket transport error.${originalError ? ` Original error: ${originalError.message}` : ""}` })
|
|
3969
|
-
}
|
|
3970
|
-
});
|
|
3971
|
-
//#endregion
|
|
3972
|
-
//#region src/ActionRuntime/Transport/Carrier/duplex/ws/ws_util.ts
|
|
3973
3665
|
/**
|
|
3974
|
-
*
|
|
3975
|
-
*
|
|
3976
|
-
*
|
|
3977
|
-
*
|
|
3666
|
+
* Open a connection to a peer from a single call — the dial-out dual of `serveChannel`. The channel is
|
|
3667
|
+
* the single source of truth for *what* is routed (`toAcceptor` domains forwarded to the peer,
|
|
3668
|
+
* `toConnector` pushes handled locally from `onPush`); the call binds the shared facts — the channel's
|
|
3669
|
+
* codec/dictionary version, the runtime, and one crypto identity (a {@link ClientCryptoKeyLink} over
|
|
3670
|
+
* `storage`) — into every transport in `transports`, so none of them restate the channel or runtime.
|
|
3671
|
+
* List several transports to make the path transport-agnostic (secure WS preferred, HTTP fallback):
|
|
3672
|
+
* ```ts
|
|
3673
|
+
* const handler = connectChannel(runtime, lobbyChannel, {
|
|
3674
|
+
* peer: runtime_coordinate_lobby_do,
|
|
3675
|
+
* storage,
|
|
3676
|
+
* transports: [{ carrier: wsCarrier(url) }, { carrier: httpCarrier(...), secure: false }],
|
|
3677
|
+
* onPush: { player_joined: (p) => { … } },
|
|
3678
|
+
* });
|
|
3679
|
+
* ```
|
|
3680
|
+
* Returns the {@link ConnectorHandler} so the caller can later `clearTransportCache()` it.
|
|
3978
3681
|
*/
|
|
3979
|
-
function
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3682
|
+
function connectChannel(runtime, channel, options) {
|
|
3683
|
+
const securityLevel = options.securityLevel ?? "authenticated";
|
|
3684
|
+
const anySecure = options.transports.some((transport) => transport.secure ?? true);
|
|
3685
|
+
let link = options.link;
|
|
3686
|
+
if (anySecure && link == null) {
|
|
3687
|
+
if (options.storage == null) throw new Error("connectChannel: a secure transport requires `storage` (or `link`). Pass it, or set `secure: false` on the transport for a plain connection.");
|
|
3688
|
+
link = new ClientCryptoKeyLink({ storageAdapter: options.storage });
|
|
3689
|
+
}
|
|
3690
|
+
const transports = options.transports.map((transport) => {
|
|
3691
|
+
const carrier = transport.carrier;
|
|
3692
|
+
const secure = transport.secure ?? true;
|
|
3693
|
+
if (isExchangeCarrierSource(carrier)) return secure ? secureTransport({
|
|
3694
|
+
channel,
|
|
3695
|
+
runtime,
|
|
3696
|
+
link,
|
|
3697
|
+
securityLevel: transport.securityLevel ?? securityLevel,
|
|
3698
|
+
available: transport.available,
|
|
3699
|
+
carrier
|
|
3700
|
+
}) : plainTransport({
|
|
3701
|
+
carrier,
|
|
3702
|
+
available: transport.available,
|
|
3703
|
+
label: transport.label
|
|
3704
|
+
});
|
|
3705
|
+
return secure ? secureTransport({
|
|
3706
|
+
channel,
|
|
3707
|
+
runtime,
|
|
3708
|
+
link,
|
|
3709
|
+
securityLevel: transport.securityLevel ?? securityLevel,
|
|
3710
|
+
available: transport.available,
|
|
3711
|
+
carrier
|
|
3712
|
+
}) : plainTransport({
|
|
3713
|
+
carrier,
|
|
3714
|
+
createFormatMessage: channel.createCodec,
|
|
3715
|
+
available: transport.available,
|
|
3716
|
+
label: transport.label
|
|
3717
|
+
});
|
|
3718
|
+
});
|
|
3719
|
+
const pushHandlers = options.onPush != null ? channel.toConnectorDomains.map((domain) => domain.wrapAsPartialLocalHandler(options.onPush)) : [];
|
|
3720
|
+
return runtime.connectTo(options.peer, {
|
|
3721
|
+
transports,
|
|
3722
|
+
domains: [...channel.toAcceptorDomains],
|
|
3723
|
+
localHandlers: pushHandlers,
|
|
3724
|
+
defaultTimeout: options.defaultTimeout
|
|
3725
|
+
});
|
|
3985
3726
|
}
|
|
3986
|
-
/**
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3727
|
+
/**
|
|
3728
|
+
* Register an acceptor handler's execution for a channel straight from its definition: the channel's
|
|
3729
|
+
* `toAcceptor` domains are served together with one merged, connection-aware case map (each case gets
|
|
3730
|
+
* the primed request + the originating connection, as with
|
|
3731
|
+
* {@link AcceptorHandler.forConnectionDomainCases}). The domain list is taken from the channel,
|
|
3732
|
+
* never restated. Add the returned handler to the runtime alongside the acceptor handler:
|
|
3733
|
+
* ```ts
|
|
3734
|
+
* runtime.addHandlers([acceptChannelConnections(serverHandler, channel, { … }), serverHandler]);
|
|
3735
|
+
* ```
|
|
3736
|
+
*/
|
|
3737
|
+
function acceptChannelConnections(serverHandler, channel, cases) {
|
|
3738
|
+
return serverHandler.forConnectionDomainCasesMulti(channel.toAcceptorDomains, cases);
|
|
3994
3739
|
}
|
|
3995
|
-
//#endregion
|
|
3996
|
-
//#region src/ActionRuntime/Transport/Carrier/duplex/ws/webSocketByteChannel.ts
|
|
3997
3740
|
/**
|
|
3998
|
-
*
|
|
3999
|
-
*
|
|
4000
|
-
*
|
|
4001
|
-
*
|
|
4002
|
-
*
|
|
3741
|
+
* Build the secure {@link AcceptorHandler} for a channel — the accept-in counterpart to
|
|
3742
|
+
* {@link connectChannel}. It folds in the same boilerplate as {@link createSecureAcceptorHandler} (the
|
|
3743
|
+
* `ClientCryptoKeyLink` + storage-backed TOFU resolver from one `storageAdapter`, the channel's codec +
|
|
3744
|
+
* dictionary version, the `security` block from the runtime coordinate) but takes the `(runtime, channel,
|
|
3745
|
+
* options)` shape of the channel family. Pair it with {@link acceptChannelConnections} for execution:
|
|
3746
|
+
* ```ts
|
|
3747
|
+
* const acceptor = acceptChannel(runtime, gameChannel, { clientEnv, storageAdapter, send });
|
|
3748
|
+
* runtime.addHandlers([acceptChannelConnections(acceptor, gameChannel, { … }), acceptor]);
|
|
3749
|
+
* ```
|
|
4003
3750
|
*/
|
|
4004
|
-
function
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
if (frame !== void 0) deliver(frame);
|
|
4016
|
-
});
|
|
4017
|
-
ws.addEventListener("close", (event) => {
|
|
4018
|
-
if (!intentional) console.error("WebSocket closed:", event);
|
|
4019
|
-
onClose?.();
|
|
4020
|
-
});
|
|
4021
|
-
ws.addEventListener("error", (event) => {
|
|
4022
|
-
console.error("WebSocket error:", event);
|
|
4023
|
-
onClose?.();
|
|
3751
|
+
function acceptChannel(runtime, channel, options) {
|
|
3752
|
+
return createSecureAcceptorHandler({
|
|
3753
|
+
channel,
|
|
3754
|
+
runtime,
|
|
3755
|
+
clientEnv: options.clientEnv,
|
|
3756
|
+
storageAdapter: options.storageAdapter,
|
|
3757
|
+
link: options.link,
|
|
3758
|
+
send: options.send,
|
|
3759
|
+
securityLevel: options.securityLevel,
|
|
3760
|
+
verifyKeyResolver: options.verifyKeyResolver,
|
|
3761
|
+
defaultTimeout: options.defaultTimeout
|
|
4024
3762
|
});
|
|
4025
|
-
return {
|
|
4026
|
-
ready: new Promise((resolve, reject) => {
|
|
4027
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
4028
|
-
resolve();
|
|
4029
|
-
return;
|
|
4030
|
-
}
|
|
4031
|
-
ws.addEventListener("open", () => resolve(), { once: true });
|
|
4032
|
-
ws.addEventListener("error", (event) => reject(event), { once: true });
|
|
4033
|
-
ws.addEventListener("close", (event) => reject(/* @__PURE__ */ new Error(`WebSocket closed before open: code=${event.code}`)), { once: true });
|
|
4034
|
-
}),
|
|
4035
|
-
isOpen: () => ws.readyState === WebSocket.OPEN,
|
|
4036
|
-
send: (frame) => sendFrame(ws, frame),
|
|
4037
|
-
attach: (handlers) => {
|
|
4038
|
-
onMessage = handlers.onMessage;
|
|
4039
|
-
onClose = handlers.onClose;
|
|
4040
|
-
for (const frame of preAttach) handlers.onMessage(frame);
|
|
4041
|
-
preAttach.length = 0;
|
|
4042
|
-
},
|
|
4043
|
-
close: () => {
|
|
4044
|
-
intentional = true;
|
|
4045
|
-
try {
|
|
4046
|
-
ws.close();
|
|
4047
|
-
} catch {}
|
|
4048
|
-
},
|
|
4049
|
-
get label() {
|
|
4050
|
-
return ws.url != null && ws.url !== "" ? ws.url : void 0;
|
|
4051
|
-
}
|
|
4052
|
-
};
|
|
4053
|
-
}
|
|
4054
|
-
/** Accept text + binary frames (ArrayBuffer / Uint8Array / Blob); Blobs are converted to a buffer. */
|
|
4055
|
-
async function normalizeFrame(data) {
|
|
4056
|
-
if (typeof data === "string" || data instanceof ArrayBuffer || data instanceof Uint8Array) return data;
|
|
4057
|
-
if (typeof Blob !== "undefined" && data instanceof Blob) return await data.arrayBuffer();
|
|
4058
3763
|
}
|
|
4059
3764
|
//#endregion
|
|
4060
|
-
//#region src/ActionRuntime/Transport/
|
|
3765
|
+
//#region src/ActionRuntime/Transport/codec/actionWireCodec.ts
|
|
4061
3766
|
/**
|
|
4062
|
-
*
|
|
4063
|
-
*
|
|
4064
|
-
* now "just another carrier" under the shared secure session, with no WS-specific transport class.
|
|
3767
|
+
* Tiny integer codes for the payload type, so the verbose `"request"`/`"result"`/`"progress"`
|
|
3768
|
+
* strings never hit the wire. The index in {@link ReversePayloadType} must line up with the value.
|
|
4065
3769
|
*/
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
};
|
|
4077
|
-
}
|
|
4078
|
-
function defaultWebSocket(url) {
|
|
4079
|
-
const ws = new WebSocket(url);
|
|
4080
|
-
ws.binaryType = "arraybuffer";
|
|
4081
|
-
return ws;
|
|
4082
|
-
}
|
|
4083
|
-
//#endregion
|
|
4084
|
-
//#region src/ActionRuntime/Transport/Carrier/exchange/http/httpAcceptorCarrier.ts
|
|
3770
|
+
const PayloadTypeToInt = {
|
|
3771
|
+
["request"]: 0,
|
|
3772
|
+
["result"]: 1,
|
|
3773
|
+
["progress"]: 2
|
|
3774
|
+
};
|
|
3775
|
+
const ReversePayloadType = [
|
|
3776
|
+
"request",
|
|
3777
|
+
"result",
|
|
3778
|
+
"progress"
|
|
3779
|
+
];
|
|
4085
3780
|
/**
|
|
4086
|
-
*
|
|
4087
|
-
*
|
|
4088
|
-
*
|
|
4089
|
-
* levels are all supplied centrally by `serveChannel`, so this only needs to say which requests carry an
|
|
4090
|
-
* action envelope and how to answer CORS.
|
|
3781
|
+
* Build the positional `domain:id` ↔ integer dictionary. Both ends of a channel MUST build it from
|
|
3782
|
+
* the same domains in the same order — the mapping is positional, so a mismatch routes to the wrong
|
|
3783
|
+
* action. Add new transported domains to the end of the list.
|
|
4091
3784
|
*/
|
|
4092
|
-
function
|
|
3785
|
+
function buildActionRouteDictionary(domains) {
|
|
3786
|
+
const routeToInt = /* @__PURE__ */ new Map();
|
|
3787
|
+
const intToRoute = [];
|
|
3788
|
+
for (const dom of domains) for (const actionId of Object.keys(dom.actionSchema)) {
|
|
3789
|
+
const routeKey = `${dom.domain}:${actionId}`;
|
|
3790
|
+
if (routeToInt.has(routeKey)) continue;
|
|
3791
|
+
routeToInt.set(routeKey, intToRoute.length);
|
|
3792
|
+
intToRoute.push({
|
|
3793
|
+
domain: dom.domain,
|
|
3794
|
+
id: actionId,
|
|
3795
|
+
allDomains: dom.allDomains
|
|
3796
|
+
});
|
|
3797
|
+
}
|
|
4093
3798
|
return {
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
secure: options.secure,
|
|
4097
|
-
isActionPath: options.isActionPath,
|
|
4098
|
-
cors: options.cors,
|
|
4099
|
-
useErrorStatus: options.useErrorStatus
|
|
3799
|
+
routeToInt,
|
|
3800
|
+
intToRoute
|
|
4100
3801
|
};
|
|
4101
3802
|
}
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
} catch {
|
|
4108
|
-
return url;
|
|
4109
|
-
}
|
|
3803
|
+
/** Pull the type-specific payload (`input` / `result` / `progress`) out of a wire JSON object. */
|
|
3804
|
+
function extractWirePayload(json) {
|
|
3805
|
+
if (json.type === "request") return json.input;
|
|
3806
|
+
if (json.type === "result") return json.result;
|
|
3807
|
+
if (json.type === "progress") return json.progress;
|
|
4110
3808
|
}
|
|
4111
3809
|
/**
|
|
4112
|
-
*
|
|
4113
|
-
*
|
|
4114
|
-
*
|
|
4115
|
-
* the request/reply correlation provided for free by the HTTP transaction.
|
|
4116
|
-
*
|
|
4117
|
-
* `createRequest` derives the URL/headers per action (keep it simple with `() => ({ url })`). The body is
|
|
4118
|
-
* the session's responsibility, so it is never built here.
|
|
3810
|
+
* Reassemble a full wire JSON object from its decoded parts. `inputHash`/`outputHash` are emitted
|
|
3811
|
+
* empty — the hydration constructors recompute them — and the result still satisfies
|
|
3812
|
+
* `isActionPayload_Any_JsonObject` so it flows through validation like a JSON frame.
|
|
4119
3813
|
*/
|
|
4120
|
-
function
|
|
4121
|
-
const
|
|
3814
|
+
function assembleWireJson(routeMeta, payloadType, time, context, payloadData) {
|
|
3815
|
+
const base = {
|
|
3816
|
+
form: "data",
|
|
3817
|
+
domain: routeMeta.domain,
|
|
3818
|
+
id: routeMeta.id,
|
|
3819
|
+
allDomains: routeMeta.allDomains,
|
|
3820
|
+
time,
|
|
3821
|
+
context
|
|
3822
|
+
};
|
|
3823
|
+
if (payloadType === "request") return {
|
|
3824
|
+
...base,
|
|
3825
|
+
type: "request",
|
|
3826
|
+
input: payloadData,
|
|
3827
|
+
inputHash: ""
|
|
3828
|
+
};
|
|
3829
|
+
if (payloadType === "result") return {
|
|
3830
|
+
...base,
|
|
3831
|
+
type: "result",
|
|
3832
|
+
result: payloadData,
|
|
3833
|
+
outputHash: ""
|
|
3834
|
+
};
|
|
4122
3835
|
return {
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
const request = createRequest(input);
|
|
4127
|
-
return {
|
|
4128
|
-
label: request.url,
|
|
4129
|
-
exchange: async (frame, opts) => {
|
|
4130
|
-
return await (await doFetch(request.url, {
|
|
4131
|
-
method: "POST",
|
|
4132
|
-
headers: {
|
|
4133
|
-
"Content-Type": "application/json",
|
|
4134
|
-
...request.headers
|
|
4135
|
-
},
|
|
4136
|
-
body: typeof frame === "string" ? frame : new Uint8Array(frame),
|
|
4137
|
-
signal: opts?.signal
|
|
4138
|
-
})).text();
|
|
4139
|
-
}
|
|
4140
|
-
};
|
|
4141
|
-
},
|
|
4142
|
-
getCacheKey: options.getTransportCacheKey ?? ((input) => [createRequest(input).url]),
|
|
4143
|
-
getRouteInfo: options.getRouteInfo ?? ((input) => {
|
|
4144
|
-
const { url } = createRequest(input);
|
|
4145
|
-
return {
|
|
4146
|
-
carrierLabel: "http",
|
|
4147
|
-
method: "POST",
|
|
4148
|
-
url,
|
|
4149
|
-
summary: `POST ${shortPath(url)}`
|
|
4150
|
-
};
|
|
4151
|
-
})
|
|
3836
|
+
...base,
|
|
3837
|
+
type: "progress",
|
|
3838
|
+
progress: payloadData
|
|
4152
3839
|
};
|
|
4153
3840
|
}
|
|
4154
3841
|
//#endregion
|
|
4155
|
-
//#region src/ActionRuntime/Transport/codec/
|
|
3842
|
+
//#region src/ActionRuntime/Transport/codec/createBinaryWireSessionFactory.ts
|
|
4156
3843
|
/**
|
|
4157
|
-
* Positional layout of the
|
|
4158
|
-
*
|
|
4159
|
-
*
|
|
4160
|
-
* routing).
|
|
4161
|
-
*
|
|
4162
|
-
* [ routeInt, typeInt, time, cuid, originClient, payloadData ]
|
|
3844
|
+
* Positional layout of the *session* binary envelope — the leanest frame. Compared to the stateless
|
|
3845
|
+
* adapter it replaces the 21-char `cuid` with a small per-connection integer and only carries
|
|
3846
|
+
* `originClient` on the very first request of each direction (the peer remembers it afterwards).
|
|
4163
3847
|
*
|
|
4164
|
-
*
|
|
4165
|
-
* `context.timeCreated` (reconstructed from `time`) and `context.routing` (rebuilt empty — the
|
|
4166
|
-
* receiver re-stamps its own route items as it handles the action). For the leanest possible frames
|
|
4167
|
-
* (integer correlation, identity dropped after a handshake), use `createBinaryWireSessionFactory`.
|
|
3848
|
+
* [ routeInt, typeInt, corrId, time, originClient?, payloadData ]
|
|
4168
3849
|
*/
|
|
4169
|
-
const ENVELOPE = {
|
|
3850
|
+
const ENVELOPE$1 = {
|
|
4170
3851
|
route: 0,
|
|
4171
3852
|
type: 1,
|
|
4172
|
-
|
|
4173
|
-
|
|
3853
|
+
corr: 2,
|
|
3854
|
+
time: 3,
|
|
4174
3855
|
originClient: 4,
|
|
4175
3856
|
payload: 5
|
|
4176
3857
|
};
|
|
4177
|
-
const ENVELOPE_LENGTH = 6;
|
|
3858
|
+
const ENVELOPE_LENGTH$1 = 6;
|
|
4178
3859
|
/**
|
|
4179
|
-
*
|
|
4180
|
-
*
|
|
4181
|
-
*
|
|
4182
|
-
*
|
|
4183
|
-
* (
|
|
3860
|
+
* How long a pending correlation entry is kept before it's swept. A correlation only matters until its
|
|
3861
|
+
* action resolves or times out, so anything older than the longest realistic action timeout can be
|
|
3862
|
+
* dropped — this bounds memory when requests time out or a connection dies mid-flight (their replies
|
|
3863
|
+
* would never arrive, leaving the entry orphaned). Generous default so live correlations are never
|
|
3864
|
+
* pruned (the default transport timeout is 10s).
|
|
3865
|
+
*/
|
|
3866
|
+
const DEFAULT_CORRELATION_TTL_MS = 5 * 6e4;
|
|
3867
|
+
function isKnownIdentity(coordinate) {
|
|
3868
|
+
return coordinate != null && coordinate.envId !== "_unset_";
|
|
3869
|
+
}
|
|
3870
|
+
/**
|
|
3871
|
+
* Drop entries older than `ttlMs`. Maps keep insertion order and entries are inserted in time order,
|
|
3872
|
+
* so the oldest are first — stop sweeping at the first live entry.
|
|
3873
|
+
*/
|
|
3874
|
+
function pruneExpired(map, now, ttlMs) {
|
|
3875
|
+
for (const [key, entry] of map) {
|
|
3876
|
+
if (now - entry.time <= ttlMs) break;
|
|
3877
|
+
map.delete(key);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
/**
|
|
3881
|
+
* Builds a factory of *stateful, per-connection* codecs for {@link LinkTransport} /
|
|
3882
|
+
* `AcceptorHandler` — the maximally compact binary wire. Call the returned factory once per live
|
|
3883
|
+
* connection (each socket on the client, each accepted connection on the server) so every channel
|
|
3884
|
+
* gets its own correlation + identity state.
|
|
4184
3885
|
*
|
|
4185
|
-
*
|
|
4186
|
-
*
|
|
4187
|
-
*
|
|
3886
|
+
* On top of everything {@link createBinaryWireAdapter} drops, a session also drops:
|
|
3887
|
+
* - **`cuid`** — replaced by a per-connection integer correlation id. The initiator maps it to its
|
|
3888
|
+
* real cuid; the responder echoes it; each side reconstructs the cuid from its own map. Correlation
|
|
3889
|
+
* only needs to be unique per socket, so a counter suffices.
|
|
3890
|
+
* - **`originClient` after the first request** — the first request each side sends carries its
|
|
3891
|
+
* identity; the peer remembers it and injects it into later frames. Replies omit it entirely (a
|
|
3892
|
+
* reply carries the initiator's own origin, which the initiator already knows).
|
|
4188
3893
|
*
|
|
4189
|
-
* Both ends
|
|
4190
|
-
*
|
|
3894
|
+
* Both ends MUST build the factory from the same domains in the same order (positional dictionary).
|
|
3895
|
+
* Text frames still return `undefined` from `incoming`, so JSON clients remain interoperable.
|
|
4191
3896
|
*
|
|
4192
|
-
*
|
|
4193
|
-
*
|
|
3897
|
+
* Hibernation note: after a server connection is evicted its session resets, so a still-connected
|
|
3898
|
+
* client (whose session persists) will keep omitting `originClient`. The server must therefore restore
|
|
3899
|
+
* the connection→client binding from its own store (see `AcceptorHandler.rehydrateConnection`) and
|
|
3900
|
+
* inject `originClient` from there — the session alone can't recover it.
|
|
4194
3901
|
*/
|
|
4195
|
-
function
|
|
3902
|
+
function createBinaryWireSessionFactory(domains, options) {
|
|
4196
3903
|
const { routeToInt, intToRoute } = buildActionRouteDictionary(domains);
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
3904
|
+
const unknownIdentity = RuntimeCoordinate.unknown.toJsonObject();
|
|
3905
|
+
const ttlMs = options?.correlationTtlMs ?? DEFAULT_CORRELATION_TTL_MS;
|
|
3906
|
+
return () => {
|
|
3907
|
+
let outCounter = 0;
|
|
3908
|
+
const corrToCuid = /* @__PURE__ */ new Map();
|
|
3909
|
+
const cuidToCorr = /* @__PURE__ */ new Map();
|
|
3910
|
+
let selfIdentity;
|
|
3911
|
+
let peerIdentity;
|
|
3912
|
+
return {
|
|
3913
|
+
outgoing: (input) => {
|
|
3914
|
+
const json = input.action.toJsonObject();
|
|
3915
|
+
const routeKey = `${json.domain}:${json.id}`;
|
|
3916
|
+
const routeInt = routeToInt.get(routeKey);
|
|
3917
|
+
if (routeInt == null) throw new Error(`[binary-wire] Cannot pack unregistered action route: ${routeKey}`);
|
|
3918
|
+
const now = Date.now();
|
|
3919
|
+
pruneExpired(corrToCuid, now, ttlMs);
|
|
3920
|
+
pruneExpired(cuidToCorr, now, ttlMs);
|
|
3921
|
+
let corr;
|
|
3922
|
+
let wireIdentity;
|
|
3923
|
+
if (json.type === "request") {
|
|
3924
|
+
corr = outCounter++;
|
|
3925
|
+
corrToCuid.set(corr, {
|
|
3926
|
+
value: json.context.cuid,
|
|
3927
|
+
time: now
|
|
3928
|
+
});
|
|
3929
|
+
if (selfIdentity == null && isKnownIdentity(json.context.originClient)) {
|
|
3930
|
+
selfIdentity = json.context.originClient;
|
|
3931
|
+
wireIdentity = json.context.originClient;
|
|
3932
|
+
}
|
|
3933
|
+
} else {
|
|
3934
|
+
corr = cuidToCorr.get(json.context.cuid)?.value ?? -1;
|
|
3935
|
+
if (json.type === "result") cuidToCorr.delete(json.context.cuid);
|
|
3936
|
+
}
|
|
3937
|
+
const envelope = new Array(ENVELOPE_LENGTH$1);
|
|
3938
|
+
envelope[ENVELOPE$1.route] = routeInt;
|
|
3939
|
+
envelope[ENVELOPE$1.type] = PayloadTypeToInt[json.type];
|
|
3940
|
+
envelope[ENVELOPE$1.corr] = corr;
|
|
3941
|
+
envelope[ENVELOPE$1.time] = json.time;
|
|
3942
|
+
envelope[ENVELOPE$1.originClient] = wireIdentity;
|
|
3943
|
+
envelope[ENVELOPE$1.payload] = extractWirePayload(json);
|
|
3944
|
+
return pack(envelope);
|
|
3945
|
+
},
|
|
3946
|
+
incoming: (frame) => {
|
|
3947
|
+
let buffer;
|
|
3948
|
+
if (frame instanceof ArrayBuffer) buffer = new Uint8Array(frame);
|
|
3949
|
+
else if (frame instanceof Uint8Array) buffer = frame;
|
|
3950
|
+
else return;
|
|
3951
|
+
try {
|
|
3952
|
+
const envelope = unpack(buffer);
|
|
3953
|
+
if (!Array.isArray(envelope) || envelope.length !== ENVELOPE_LENGTH$1) return void 0;
|
|
3954
|
+
const routeMeta = intToRoute[envelope[ENVELOPE$1.route]];
|
|
3955
|
+
const payloadType = ReversePayloadType[envelope[ENVELOPE$1.type]];
|
|
3956
|
+
if (routeMeta == null || payloadType == null) return void 0;
|
|
3957
|
+
const now = Date.now();
|
|
3958
|
+
pruneExpired(corrToCuid, now, ttlMs);
|
|
3959
|
+
pruneExpired(cuidToCorr, now, ttlMs);
|
|
3960
|
+
const corr = envelope[ENVELOPE$1.corr];
|
|
3961
|
+
const time = envelope[ENVELOPE$1.time];
|
|
3962
|
+
const wireIdentity = envelope[ENVELOPE$1.originClient];
|
|
3963
|
+
let cuid;
|
|
3964
|
+
let originClient;
|
|
3965
|
+
if (payloadType === "request") {
|
|
3966
|
+
cuid = nanoid();
|
|
3967
|
+
cuidToCorr.set(cuid, {
|
|
3968
|
+
value: corr,
|
|
3969
|
+
time: now
|
|
3970
|
+
});
|
|
3971
|
+
if (isKnownIdentity(wireIdentity)) peerIdentity = wireIdentity;
|
|
3972
|
+
originClient = peerIdentity ?? unknownIdentity;
|
|
3973
|
+
} else {
|
|
3974
|
+
cuid = corrToCuid.get(corr)?.value ?? nanoid();
|
|
3975
|
+
if (payloadType === "result") corrToCuid.delete(corr);
|
|
3976
|
+
originClient = selfIdentity ?? unknownIdentity;
|
|
3977
|
+
}
|
|
3978
|
+
return assembleWireJson(routeMeta, payloadType, time, {
|
|
3979
|
+
cuid,
|
|
3980
|
+
timeCreated: time,
|
|
3981
|
+
routing: [],
|
|
3982
|
+
originClient
|
|
3983
|
+
}, envelope[ENVELOPE$1.payload]);
|
|
3984
|
+
} catch (e) {
|
|
3985
|
+
console.error("[binary-wire] Failed to unpack binary action session frame", e);
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
4233
3988
|
}
|
|
4234
|
-
}
|
|
3989
|
+
};
|
|
4235
3990
|
};
|
|
4236
3991
|
}
|
|
4237
3992
|
//#endregion
|
|
4238
|
-
//#region src/ActionRuntime/
|
|
3993
|
+
//#region src/ActionRuntime/Channel/secureChannel.ts
|
|
4239
3994
|
/**
|
|
4240
|
-
*
|
|
4241
|
-
*
|
|
4242
|
-
*
|
|
4243
|
-
* definition can be shared across multiple handlers — each handler builds its own live
|
|
4244
|
-
* {@link TransportConnection} via {@link TransportConnection._createConnection}.
|
|
3995
|
+
* Derive a stable wire-dictionary version from the ordered route list (FNV-1a over `domain:id,…`), so
|
|
3996
|
+
* the version moves automatically whenever the transported domains change — a stale peer is then
|
|
3997
|
+
* rejected by the handshake instead of silently misrouting a positionally-packed frame.
|
|
4245
3998
|
*/
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
}
|
|
4255
|
-
/** Secure path: run the handshake (two exchanges) once at bring-up, then reuse the token + crypto. */
|
|
4256
|
-
async function finalizeSecureExchangeMethods(ctx) {
|
|
4257
|
-
return buildExchangeMethods(ctx, await runConnectorExchangeHandshake(ctx.carrier, ctx.secure));
|
|
3999
|
+
function deriveDictionaryVersion(domains) {
|
|
4000
|
+
const { intToRoute } = buildActionRouteDictionary(domains);
|
|
4001
|
+
const signature = intToRoute.map((route) => `${route.domain}:${route.id}`).join(",");
|
|
4002
|
+
let hash = 2166136261;
|
|
4003
|
+
for (let i = 0; i < signature.length; i++) {
|
|
4004
|
+
hash ^= signature.charCodeAt(i);
|
|
4005
|
+
hash = Math.imul(hash, 16777619);
|
|
4006
|
+
}
|
|
4007
|
+
return `auto:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
4258
4008
|
}
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4009
|
+
/**
|
|
4010
|
+
* Bundle a secure channel's shared identity from its transported domains. Both ends MUST call this
|
|
4011
|
+
* with the same domains in the same order (the binary wire dictionary is positional). The
|
|
4012
|
+
* `dictionaryVersion` is derived from those domains unless you pin an explicit one.
|
|
4013
|
+
*
|
|
4014
|
+
* Declare the domains *by role* — `toAcceptor` (connector→acceptor requests) and `toConnector`
|
|
4015
|
+
* (acceptor→connector pushes) — so the routing for both ends is derived from the channel (see
|
|
4016
|
+
* {@link connectChannel} and `acceptChannelConnections`) instead of being restated at each end. The
|
|
4017
|
+
* wire dictionary spans `[...toAcceptor, ...toConnector]` in that order; add new domains to the end of
|
|
4018
|
+
* their list to keep older peers compatible. (`domains` is still accepted as a legacy alias for
|
|
4019
|
+
* `toAcceptor`.)
|
|
4020
|
+
*/
|
|
4021
|
+
function defineSecureChannel(options) {
|
|
4022
|
+
const base = defineChannel({
|
|
4023
|
+
toAcceptor: options.toAcceptor,
|
|
4024
|
+
toConnector: options.toConnector
|
|
4025
|
+
});
|
|
4026
|
+
const allDomains = [...base.toAcceptorDomains, ...base.toConnectorDomains];
|
|
4263
4027
|
return {
|
|
4264
|
-
|
|
4265
|
-
|
|
4028
|
+
...base,
|
|
4029
|
+
dictionaryVersion: options.dictionaryVersion ?? deriveDictionaryVersion(allDomains),
|
|
4030
|
+
createCodec: createBinaryWireSessionFactory(allDomains, options.sessionOptions)
|
|
4266
4031
|
};
|
|
4267
4032
|
}
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
} catch (err) {
|
|
4297
|
-
if (timedOut) throw err_nice_transport.fromId("timeout", { timeout });
|
|
4298
|
-
throw err;
|
|
4299
|
-
} finally {
|
|
4300
|
-
clearTimeout(timeoutId);
|
|
4301
|
-
unsubscribe();
|
|
4033
|
+
//#endregion
|
|
4034
|
+
//#region src/ActionRuntime/Transport/SecureSession/exchangeAcceptor.ts
|
|
4035
|
+
const textEncoder = new TextEncoder();
|
|
4036
|
+
const textDecoder = new TextDecoder();
|
|
4037
|
+
/**
|
|
4038
|
+
* Acceptor (accept-in) side of the secure exchange protocol — the HTTP counterpart to
|
|
4039
|
+
* {@link AcceptorSecureSession}. Each POST body is one {@link decodeExchangeRequest} envelope; the
|
|
4040
|
+
* acceptor drives the server handshake over the two `hs` POSTs (correlated by `hsid`, since stateless
|
|
4041
|
+
* requests can't rely on channel ordering), mints a session **token** on accept, and on every later `act`
|
|
4042
|
+
* POST resolves the session by token, decrypts the body (at `encrypted`), routes it through the runtime,
|
|
4043
|
+
* and returns the (encrypted) result inline as the reply.
|
|
4044
|
+
*
|
|
4045
|
+
* Sessions and in-flight handshakes are held in memory — fine for a single-instance server. (Surviving a
|
|
4046
|
+
* Durable-Object eviction would persist each token's `keyMaterial` and re-derive the key on a miss, the
|
|
4047
|
+
* same primitive `AcceptorSecureSession.rehydrate` uses; left as a follow-up.)
|
|
4048
|
+
*/
|
|
4049
|
+
var ExchangeAcceptor = class {
|
|
4050
|
+
_security;
|
|
4051
|
+
_runtime;
|
|
4052
|
+
_allowedLevels;
|
|
4053
|
+
_noneAllowed;
|
|
4054
|
+
_pendingHandshakes = /* @__PURE__ */ new Map();
|
|
4055
|
+
_sessions = /* @__PURE__ */ new Map();
|
|
4056
|
+
constructor(config) {
|
|
4057
|
+
this._security = config.security;
|
|
4058
|
+
this._runtime = config.runtime;
|
|
4059
|
+
this._allowedLevels = Array.isArray(config.security.securityLevel) ? config.security.securityLevel : [config.security.securityLevel];
|
|
4060
|
+
this._noneAllowed = this._allowedLevels.includes("none");
|
|
4302
4061
|
}
|
|
4303
|
-
|
|
4304
|
-
async
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4062
|
+
/** Process one POST body (an exchange envelope), returning the reply body to send back. */
|
|
4063
|
+
async handlePost(body) {
|
|
4064
|
+
const request = decodeExchangeRequest(body);
|
|
4065
|
+
if (request == null) return this._err("malformed exchange request");
|
|
4066
|
+
if (request.k === "hs") return encodeExchange(await this._handleHandshake(request));
|
|
4067
|
+
return encodeExchange(await this._handleAction(request));
|
|
4068
|
+
}
|
|
4069
|
+
async _handleHandshake(request) {
|
|
4070
|
+
const message = decodeHandshakeMessage(request.m);
|
|
4071
|
+
if (message == null) return {
|
|
4072
|
+
k: "err",
|
|
4073
|
+
message: "malformed handshake message"
|
|
4074
|
+
};
|
|
4075
|
+
const security = this._security;
|
|
4076
|
+
await security.link.initialize();
|
|
4077
|
+
let handshake = this._pendingHandshakes.get(request.hsid);
|
|
4078
|
+
if (handshake == null) {
|
|
4079
|
+
handshake = createServerHandshake({
|
|
4080
|
+
link: security.link,
|
|
4081
|
+
localCoordinate: security.localCoordinate,
|
|
4082
|
+
dictionaryVersion: security.dictionaryVersion,
|
|
4083
|
+
securityLevel: security.securityLevel,
|
|
4084
|
+
verifyKeyResolver: security.verifyKeyResolver
|
|
4085
|
+
});
|
|
4086
|
+
this._pendingHandshakes.set(request.hsid, handshake);
|
|
4087
|
+
}
|
|
4088
|
+
if (message.t === "hello") return {
|
|
4089
|
+
k: "hs",
|
|
4090
|
+
m: encodeHandshakeMessage(await handshake.onHello(message))
|
|
4091
|
+
};
|
|
4092
|
+
if (message.t === "prove") {
|
|
4093
|
+
const reply = await handshake.onProve(message);
|
|
4094
|
+
this._pendingHandshakes.delete(request.hsid);
|
|
4095
|
+
const result = handshake.getResult();
|
|
4096
|
+
if (reply.t === "accept" && result != null) {
|
|
4097
|
+
const token = nanoid();
|
|
4098
|
+
this._sessions.set(token, {
|
|
4099
|
+
client: new RuntimeCoordinate(result.remote),
|
|
4100
|
+
securityLevel: result.securityLevel,
|
|
4101
|
+
crypto: result.securityLevel === "encrypted" ? createActionFrameCrypto({
|
|
4102
|
+
link: security.link,
|
|
4103
|
+
linkedClientId: result.linkedClientId
|
|
4104
|
+
}) : void 0
|
|
4105
|
+
});
|
|
4106
|
+
return {
|
|
4107
|
+
k: "hs",
|
|
4108
|
+
m: encodeHandshakeMessage(reply),
|
|
4109
|
+
t: token
|
|
4110
|
+
};
|
|
4111
|
+
}
|
|
4112
|
+
return {
|
|
4113
|
+
k: "hs",
|
|
4114
|
+
m: encodeHandshakeMessage(reply)
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4308
4117
|
return {
|
|
4118
|
+
k: "err",
|
|
4119
|
+
message: `unexpected handshake message ${message.t}`
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
async _handleAction(request) {
|
|
4123
|
+
let session;
|
|
4124
|
+
let candidate;
|
|
4125
|
+
if (request.t != null) {
|
|
4126
|
+
session = this._sessions.get(request.t);
|
|
4127
|
+
if (session == null) return {
|
|
4128
|
+
k: "err",
|
|
4129
|
+
message: "unknown or expired session token"
|
|
4130
|
+
};
|
|
4131
|
+
if ("c" in request) {
|
|
4132
|
+
if (session.crypto == null) return {
|
|
4133
|
+
k: "err",
|
|
4134
|
+
message: "session is not encrypted"
|
|
4135
|
+
};
|
|
4136
|
+
const plain = await session.crypto.decryptFrame(base64ToBytes(request.c));
|
|
4137
|
+
candidate = JSON.parse(textDecoder.decode(plain));
|
|
4138
|
+
} else candidate = request.w;
|
|
4139
|
+
} else {
|
|
4140
|
+
if (!this._noneAllowed || "c" in request) return {
|
|
4141
|
+
k: "err",
|
|
4142
|
+
message: "missing session token"
|
|
4143
|
+
};
|
|
4144
|
+
candidate = request.w;
|
|
4145
|
+
}
|
|
4146
|
+
if (!isActionPayload_Any_JsonObject(candidate)) return {
|
|
4147
|
+
k: "err",
|
|
4148
|
+
message: "malformed action wire"
|
|
4149
|
+
};
|
|
4150
|
+
const wire = candidate;
|
|
4151
|
+
if (session != null && wire.type === "request") wire.context.originClient = session.client.toJsonObject();
|
|
4152
|
+
const resultWire = (await (await this._runtime.handleActionPayloadWire(wire)).waitForResultPayload()).toJsonObject();
|
|
4153
|
+
if (session?.crypto != null) return {
|
|
4309
4154
|
k: "act",
|
|
4310
|
-
|
|
4311
|
-
|
|
4155
|
+
c: bytesToBase64(await session.crypto.encryptFrame(textEncoder.encode(JSON.stringify(resultWire))))
|
|
4156
|
+
};
|
|
4157
|
+
return {
|
|
4158
|
+
k: "act",
|
|
4159
|
+
w: resultWire
|
|
4312
4160
|
};
|
|
4313
4161
|
}
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
}
|
|
4320
|
-
async function extractReplyWire(state, reply) {
|
|
4321
|
-
if (reply.k !== "act") return void 0;
|
|
4322
|
-
if ("c" in reply) {
|
|
4323
|
-
if (state.crypto == null) return void 0;
|
|
4324
|
-
const plain = await state.crypto.decryptFrame(base64ToBytes(reply.c));
|
|
4325
|
-
return JSON.parse(textDecoder.decode(plain));
|
|
4162
|
+
_err(message) {
|
|
4163
|
+
return encodeExchange({
|
|
4164
|
+
k: "err",
|
|
4165
|
+
message
|
|
4166
|
+
});
|
|
4326
4167
|
}
|
|
4327
|
-
|
|
4328
|
-
}
|
|
4329
|
-
async function runConnectorExchangeHandshake(carrier, secure) {
|
|
4330
|
-
await secure.link.initialize();
|
|
4331
|
-
const handshake = createClientHandshake({
|
|
4332
|
-
link: secure.link,
|
|
4333
|
-
localCoordinate: secure.localCoordinate,
|
|
4334
|
-
dictionaryVersion: secure.dictionaryVersion,
|
|
4335
|
-
securityLevel: secure.securityLevel
|
|
4336
|
-
});
|
|
4337
|
-
const hsid = nanoid();
|
|
4338
|
-
const hello = await handshake.createHello();
|
|
4339
|
-
const welcomeReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
|
|
4340
|
-
k: "hs",
|
|
4341
|
-
hsid,
|
|
4342
|
-
m: encodeHandshakeMessage(hello)
|
|
4343
|
-
}))));
|
|
4344
|
-
if (welcomeReply?.k !== "hs") throw new Error("[exchange-handshake] expected a welcome reply");
|
|
4345
|
-
const welcome = decodeHandshakeMessage(welcomeReply.m);
|
|
4346
|
-
if (welcome == null) throw new Error("[exchange-handshake] malformed welcome");
|
|
4347
|
-
if (welcome.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${welcome.reason}`);
|
|
4348
|
-
if (welcome.t !== "welcome") throw new Error(`[exchange-handshake] expected welcome, got ${welcome.t}`);
|
|
4349
|
-
const prove = await handshake.onWelcome(welcome);
|
|
4350
|
-
const acceptReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
|
|
4351
|
-
k: "hs",
|
|
4352
|
-
hsid,
|
|
4353
|
-
m: encodeHandshakeMessage(prove)
|
|
4354
|
-
}))));
|
|
4355
|
-
if (acceptReply?.k !== "hs") throw new Error("[exchange-handshake] expected an accept reply");
|
|
4356
|
-
const accept = decodeHandshakeMessage(acceptReply.m);
|
|
4357
|
-
if (accept == null) throw new Error("[exchange-handshake] malformed accept");
|
|
4358
|
-
if (accept.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${accept.reason}`);
|
|
4359
|
-
if (accept.t !== "accept") throw new Error(`[exchange-handshake] expected accept, got ${accept.t}`);
|
|
4360
|
-
if (acceptReply.t == null) throw new Error("[exchange-handshake] accept missing session token");
|
|
4361
|
-
const result = await handshake.onAccept(accept);
|
|
4362
|
-
const crypto = result.securityLevel === "encrypted" ? createActionFrameCrypto({
|
|
4363
|
-
link: secure.link,
|
|
4364
|
-
linkedClientId: result.linkedClientId
|
|
4365
|
-
}) : void 0;
|
|
4366
|
-
return {
|
|
4367
|
-
token: acceptReply.t,
|
|
4368
|
-
crypto
|
|
4369
|
-
};
|
|
4370
|
-
}
|
|
4371
|
-
function asString(frame) {
|
|
4372
|
-
if (typeof frame === "string") return frame;
|
|
4373
|
-
return textDecoder.decode(frame instanceof ArrayBuffer ? new Uint8Array(frame) : frame);
|
|
4374
|
-
}
|
|
4168
|
+
};
|
|
4375
4169
|
//#endregion
|
|
4376
|
-
//#region src/ActionRuntime/
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4170
|
+
//#region src/ActionRuntime/Handler/PeerLink/Acceptor/createActionFetchHandler.ts
|
|
4171
|
+
/** Permissive defaults — fine for a public action endpoint; override (or disable) via `cors`. */
|
|
4172
|
+
const DEFAULT_CORS_HEADERS = {
|
|
4173
|
+
"Access-Control-Allow-Origin": "*",
|
|
4174
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
4175
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
4176
|
+
"Access-Control-Max-Age": "86400"
|
|
4177
|
+
};
|
|
4178
|
+
/**
|
|
4179
|
+
* Build the `fetch` handler a server/Durable-Object exposes for action traffic, folding in the
|
|
4180
|
+
* boilerplate every endpoint repeats: CORS (incl. the `OPTIONS` preflight), routing the `/action`
|
|
4181
|
+
* `POST` body through the runtime (`handleActionPayloadWire` → `waitForResultPayload` →
|
|
4182
|
+
* `toHttpResponse`), an optional WebSocket-upgrade hook, and a `404` fallback.
|
|
4183
|
+
*
|
|
4184
|
+
* It only touches web-standard `Request`/`Response`, so it stays transport-agnostic — the one
|
|
4185
|
+
* environment-specific bit (the WS upgrade) is injected via {@link IActionFetchHandlerOptions.onWebSocketUpgrade}:
|
|
4186
|
+
* ```ts
|
|
4187
|
+
* this.fetchHandler = createActionFetchHandler(this.runtime, {
|
|
4188
|
+
* onWebSocketUpgrade: () => {
|
|
4189
|
+
* const pair = new WebSocketPair();
|
|
4190
|
+
* this.ctx.acceptWebSocket(pair[1]);
|
|
4191
|
+
* return new Response(null, { status: 101, webSocket: pair[0] });
|
|
4192
|
+
* },
|
|
4193
|
+
* });
|
|
4194
|
+
* // async fetch(request) { return this.fetchHandler(request); }
|
|
4195
|
+
* ```
|
|
4196
|
+
*/
|
|
4197
|
+
function createActionFetchHandler(runtime, options = {}) {
|
|
4198
|
+
const corsHeaders = options.cors === false ? {} : options.cors ?? DEFAULT_CORS_HEADERS;
|
|
4199
|
+
const isActionPath = options.isActionPath ?? ((url) => url.pathname.endsWith("/action"));
|
|
4200
|
+
const isWebSocketPath = options.isWebSocketPath ?? ((url) => url.pathname.endsWith("/ws"));
|
|
4201
|
+
const exchangeAcceptor = options.security != null ? new ExchangeAcceptor({
|
|
4202
|
+
runtime,
|
|
4203
|
+
security: options.security
|
|
4204
|
+
}) : void 0;
|
|
4205
|
+
const withCors = (response) => {
|
|
4206
|
+
if (options.cors === false) return response;
|
|
4207
|
+
const headers = new Headers(response.headers);
|
|
4208
|
+
for (const [key, value] of Object.entries(corsHeaders)) headers.set(key, value);
|
|
4209
|
+
return new Response(response.body, {
|
|
4210
|
+
status: response.status,
|
|
4211
|
+
headers
|
|
4212
|
+
});
|
|
4386
4213
|
};
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4214
|
+
return async (request) => {
|
|
4215
|
+
if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 }));
|
|
4216
|
+
const url = new URL(request.url);
|
|
4217
|
+
const isWebSocketUpgrade = options.isWebSocketUpgrade ?? ((req, u) => req.headers.get("Upgrade") === "websocket" && isWebSocketPath(u));
|
|
4218
|
+
if (options.onWebSocketUpgrade != null && isWebSocketUpgrade(request, url)) return options.onWebSocketUpgrade(request, url);
|
|
4219
|
+
if (request.method === "POST" && isActionPath(url)) {
|
|
4220
|
+
if (exchangeAcceptor != null) {
|
|
4221
|
+
const reply = await exchangeAcceptor.handlePost(await request.text());
|
|
4222
|
+
return withCors(new Response(reply, {
|
|
4223
|
+
status: 200,
|
|
4224
|
+
headers: { "Content-Type": "application/json" }
|
|
4225
|
+
}));
|
|
4226
|
+
}
|
|
4227
|
+
return withCors((await (await runtime.handleActionPayloadWire(await request.json())).waitForResultPayload()).toHttpResponse({ useErrorStatus: options.useErrorStatus }));
|
|
4228
|
+
}
|
|
4229
|
+
return withCors(new Response("Not found", { status: 404 }));
|
|
4391
4230
|
};
|
|
4392
|
-
if (transportStatus.status === "unsupported") return { status: "unsupported" };
|
|
4393
|
-
return { status: "uninitialized" };
|
|
4394
4231
|
}
|
|
4395
4232
|
//#endregion
|
|
4396
|
-
//#region src/ActionRuntime/
|
|
4397
|
-
let transportOrd = 0;
|
|
4233
|
+
//#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/ConnectionStateStore.ts
|
|
4398
4234
|
/**
|
|
4399
|
-
*
|
|
4400
|
-
*
|
|
4401
|
-
*
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
/**
|
|
4424
|
-
* Whether a `ready`-status transport still needs asynchronous bring-up before its methods exist —
|
|
4425
|
-
* awaiting the carrier to open and/or running a handshake. Default `false`: a stateless transport
|
|
4426
|
-
* (HTTP) is usable the instant `getTransport` reports `ready`, so it stays a terminal *synchronous*
|
|
4427
|
-
* fallback in {@link ConnectionTransportManager}. Stream carriers (Link/WS) override to `true`.
|
|
4428
|
-
*/
|
|
4429
|
-
_needsAsyncBringUp(_readyData) {
|
|
4430
|
-
return false;
|
|
4235
|
+
* A typed per-connection state store that co-owns the app state and the acceptor handler's routing
|
|
4236
|
+
* binding in one attachment, so neither the consumer nor the handler has to hand-merge the two. Create
|
|
4237
|
+
* it through {@link createConnectionStateStore} (which also wires binding persistence and replays
|
|
4238
|
+
* surviving connections after a wake), then `get`/`set`/`clearApp` the app state directly.
|
|
4239
|
+
*
|
|
4240
|
+
* The mechanism is carrier-neutral — it only needs read/write/enumerate callbacks for the connection's
|
|
4241
|
+
* attachment — but it pays off on transports whose connections outlive process eviction (e.g. a
|
|
4242
|
+
* Durable Object's hibernatable WebSockets), which is why it lives beside the hibernation adapter.
|
|
4243
|
+
*
|
|
4244
|
+
* ```ts
|
|
4245
|
+
* const players = createConnectionStateStore(serverHandler, {
|
|
4246
|
+
* schema: vs_player,
|
|
4247
|
+
* read: (ws) => ws.deserializeAttachment(),
|
|
4248
|
+
* write: (ws, v) => ws.serializeAttachment(v),
|
|
4249
|
+
* getConnections: () => ctx.getWebSockets(),
|
|
4250
|
+
* });
|
|
4251
|
+
* players.set(ws, player); // binding is preserved automatically
|
|
4252
|
+
* const player = players.get(ws);
|
|
4253
|
+
* ```
|
|
4254
|
+
*/
|
|
4255
|
+
var ConnectionStateStore = class {
|
|
4256
|
+
options;
|
|
4257
|
+
constructor(options) {
|
|
4258
|
+
this.options = options;
|
|
4431
4259
|
}
|
|
4432
|
-
/**
|
|
4433
|
-
|
|
4434
|
-
return
|
|
4260
|
+
/** The validated app state for a connection, or `null` if unset / invalid. */
|
|
4261
|
+
get(connection) {
|
|
4262
|
+
return this._readAttachment(connection).app ?? null;
|
|
4435
4263
|
}
|
|
4436
|
-
/**
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4264
|
+
/** Set the app state, preserving the runtime binding already pinned to the connection. */
|
|
4265
|
+
set(connection, app) {
|
|
4266
|
+
const existing = this._readAttachment(connection);
|
|
4267
|
+
this.options.write(connection, {
|
|
4268
|
+
app,
|
|
4269
|
+
binding: existing.binding
|
|
4270
|
+
});
|
|
4442
4271
|
}
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4272
|
+
/** Clear the app state but keep the binding (e.g. a spectator that stopped watching). */
|
|
4273
|
+
clearApp(connection) {
|
|
4274
|
+
const existing = this._readAttachment(connection);
|
|
4275
|
+
this.options.write(connection, { binding: existing.binding });
|
|
4447
4276
|
}
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
return `${this.transOrd}:${inner}`;
|
|
4277
|
+
/** Every live connection paired with its (validated) app state — for rebuilding in-memory state after a wake. */
|
|
4278
|
+
entries() {
|
|
4279
|
+
return this.options.getConnections().map((connection) => [connection, this._readAttachment(connection).app ?? null]);
|
|
4452
4280
|
}
|
|
4453
|
-
/**
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
return this.initialized.isAvailable?.(input) ?? true;
|
|
4281
|
+
/** @internal Persist a freshly-bound connection's binding, preserving any app state already stored. */
|
|
4282
|
+
_persistBinding(connection, binding) {
|
|
4283
|
+
const existing = this._readAttachment(connection);
|
|
4284
|
+
this.options.write(connection, {
|
|
4285
|
+
app: existing.app,
|
|
4286
|
+
binding
|
|
4287
|
+
});
|
|
4461
4288
|
}
|
|
4462
|
-
|
|
4463
|
-
|
|
4289
|
+
/** @internal The persisted binding for a connection, if any (used to replay routing after a wake). */
|
|
4290
|
+
_readBinding(connection) {
|
|
4291
|
+
return this._readAttachment(connection).binding;
|
|
4464
4292
|
}
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
if (
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
}
|
|
4478
|
-
|
|
4479
|
-
const initializationPromise = statusInfo.initializationPromise.then((result) => result.status === "ready" ? this._bringUp(result.readyData) : result);
|
|
4480
|
-
return {
|
|
4481
|
-
status: "initializing",
|
|
4482
|
-
timeStarted: statusInfo.timeStarted,
|
|
4483
|
-
initializationPromise
|
|
4484
|
-
};
|
|
4293
|
+
_readAttachment(connection) {
|
|
4294
|
+
try {
|
|
4295
|
+
const raw = this.options.read(connection);
|
|
4296
|
+
if (typeof raw !== "object" || raw === null) return {};
|
|
4297
|
+
const attachment = raw;
|
|
4298
|
+
const result = {};
|
|
4299
|
+
if (attachment.binding != null) result.binding = attachment.binding;
|
|
4300
|
+
if (attachment.app !== void 0) {
|
|
4301
|
+
const app = this._validateApp(attachment.app);
|
|
4302
|
+
if (app !== void 0) result.app = app;
|
|
4303
|
+
}
|
|
4304
|
+
return result;
|
|
4305
|
+
} catch {
|
|
4306
|
+
return {};
|
|
4485
4307
|
}
|
|
4486
|
-
return statusInfo;
|
|
4487
4308
|
}
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4309
|
+
_validateApp(value) {
|
|
4310
|
+
const schema = this.options.schema;
|
|
4311
|
+
if (schema == null) return value;
|
|
4312
|
+
const result = schema["~standard"].validate(value);
|
|
4313
|
+
if (result instanceof Promise) return void 0;
|
|
4314
|
+
if (result.issues != null) return void 0;
|
|
4315
|
+
return result.value;
|
|
4495
4316
|
}
|
|
4496
4317
|
};
|
|
4497
|
-
//#endregion
|
|
4498
|
-
//#region src/ActionRuntime/Transport/Exchange/ExchangeConnection.ts
|
|
4499
4318
|
/**
|
|
4500
|
-
*
|
|
4501
|
-
*
|
|
4502
|
-
*
|
|
4319
|
+
* Build a per-connection {@link ConnectionStateStore} bound to an {@link AcceptorHandler}: it registers
|
|
4320
|
+
* itself as the handler's connection-bound persistence callback (so bindings are written without
|
|
4321
|
+
* overwriting app state) and immediately replays every live connection's stored binding via
|
|
4322
|
+
* {@link AcceptorHandler.rehydrateConnection} — so on a transport that resumes after eviction (e.g. a
|
|
4323
|
+
* Durable Object waking from hibernation) both the app identity and the action routing come back from a
|
|
4324
|
+
* single attachment, with no storage reads and no hand-rolled merge.
|
|
4325
|
+
*
|
|
4326
|
+
* Lives outside the handler so the generic {@link AcceptorHandler} stays free of any attachment/
|
|
4327
|
+
* hibernation concern — it exposes only the neutral `setOnConnectionBound` + `rehydrateConnection`
|
|
4328
|
+
* hooks this builder drives.
|
|
4503
4329
|
*/
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
}
|
|
4511
|
-
_getCacheKey(input) {
|
|
4512
|
-
return this.initialized.getTransportCacheKey?.(input).join("\0") ?? "";
|
|
4513
|
-
}
|
|
4514
|
-
_needsAsyncBringUp(data) {
|
|
4515
|
-
return data.secureChannel != null && data.secureChannel.securityLevel !== "none";
|
|
4516
|
-
}
|
|
4517
|
-
_finalizeReady(data) {
|
|
4518
|
-
const secure = data.secureChannel;
|
|
4519
|
-
if (secure != null && secure.securityLevel !== "none") return finalizeSecureExchangeMethods({
|
|
4520
|
-
...this._sessionContext(data),
|
|
4521
|
-
secure
|
|
4522
|
-
});
|
|
4523
|
-
return this._finalizeTransportMethods(data);
|
|
4524
|
-
}
|
|
4525
|
-
_finalizeTransportMethods(data) {
|
|
4526
|
-
return finalizePlainExchangeMethods(this._sessionContext(data));
|
|
4527
|
-
}
|
|
4528
|
-
_sessionContext(data) {
|
|
4529
|
-
return {
|
|
4530
|
-
carrier: data.carrier,
|
|
4531
|
-
updateRunConfig: data.updateRunConfig,
|
|
4532
|
-
secure: data.secureChannel
|
|
4533
|
-
};
|
|
4330
|
+
function createConnectionStateStore(handler, options) {
|
|
4331
|
+
const store = new ConnectionStateStore(options);
|
|
4332
|
+
handler.setOnConnectionBound((connection, binding) => store._persistBinding(connection, binding));
|
|
4333
|
+
for (const connection of options.getConnections()) {
|
|
4334
|
+
const binding = store._readBinding(connection);
|
|
4335
|
+
if (binding != null) handler.rehydrateConnection(connection, binding);
|
|
4534
4336
|
}
|
|
4535
|
-
|
|
4337
|
+
return store;
|
|
4338
|
+
}
|
|
4536
4339
|
//#endregion
|
|
4537
|
-
//#region src/ActionRuntime/
|
|
4340
|
+
//#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/createHibernatableWsServerAdapter.ts
|
|
4538
4341
|
/**
|
|
4539
|
-
*
|
|
4540
|
-
*
|
|
4541
|
-
*
|
|
4542
|
-
*
|
|
4342
|
+
* Wire the hibernation lifecycle for an acceptor handler on a transport whose connections outlive process
|
|
4343
|
+
* eviction (e.g. a Durable Object's hibernatable WebSockets). It owns persistence end to end:
|
|
4344
|
+
* registers `setAttachment` as the handler's connection-bound callback and immediately replays every
|
|
4345
|
+
* live connection's stored binding via `getAttachment`, so results/pushes still route after a wake.
|
|
4346
|
+
*
|
|
4347
|
+
* Layered on top of the generic {@link AcceptorHandler} — it touches only the handler's neutral
|
|
4348
|
+
* `setOnConnectionBound` / `rehydrateConnection` / `receive` / `dropConnection` surface, so no
|
|
4349
|
+
* hibernation concern leaks into the handler itself.
|
|
4350
|
+
*
|
|
4351
|
+
* Construct it once when the handler is built, then forward connection events:
|
|
4352
|
+
* ```ts
|
|
4353
|
+
* const duplex = createHibernatableWsServerAdapter({ handler, getConnections, getAttachment, setAttachment });
|
|
4354
|
+
* // webSocketMessage(ws, msg) => duplex.receive(ws, msg);
|
|
4355
|
+
* // webSocketClose/Error(ws) => duplex.drop(ws);
|
|
4356
|
+
* ```
|
|
4543
4357
|
*/
|
|
4544
|
-
|
|
4545
|
-
options;
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4358
|
+
function createHibernatableWsServerAdapter(options) {
|
|
4359
|
+
const { handler, getConnections, getAttachment, setAttachment } = options;
|
|
4360
|
+
handler.setOnConnectionBound(setAttachment);
|
|
4361
|
+
for (const connection of getConnections()) {
|
|
4362
|
+
const binding = getAttachment(connection);
|
|
4363
|
+
if (binding != null) handler.rehydrateConnection(connection, binding);
|
|
4550
4364
|
}
|
|
4551
|
-
|
|
4552
|
-
|
|
4365
|
+
return {
|
|
4366
|
+
receive: (connection, frame) => handler.receive(connection, frame),
|
|
4367
|
+
drop: (connection) => handler.dropConnection(connection)
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
//#endregion
|
|
4371
|
+
//#region src/ActionRuntime/Channel/serveChannel.ts
|
|
4372
|
+
/** Default accepted set, shared by every carrier: negotiate per connection to whatever the client picks. */
|
|
4373
|
+
const DEFAULT_SERVER_SECURITY_LEVELS = [
|
|
4374
|
+
"none",
|
|
4375
|
+
"authenticated",
|
|
4376
|
+
"encrypted"
|
|
4377
|
+
];
|
|
4378
|
+
function serveChannel(runtime, channel, options) {
|
|
4379
|
+
const duplexCarriers = options.carriers.filter((carrier) => !isExchangeAcceptorCarrier(carrier));
|
|
4380
|
+
const exchangeCarriers = options.carriers.filter(isExchangeAcceptorCarrier);
|
|
4381
|
+
if (exchangeCarriers.length > 1) throw new Error("serveChannel: at most one exchange carrier is supported");
|
|
4382
|
+
const exchangeCarrier = exchangeCarriers[0];
|
|
4383
|
+
const singleDuplex = duplexCarriers.length === 1;
|
|
4384
|
+
if (options.connectionState != null && !singleDuplex) throw new Error("serveChannel: `connectionState` requires exactly one duplex carrier");
|
|
4385
|
+
if (options.channelCases != null && !singleDuplex) throw new Error("serveChannel: `channelCases` requires exactly one duplex carrier");
|
|
4386
|
+
const exchangeSecure = exchangeCarrier != null && (exchangeCarrier.secure ?? true);
|
|
4387
|
+
const anyDuplexSecure = duplexCarriers.some((carrier) => carrier.secure ?? true);
|
|
4388
|
+
const securityLevel = options.securityLevel ?? DEFAULT_SERVER_SECURITY_LEVELS;
|
|
4389
|
+
let secure;
|
|
4390
|
+
if (anyDuplexSecure || exchangeSecure) {
|
|
4391
|
+
const storage = options.storage;
|
|
4392
|
+
if (storage == null) throw new Error("serveChannel: a secure carrier requires `storage`. Pass it, or set `secure: false` on the carrier for a plain endpoint.");
|
|
4393
|
+
secure = {
|
|
4394
|
+
storage,
|
|
4395
|
+
link: options.link ?? new ClientCryptoKeyLink({ storageAdapter: storage }),
|
|
4396
|
+
verifyKeyResolver: options.verifyKeyResolver ?? createStorageTofuVerifyKeyResolver(storage)
|
|
4397
|
+
};
|
|
4553
4398
|
}
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4399
|
+
const plainRouter = (handler) => ({
|
|
4400
|
+
receive: (connection, frame) => handler.receive(connection, frame),
|
|
4401
|
+
drop: (connection) => handler.dropConnection(connection)
|
|
4402
|
+
});
|
|
4403
|
+
const asObject = (value) => typeof value === "object" && value != null ? value : {};
|
|
4404
|
+
const handlers = [];
|
|
4405
|
+
let connections;
|
|
4406
|
+
for (const carrier of duplexCarriers) {
|
|
4407
|
+
const handler = (carrier.secure ?? true) && secure != null ? acceptChannel(runtime, channel, {
|
|
4408
|
+
clientEnv: options.clientEnv,
|
|
4409
|
+
storageAdapter: secure.storage,
|
|
4410
|
+
link: secure.link,
|
|
4411
|
+
verifyKeyResolver: secure.verifyKeyResolver,
|
|
4412
|
+
securityLevel,
|
|
4413
|
+
send: carrier.send,
|
|
4414
|
+
defaultTimeout: options.defaultTimeout
|
|
4415
|
+
}) : createAcceptorHandler({
|
|
4416
|
+
clientEnv: options.clientEnv,
|
|
4417
|
+
createFormatMessage: channel.createCodec,
|
|
4418
|
+
send: carrier.send,
|
|
4419
|
+
runtime,
|
|
4420
|
+
defaultTimeout: options.defaultTimeout
|
|
4421
|
+
});
|
|
4422
|
+
const attach = carrier.attachmentStore;
|
|
4423
|
+
let router;
|
|
4424
|
+
if (attach == null) router = plainRouter(handler);
|
|
4425
|
+
else if (options.connectionState != null) {
|
|
4426
|
+
connections = createConnectionStateStore(handler, {
|
|
4427
|
+
schema: options.connectionState.schema,
|
|
4428
|
+
getConnections: attach.getConnections,
|
|
4429
|
+
read: attach.read,
|
|
4430
|
+
write: attach.write
|
|
4431
|
+
});
|
|
4432
|
+
router = plainRouter(handler);
|
|
4433
|
+
} else router = createHibernatableWsServerAdapter({
|
|
4434
|
+
handler,
|
|
4435
|
+
getConnections: attach.getConnections,
|
|
4436
|
+
getAttachment: (connection) => attach.read(connection)?.binding,
|
|
4437
|
+
setAttachment: (connection, binding) => attach.write(connection, {
|
|
4438
|
+
...asObject(attach.read(connection)),
|
|
4439
|
+
binding
|
|
4566
4440
|
})
|
|
4567
|
-
})
|
|
4441
|
+
});
|
|
4442
|
+
carrier._activate(router);
|
|
4443
|
+
handlers.push(handler);
|
|
4568
4444
|
}
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4445
|
+
runtime.addHandlers([...options.handlers ?? [], ...handlers]);
|
|
4446
|
+
if (options.channelCases != null) runtime.addHandlers([acceptChannelConnections(handlers[0], channel, options.channelCases)]);
|
|
4447
|
+
const exchangeSecurity = exchangeSecure && secure != null ? {
|
|
4448
|
+
link: secure.link,
|
|
4449
|
+
verifyKeyResolver: secure.verifyKeyResolver,
|
|
4450
|
+
localCoordinate: runtime.coordinate.toJsonObject(),
|
|
4451
|
+
dictionaryVersion: channel.dictionaryVersion,
|
|
4452
|
+
securityLevel
|
|
4453
|
+
} : void 0;
|
|
4454
|
+
const defaultIsUpgrade = (request) => request.headers.get("Upgrade") === "websocket";
|
|
4455
|
+
const upgraders = [];
|
|
4456
|
+
for (const carrier of duplexCarriers) {
|
|
4457
|
+
if (carrier.upgrade == null) continue;
|
|
4458
|
+
upgraders.push({
|
|
4459
|
+
isUpgrade: carrier.isUpgrade ?? defaultIsUpgrade,
|
|
4460
|
+
upgrade: carrier.upgrade
|
|
4461
|
+
});
|
|
4575
4462
|
}
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
//#region src/ActionRuntime/Transport/SecureSession/establishLinkSession.ts
|
|
4584
|
-
const HANDSHAKE_TIMEOUT_MS = 15e3;
|
|
4585
|
-
/** Plain path (no handshake): route every inbound frame to the runtime; send without crypto. */
|
|
4586
|
-
function finalizePlainLinkMethods(ctx) {
|
|
4587
|
-
const disconnectListeners = [];
|
|
4588
|
-
const abortSet = /* @__PURE__ */ new Set();
|
|
4589
|
-
const pipe = makePipe(ctx, void 0);
|
|
4590
|
-
ctx.channel.attach({
|
|
4591
|
-
onMessage: (frame) => void handleIncomingActionFrame(ctx, pipe, frame),
|
|
4592
|
-
onClose: () => onChannelClosed(ctx, disconnectListeners, abortSet),
|
|
4593
|
-
onError: () => onChannelClosed(ctx, disconnectListeners, abortSet)
|
|
4463
|
+
const fetch = createActionFetchHandler(runtime, {
|
|
4464
|
+
cors: exchangeCarrier?.cors,
|
|
4465
|
+
onWebSocketUpgrade: upgraders.length === 0 ? void 0 : (request, url) => (upgraders.find((u) => u.isUpgrade(request, url)) ?? upgraders[0]).upgrade(request, url),
|
|
4466
|
+
isWebSocketUpgrade: upgraders.length === 0 ? void 0 : (request, url) => upgraders.some((u) => u.isUpgrade(request, url)),
|
|
4467
|
+
isActionPath: exchangeCarrier != null ? exchangeCarrier.isActionPath ?? (() => true) : () => false,
|
|
4468
|
+
security: exchangeSecurity,
|
|
4469
|
+
useErrorStatus: exchangeCarrier?.useErrorStatus
|
|
4594
4470
|
});
|
|
4595
|
-
|
|
4471
|
+
const duplex = duplexCarriers.length === 1 ? duplexCarriers[0] : void 0;
|
|
4472
|
+
const pushToClient = (target, request, pushOptions) => {
|
|
4473
|
+
const owner = target instanceof RuntimeCoordinate ? handlers.find((handler) => handler.ownsLiveConnectionFor(target)) : handlers.find((handler) => handler.hasConnection(target));
|
|
4474
|
+
if (owner == null) throw new Error("serveChannel: no duplex carrier holds a connection for the push target");
|
|
4475
|
+
return owner.pushToClient(runtime, target, request, pushOptions);
|
|
4476
|
+
};
|
|
4477
|
+
const broadcast = (makeRequest, broadcastOptions) => {
|
|
4478
|
+
if (!singleDuplex) throw new Error("serveChannel: broadcast requires exactly one duplex carrier — broadcast over a specific handlers[i] instead");
|
|
4479
|
+
handlers[0].broadcast(makeRequest, {
|
|
4480
|
+
runtime,
|
|
4481
|
+
...broadcastOptions
|
|
4482
|
+
});
|
|
4483
|
+
};
|
|
4484
|
+
return {
|
|
4485
|
+
handlers,
|
|
4486
|
+
fetch,
|
|
4487
|
+
duplex,
|
|
4488
|
+
pushToClient,
|
|
4489
|
+
broadcast,
|
|
4490
|
+
connections
|
|
4491
|
+
};
|
|
4596
4492
|
}
|
|
4493
|
+
//#endregion
|
|
4494
|
+
//#region src/ActionRuntime/Transport/Carrier/duplex/inMemory/createInMemoryChannel.ts
|
|
4597
4495
|
/**
|
|
4598
|
-
*
|
|
4599
|
-
*
|
|
4600
|
-
*
|
|
4496
|
+
* Two cross-wired in-process byte channels — a loopback carrier with no socket. The client end is a
|
|
4497
|
+
* {@link IDuplexCarrier} you hand to a {@link LinkTransport}; the server end plugs into an
|
|
4498
|
+
* `AcceptorHandler` (`send: (_, f) => serverEndpoint.send(f)`, and `serverEndpoint.onMessage(f =>
|
|
4499
|
+
* handler.receive(conn, f))`). Frames are delivered on a microtask, so each side observes the other
|
|
4500
|
+
* asynchronously — exactly like a real transport — which makes this ideal for tests and for running
|
|
4501
|
+
* two runtimes in one process (or proving a non-WS carrier end to end).
|
|
4601
4502
|
*/
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
let
|
|
4607
|
-
|
|
4608
|
-
const
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
return;
|
|
4615
|
-
}
|
|
4616
|
-
if (typeof frame === "string") {
|
|
4617
|
-
const message = decodeHandshakeMessage(frame);
|
|
4618
|
-
if (message != null) {
|
|
4619
|
-
const waiter = handshakeWaiters.shift();
|
|
4620
|
-
if (waiter != null) waiter(message);
|
|
4621
|
-
else handshakeQueue.push(message);
|
|
4622
|
-
return;
|
|
4623
|
-
}
|
|
4624
|
-
}
|
|
4625
|
-
pendingActionFrames.push(frame);
|
|
4626
|
-
},
|
|
4627
|
-
onClose: () => onChannelClosed(ctx, disconnectListeners, abortSet),
|
|
4628
|
-
onError: () => onChannelClosed(ctx, disconnectListeners, abortSet)
|
|
4629
|
-
});
|
|
4630
|
-
const nextHandshakeMessage = () => {
|
|
4631
|
-
const queued = handshakeQueue.shift();
|
|
4632
|
-
if (queued != null) return Promise.resolve(queued);
|
|
4633
|
-
return new Promise((resolve, reject) => {
|
|
4634
|
-
const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("[link-handshake] timed out waiting for peer reply")), HANDSHAKE_TIMEOUT_MS);
|
|
4635
|
-
handshakeWaiters.push((message) => {
|
|
4636
|
-
clearTimeout(timeout);
|
|
4637
|
-
resolve(message);
|
|
4638
|
-
});
|
|
4503
|
+
function createInMemoryChannelPair() {
|
|
4504
|
+
let clientMessage;
|
|
4505
|
+
let clientClose;
|
|
4506
|
+
let serverMessage;
|
|
4507
|
+
let serverClose;
|
|
4508
|
+
let open = true;
|
|
4509
|
+
const closeBoth = () => {
|
|
4510
|
+
if (!open) return;
|
|
4511
|
+
open = false;
|
|
4512
|
+
queueMicrotask(() => {
|
|
4513
|
+
clientClose?.();
|
|
4514
|
+
serverClose?.();
|
|
4639
4515
|
});
|
|
4640
4516
|
};
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
const accept = await nextHandshakeMessage();
|
|
4669
|
-
if (accept.t === "reject") throw new Error(`[link-handshake] rejected by peer: ${accept.reason}`);
|
|
4670
|
-
if (accept.t !== "accept") throw new Error(`[link-handshake] expected accept, got ${accept.t}`);
|
|
4671
|
-
const result = await handshake.onAccept(accept);
|
|
4672
|
-
return result.securityLevel === "encrypted" ? createActionFrameCrypto({
|
|
4673
|
-
link: secure.link,
|
|
4674
|
-
linkedClientId: result.linkedClientId
|
|
4675
|
-
}) : void 0;
|
|
4676
|
-
}
|
|
4677
|
-
function buildSendMethods(ctx, pipe, disconnectListeners, abortSet) {
|
|
4678
|
-
const channel = ctx.channel;
|
|
4679
|
-
const sendActionData = (inputs) => {
|
|
4680
|
-
const { action, runningAction, timeout } = inputs;
|
|
4681
|
-
if (!channel.isOpen()) {
|
|
4682
|
-
if (action.type === "request") runningAction._abort(ctx.makeDisconnectError(action.id));
|
|
4683
|
-
return;
|
|
4684
|
-
}
|
|
4685
|
-
if (action.type === "request") {
|
|
4686
|
-
abortSet.add(runningAction);
|
|
4687
|
-
const timeoutId = setTimeout(() => {
|
|
4688
|
-
runningAction._abort(err_nice_transport.fromId("timeout", { timeout }));
|
|
4689
|
-
}, timeout);
|
|
4690
|
-
runningAction.addUpdateListeners([(update) => {
|
|
4691
|
-
if (update.type === "finished") {
|
|
4692
|
-
clearTimeout(timeoutId);
|
|
4693
|
-
abortSet.delete(runningAction);
|
|
4694
|
-
}
|
|
4695
|
-
}]);
|
|
4517
|
+
return {
|
|
4518
|
+
clientChannel: {
|
|
4519
|
+
ready: Promise.resolve(),
|
|
4520
|
+
isOpen: () => open,
|
|
4521
|
+
send: (frame) => {
|
|
4522
|
+
if (!open) return;
|
|
4523
|
+
queueMicrotask(() => serverMessage?.(frame));
|
|
4524
|
+
},
|
|
4525
|
+
attach: ({ onMessage, onClose }) => {
|
|
4526
|
+
clientMessage = onMessage;
|
|
4527
|
+
clientClose = onClose;
|
|
4528
|
+
},
|
|
4529
|
+
close: closeBoth,
|
|
4530
|
+
label: "in-memory"
|
|
4531
|
+
},
|
|
4532
|
+
serverEndpoint: {
|
|
4533
|
+
send: (frame) => {
|
|
4534
|
+
if (!open) return;
|
|
4535
|
+
queueMicrotask(() => clientMessage?.(frame));
|
|
4536
|
+
},
|
|
4537
|
+
onMessage: (handler) => {
|
|
4538
|
+
serverMessage = handler;
|
|
4539
|
+
},
|
|
4540
|
+
onClose: (handler) => {
|
|
4541
|
+
serverClose = handler;
|
|
4542
|
+
},
|
|
4543
|
+
close: closeBoth
|
|
4696
4544
|
}
|
|
4697
|
-
pipe.send(ctx.formatMessage?.outgoing(inputs) ?? JSON.stringify(inputs.action.toJsonObject()));
|
|
4698
4545
|
};
|
|
4546
|
+
}
|
|
4547
|
+
//#endregion
|
|
4548
|
+
//#region src/ActionRuntime/Transport/Carrier/duplex/inMemory/inMemoryCarrier.ts
|
|
4549
|
+
/**
|
|
4550
|
+
* A loopback duplex carrier with no socket — two cross-wired in-process ends. The connector end is an
|
|
4551
|
+
* {@link IDuplexCarrierSource} for {@link secureTransport}; the acceptor end plugs into an
|
|
4552
|
+
* `AcceptorHandler`. Ideal for tests and for running two runtimes in one process, or proving a
|
|
4553
|
+
* non-WS carrier end to end.
|
|
4554
|
+
*/
|
|
4555
|
+
function inMemoryCarrier() {
|
|
4556
|
+
const { clientChannel, serverEndpoint } = createInMemoryChannelPair();
|
|
4557
|
+
return {
|
|
4558
|
+
carrier: {
|
|
4559
|
+
carrierLabel: "memory",
|
|
4560
|
+
open: () => clientChannel,
|
|
4561
|
+
getCacheKey: () => ["memory"]
|
|
4562
|
+
},
|
|
4563
|
+
serverEndpoint
|
|
4564
|
+
};
|
|
4565
|
+
}
|
|
4566
|
+
//#endregion
|
|
4567
|
+
//#region src/ActionRuntime/Transport/Carrier/duplex/rtc/rtcDataChannelByteChannel.ts
|
|
4568
|
+
/**
|
|
4569
|
+
* Adapt a WebRTC `RTCDataChannel` to the carrier-agnostic {@link IDuplexCarrier}, so two browsers
|
|
4570
|
+
* (or two mobile apps) linked peer-to-peer — no server in the middle — run the *same* secure session as
|
|
4571
|
+
* a WebSocket. Hand it to `createSecureLinkTransport({ openChannel: () => rtcDataChannelByteChannel(dc) })`.
|
|
4572
|
+
*
|
|
4573
|
+
* The data channel must already be created (its negotiation/signaling is the app's concern); this only
|
|
4574
|
+
* drives bytes over it. Binary frames are requested as `ArrayBuffer` so the binary session codec unpacks
|
|
4575
|
+
* them synchronously; a `Blob` (if the channel hands one back) is normalized to a buffer.
|
|
4576
|
+
*/
|
|
4577
|
+
function rtcDataChannelByteChannel(dc) {
|
|
4578
|
+
dc.binaryType = "arraybuffer";
|
|
4579
|
+
let intentional = false;
|
|
4580
|
+
let onMessage;
|
|
4581
|
+
let onClose;
|
|
4582
|
+
const preAttach = [];
|
|
4583
|
+
const deliver = (frame) => {
|
|
4584
|
+
if (onMessage != null) onMessage(frame);
|
|
4585
|
+
else preAttach.push(frame);
|
|
4586
|
+
};
|
|
4587
|
+
dc.addEventListener("message", async (event) => {
|
|
4588
|
+
const frame = await normalizeFrame$1(event.data);
|
|
4589
|
+
if (frame !== void 0) deliver(frame);
|
|
4590
|
+
});
|
|
4591
|
+
dc.addEventListener("close", () => {
|
|
4592
|
+
if (!intentional) console.error("RTCDataChannel closed");
|
|
4593
|
+
onClose?.();
|
|
4594
|
+
});
|
|
4595
|
+
dc.addEventListener("error", (event) => {
|
|
4596
|
+
console.error("RTCDataChannel error:", event);
|
|
4597
|
+
onClose?.();
|
|
4598
|
+
});
|
|
4699
4599
|
return {
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4600
|
+
ready: new Promise((resolve, reject) => {
|
|
4601
|
+
if (dc.readyState === "open") {
|
|
4602
|
+
resolve();
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
dc.addEventListener("open", () => resolve(), { once: true });
|
|
4606
|
+
dc.addEventListener("error", (event) => reject(event), { once: true });
|
|
4607
|
+
dc.addEventListener("close", () => reject(/* @__PURE__ */ new Error("RTCDataChannel closed before open")), { once: true });
|
|
4608
|
+
}),
|
|
4609
|
+
isOpen: () => dc.readyState === "open",
|
|
4610
|
+
send: (frame) => {
|
|
4611
|
+
if (typeof frame === "string" || frame instanceof ArrayBuffer) dc.send(frame);
|
|
4612
|
+
else dc.send(new Uint8Array(frame));
|
|
4704
4613
|
},
|
|
4705
|
-
|
|
4614
|
+
attach: (handlers) => {
|
|
4615
|
+
onMessage = handlers.onMessage;
|
|
4616
|
+
onClose = handlers.onClose;
|
|
4617
|
+
for (const frame of preAttach) handlers.onMessage(frame);
|
|
4618
|
+
preAttach.length = 0;
|
|
4619
|
+
},
|
|
4620
|
+
close: () => {
|
|
4621
|
+
intentional = true;
|
|
4706
4622
|
try {
|
|
4707
|
-
|
|
4623
|
+
dc.close();
|
|
4708
4624
|
} catch {}
|
|
4709
4625
|
},
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
action: payload,
|
|
4713
|
-
...clients
|
|
4714
|
-
}) : void 0;
|
|
4715
|
-
pipe.send(formatted ?? JSON.stringify(payload.toJsonObject()));
|
|
4626
|
+
get label() {
|
|
4627
|
+
return dc.label != null && dc.label !== "" ? dc.label : void 0;
|
|
4716
4628
|
}
|
|
4717
4629
|
};
|
|
4718
4630
|
}
|
|
4719
|
-
async function
|
|
4720
|
-
|
|
4721
|
-
if (
|
|
4722
|
-
const rawJson = decodeActionFrame(decoded, ctx.formatMessage);
|
|
4723
|
-
if (rawJson != null) ctx.resolvers.onIncomingActionDataJson(rawJson);
|
|
4724
|
-
}
|
|
4725
|
-
function onChannelClosed(ctx, disconnectListeners, abortSet) {
|
|
4726
|
-
for (const cb of disconnectListeners) cb();
|
|
4727
|
-
const error = ctx.makeDisconnectError("—");
|
|
4728
|
-
for (const ra of [...abortSet]) ra._abort(error);
|
|
4631
|
+
async function normalizeFrame$1(data) {
|
|
4632
|
+
if (typeof data === "string" || data instanceof ArrayBuffer || data instanceof Uint8Array) return data;
|
|
4633
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) return await data.arrayBuffer();
|
|
4729
4634
|
}
|
|
4730
4635
|
//#endregion
|
|
4731
|
-
//#region src/ActionRuntime/Transport/
|
|
4732
|
-
/** Abort error for a closed link channel (carrier-neutral — the carrier itself isn't named). */
|
|
4733
|
-
function linkDisconnectError(actionId) {
|
|
4734
|
-
return err_nice_transport.fromId("send_failed", {
|
|
4735
|
-
actionId,
|
|
4736
|
-
actionState: "request",
|
|
4737
|
-
message: "link channel disconnected"
|
|
4738
|
-
});
|
|
4739
|
-
}
|
|
4636
|
+
//#region src/ActionRuntime/Transport/Carrier/duplex/rtc/rtcCarrier.ts
|
|
4740
4637
|
/**
|
|
4741
|
-
*
|
|
4742
|
-
*
|
|
4743
|
-
*
|
|
4744
|
-
* channel, a Bluetooth characteristic, and an in-memory pipe all run the identical secure layer.
|
|
4638
|
+
* A WebRTC {@link IDuplexCarrierSource} over an already-negotiated `RTCDataChannel` (signaling is the
|
|
4639
|
+
* app's concern). Hand it to {@link secureTransport} so two browsers/apps linked peer-to-peer run the
|
|
4640
|
+
* identical secure session as a WebSocket.
|
|
4745
4641
|
*/
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
secure
|
|
4769
|
-
});
|
|
4770
|
-
return this._finalizeTransportMethods(data);
|
|
4642
|
+
function rtcCarrier(dataChannel, options = {}) {
|
|
4643
|
+
return {
|
|
4644
|
+
carrierLabel: "webrtc",
|
|
4645
|
+
open: () => rtcDataChannelByteChannel(dataChannel),
|
|
4646
|
+
getCacheKey: options.getTransportCacheKey ?? (() => ["webrtc"]),
|
|
4647
|
+
getRouteInfo: options.getRouteInfo
|
|
4648
|
+
};
|
|
4649
|
+
}
|
|
4650
|
+
//#endregion
|
|
4651
|
+
//#region src/ActionRuntime/Transport/Carrier/duplex/ws/err_nice_transport_ws.ts
|
|
4652
|
+
let EErrId_NiceTransport_WebSocket = /* @__PURE__ */ function(EErrId_NiceTransport_WebSocket) {
|
|
4653
|
+
EErrId_NiceTransport_WebSocket["ws_disconnected"] = "ws_disconnected";
|
|
4654
|
+
EErrId_NiceTransport_WebSocket["ws_create_failed"] = "ws_create_failed";
|
|
4655
|
+
EErrId_NiceTransport_WebSocket["ws_error"] = "ws_error";
|
|
4656
|
+
return EErrId_NiceTransport_WebSocket;
|
|
4657
|
+
}({});
|
|
4658
|
+
const err_nice_transport_ws = err_nice_transport.createChildDomain({
|
|
4659
|
+
domain: "ws_transport",
|
|
4660
|
+
schema: {
|
|
4661
|
+
["ws_disconnected"]: err({ message: () => `WebSocket transport disconnected.` }),
|
|
4662
|
+
["ws_create_failed"]: err({ message: ({ originalError }) => `Failed to create WebSocket transport.${originalError ? ` Original error: ${originalError.message}` : ""}` }),
|
|
4663
|
+
["ws_error"]: err({ message: ({ originalError }) => `WebSocket transport error.${originalError ? ` Original error: ${originalError.message}` : ""}` })
|
|
4771
4664
|
}
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4665
|
+
});
|
|
4666
|
+
//#endregion
|
|
4667
|
+
//#region src/ActionRuntime/Transport/Carrier/duplex/ws/ws_util.ts
|
|
4668
|
+
/**
|
|
4669
|
+
* Send a text or binary frame over a socket. A binary formatter may hand back a `Uint8Array` whose
|
|
4670
|
+
* backing buffer is typed as `ArrayBufferLike` (msgpackr pools buffers / may be `SharedArrayBuffer`),
|
|
4671
|
+
* which `WebSocket.send`'s `BufferSource` parameter rejects — copy it into a fresh `ArrayBuffer`-backed
|
|
4672
|
+
* view so the type (and the bytes) are safe to send.
|
|
4673
|
+
*/
|
|
4674
|
+
function sendFrame(ws, data) {
|
|
4675
|
+
if (typeof data === "string" || data instanceof ArrayBuffer) {
|
|
4676
|
+
ws.send(data);
|
|
4677
|
+
return;
|
|
4780
4678
|
}
|
|
4781
|
-
|
|
4782
|
-
|
|
4679
|
+
ws.send(new Uint8Array(data));
|
|
4680
|
+
}
|
|
4681
|
+
/** Compact a WebSocket URL to `host/pathname` for devtools display, falling back to the raw url. */
|
|
4682
|
+
function shortWs(url) {
|
|
4683
|
+
try {
|
|
4684
|
+
const u = new URL(url);
|
|
4685
|
+
return `${u.host}${u.pathname}`;
|
|
4686
|
+
} catch {
|
|
4687
|
+
return url;
|
|
4783
4688
|
}
|
|
4784
|
-
}
|
|
4689
|
+
}
|
|
4785
4690
|
//#endregion
|
|
4786
|
-
//#region src/ActionRuntime/Transport/
|
|
4691
|
+
//#region src/ActionRuntime/Transport/Carrier/duplex/ws/webSocketByteChannel.ts
|
|
4692
|
+
/**
|
|
4693
|
+
* Adapt a `WebSocket` to the carrier-agnostic {@link IDuplexCarrier}, so the WebSocket becomes
|
|
4694
|
+
* "just another carrier" under the shared secure session. It owns every WebSocket-specific concern —
|
|
4695
|
+
* awaiting `open`, normalizing `Blob` frames to bytes, suppressing the log on a deliberate `close`, and
|
|
4696
|
+
* buffering any frame that arrives before the session attaches its handler — leaving the session itself
|
|
4697
|
+
* carrier-neutral.
|
|
4698
|
+
*/
|
|
4699
|
+
function webSocketByteChannel(ws) {
|
|
4700
|
+
let intentional = false;
|
|
4701
|
+
let onMessage;
|
|
4702
|
+
let onClose;
|
|
4703
|
+
const preAttach = [];
|
|
4704
|
+
const deliver = (frame) => {
|
|
4705
|
+
if (onMessage != null) onMessage(frame);
|
|
4706
|
+
else preAttach.push(frame);
|
|
4707
|
+
};
|
|
4708
|
+
ws.addEventListener("message", async (event) => {
|
|
4709
|
+
const frame = await normalizeFrame(event.data);
|
|
4710
|
+
if (frame !== void 0) deliver(frame);
|
|
4711
|
+
});
|
|
4712
|
+
ws.addEventListener("close", (event) => {
|
|
4713
|
+
if (!intentional) console.error("WebSocket closed:", event);
|
|
4714
|
+
onClose?.();
|
|
4715
|
+
});
|
|
4716
|
+
ws.addEventListener("error", (event) => {
|
|
4717
|
+
console.error("WebSocket error:", event);
|
|
4718
|
+
onClose?.();
|
|
4719
|
+
});
|
|
4720
|
+
return {
|
|
4721
|
+
ready: new Promise((resolve, reject) => {
|
|
4722
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
4723
|
+
resolve();
|
|
4724
|
+
return;
|
|
4725
|
+
}
|
|
4726
|
+
ws.addEventListener("open", () => resolve(), { once: true });
|
|
4727
|
+
ws.addEventListener("error", (event) => reject(event), { once: true });
|
|
4728
|
+
ws.addEventListener("close", (event) => reject(/* @__PURE__ */ new Error(`WebSocket closed before open: code=${event.code}`)), { once: true });
|
|
4729
|
+
}),
|
|
4730
|
+
isOpen: () => ws.readyState === WebSocket.OPEN,
|
|
4731
|
+
send: (frame) => sendFrame(ws, frame),
|
|
4732
|
+
attach: (handlers) => {
|
|
4733
|
+
onMessage = handlers.onMessage;
|
|
4734
|
+
onClose = handlers.onClose;
|
|
4735
|
+
for (const frame of preAttach) handlers.onMessage(frame);
|
|
4736
|
+
preAttach.length = 0;
|
|
4737
|
+
},
|
|
4738
|
+
close: () => {
|
|
4739
|
+
intentional = true;
|
|
4740
|
+
try {
|
|
4741
|
+
ws.close();
|
|
4742
|
+
} catch {}
|
|
4743
|
+
},
|
|
4744
|
+
get label() {
|
|
4745
|
+
return ws.url != null && ws.url !== "" ? ws.url : void 0;
|
|
4746
|
+
}
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
/** Accept text + binary frames (ArrayBuffer / Uint8Array / Blob); Blobs are converted to a buffer. */
|
|
4750
|
+
async function normalizeFrame(data) {
|
|
4751
|
+
if (typeof data === "string" || data instanceof ArrayBuffer || data instanceof Uint8Array) return data;
|
|
4752
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) return await data.arrayBuffer();
|
|
4753
|
+
}
|
|
4754
|
+
//#endregion
|
|
4755
|
+
//#region src/ActionRuntime/Transport/Carrier/duplex/ws/wsCarrier.ts
|
|
4787
4756
|
/**
|
|
4788
|
-
* A
|
|
4789
|
-
*
|
|
4790
|
-
*
|
|
4791
|
-
* or an in-memory pipe. Reported with an overridable carrier label in the devtools (defaults to "link").
|
|
4757
|
+
* A WebSocket {@link IDuplexCarrierSource}: opens an `arraybuffer` socket to `url` (cached per endpoint)
|
|
4758
|
+
* and adapts it to a carrier. Hand it to {@link secureTransport} (or `LinkTransport`) — the WebSocket is
|
|
4759
|
+
* now "just another carrier" under the shared secure session, with no WS-specific transport class.
|
|
4792
4760
|
*/
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
readyData: {
|
|
4811
|
-
channel: options.openChannel(input),
|
|
4812
|
-
formatMessage: options.createFormatMessage?.() ?? options.formatMessage,
|
|
4813
|
-
updateRunConfig: options.updateRunConfig,
|
|
4814
|
-
secureChannel: options.security
|
|
4815
|
-
}
|
|
4816
|
-
})
|
|
4817
|
-
}) }, ctx.resolvers);
|
|
4818
|
-
}
|
|
4819
|
-
getRouteInfo(input) {
|
|
4820
|
-
if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
|
|
4821
|
-
return {
|
|
4822
|
-
carrierLabel: this.options.label ?? "link",
|
|
4823
|
-
summary: this.options.label ?? "link"
|
|
4824
|
-
};
|
|
4825
|
-
}
|
|
4826
|
-
};
|
|
4761
|
+
function wsCarrier(url, options = {}) {
|
|
4762
|
+
return {
|
|
4763
|
+
carrierLabel: "ws",
|
|
4764
|
+
open: (input) => webSocketByteChannel(options.createWebSocket?.(input) ?? defaultWebSocket(url)),
|
|
4765
|
+
getCacheKey: options.getTransportCacheKey ?? (() => [url]),
|
|
4766
|
+
getRouteInfo: options.getRouteInfo ?? (() => ({
|
|
4767
|
+
carrierLabel: "ws",
|
|
4768
|
+
url,
|
|
4769
|
+
summary: `ws ${shortWs(url)}`
|
|
4770
|
+
}))
|
|
4771
|
+
};
|
|
4772
|
+
}
|
|
4773
|
+
function defaultWebSocket(url) {
|
|
4774
|
+
const ws = new WebSocket(url);
|
|
4775
|
+
ws.binaryType = "arraybuffer";
|
|
4776
|
+
return ws;
|
|
4777
|
+
}
|
|
4827
4778
|
//#endregion
|
|
4828
|
-
//#region src/ActionRuntime/Transport/Carrier/
|
|
4779
|
+
//#region src/ActionRuntime/Transport/Carrier/exchange/http/httpAcceptorCarrier.ts
|
|
4829
4780
|
/**
|
|
4830
|
-
*
|
|
4831
|
-
*
|
|
4832
|
-
*
|
|
4781
|
+
* An HTTP {@link IExchangeAcceptorCarrier}: the accept-in dual of {@link httpCarrier}. It serves the
|
|
4782
|
+
* secure exchange protocol (handshake → token session → encrypted frames) over web-standard
|
|
4783
|
+
* `Request`/`Response`. The crypto identity, runtime coordinate, dictionary version, and accepted security
|
|
4784
|
+
* levels are all supplied centrally by `serveChannel`, so this only needs to say which requests carry an
|
|
4785
|
+
* action envelope and how to answer CORS.
|
|
4833
4786
|
*/
|
|
4834
|
-
function
|
|
4835
|
-
return
|
|
4787
|
+
function httpAcceptorCarrier(options = {}) {
|
|
4788
|
+
return {
|
|
4789
|
+
shape: "exchange",
|
|
4790
|
+
carrierLabel: options.carrierLabel ?? "http",
|
|
4791
|
+
secure: options.secure,
|
|
4792
|
+
isActionPath: options.isActionPath,
|
|
4793
|
+
cors: options.cors,
|
|
4794
|
+
useErrorStatus: options.useErrorStatus
|
|
4795
|
+
};
|
|
4836
4796
|
}
|
|
4837
4797
|
//#endregion
|
|
4838
|
-
//#region src/ActionRuntime/Transport/
|
|
4839
|
-
function
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4798
|
+
//#region src/ActionRuntime/Transport/Carrier/exchange/http/httpCarrier.ts
|
|
4799
|
+
function shortPath(url) {
|
|
4800
|
+
try {
|
|
4801
|
+
return new URL(url).pathname || url;
|
|
4802
|
+
} catch {
|
|
4803
|
+
return url;
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
/**
|
|
4807
|
+
* An HTTP {@link IExchangeCarrierSource}: each `exchange` POSTs one frame body to the action endpoint and
|
|
4808
|
+
* resolves with the response body as the single correlated reply. Hand it to {@link secureTransport} —
|
|
4809
|
+
* HTTP then runs the *same* secure session as a duplex carrier (handshake → token → encrypted frames),
|
|
4810
|
+
* the request/reply correlation provided for free by the HTTP transaction.
|
|
4811
|
+
*
|
|
4812
|
+
* `createRequest` derives the URL/headers per action (keep it simple with `() => ({ url })`). The body is
|
|
4813
|
+
* the session's responsibility, so it is never built here.
|
|
4814
|
+
*/
|
|
4815
|
+
function httpCarrier(createRequest, options = {}) {
|
|
4816
|
+
const doFetch = options.fetch ?? fetch;
|
|
4817
|
+
return {
|
|
4818
|
+
shape: "exchange",
|
|
4819
|
+
carrierLabel: "http",
|
|
4820
|
+
open: (input) => {
|
|
4821
|
+
const request = createRequest(input);
|
|
4822
|
+
return {
|
|
4823
|
+
label: request.url,
|
|
4824
|
+
exchange: async (frame, opts) => {
|
|
4825
|
+
return await (await doFetch(request.url, {
|
|
4826
|
+
method: "POST",
|
|
4827
|
+
headers: {
|
|
4828
|
+
"Content-Type": "application/json",
|
|
4829
|
+
...request.headers
|
|
4830
|
+
},
|
|
4831
|
+
body: typeof frame === "string" ? frame : new Uint8Array(frame),
|
|
4832
|
+
signal: opts?.signal
|
|
4833
|
+
})).text();
|
|
4834
|
+
}
|
|
4835
|
+
};
|
|
4836
|
+
},
|
|
4837
|
+
getCacheKey: options.getTransportCacheKey ?? ((input) => [createRequest(input).url]),
|
|
4838
|
+
getRouteInfo: options.getRouteInfo ?? ((input) => {
|
|
4839
|
+
const { url } = createRequest(input);
|
|
4840
|
+
return {
|
|
4841
|
+
carrierLabel: "http",
|
|
4842
|
+
method: "POST",
|
|
4843
|
+
url,
|
|
4844
|
+
summary: `POST ${shortPath(url)}`
|
|
4845
|
+
};
|
|
4846
|
+
})
|
|
4847
|
+
};
|
|
4859
4848
|
}
|
|
4860
4849
|
//#endregion
|
|
4861
|
-
//#region src/ActionRuntime/Transport/
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4850
|
+
//#region src/ActionRuntime/Transport/codec/createBinaryWireAdapter.ts
|
|
4851
|
+
/**
|
|
4852
|
+
* Positional layout of the stateless binary envelope. A flat tuple (rather than an object) strips the
|
|
4853
|
+
* repeated `domain`/`id`/`form`/`type` and context key names from every frame, and we carry only the
|
|
4854
|
+
* context fields the receiver can't recompute: `cuid` (correlation) and `originClient` (return
|
|
4855
|
+
* routing).
|
|
4856
|
+
*
|
|
4857
|
+
* [ routeInt, typeInt, time, cuid, originClient, payloadData ]
|
|
4858
|
+
*
|
|
4859
|
+
* Dropped vs the JSON wire: `form`/`type` strings, `inputHash`/`outputHash` (recomputed on hydrate),
|
|
4860
|
+
* `context.timeCreated` (reconstructed from `time`) and `context.routing` (rebuilt empty — the
|
|
4861
|
+
* receiver re-stamps its own route items as it handles the action). For the leanest possible frames
|
|
4862
|
+
* (integer correlation, identity dropped after a handshake), use `createBinaryWireSessionFactory`.
|
|
4863
|
+
*/
|
|
4864
|
+
const ENVELOPE = {
|
|
4865
|
+
route: 0,
|
|
4866
|
+
type: 1,
|
|
4867
|
+
time: 2,
|
|
4868
|
+
cuid: 3,
|
|
4869
|
+
originClient: 4,
|
|
4870
|
+
payload: 5
|
|
4871
|
+
};
|
|
4872
|
+
const ENVELOPE_LENGTH = 6;
|
|
4873
|
+
/**
|
|
4874
|
+
* Builds a *stateless* `formatMessage` pipeline for {@link LinkTransport}, packing action
|
|
4875
|
+
* payloads into a compact msgpackr binary frame instead of JSON. The `domain`/`id` route collapses to
|
|
4876
|
+
* a single integer drawn from a shared dictionary; `form`/`type`, the recomputable
|
|
4877
|
+
* `inputHash`/`outputHash`, and the per-frame `context.routing`/`context.timeCreated` are all dropped
|
|
4878
|
+
* (see {@link ENVELOPE}).
|
|
4879
|
+
*
|
|
4880
|
+
* No validation runs here: `incoming` blindly reconstructs the wire JSON shape and hands it back to
|
|
4881
|
+
* the connection, which flows into `ActionRuntime` → `domain.hydrateAnyAction()` where the Valibot
|
|
4882
|
+
* schemas validate it exactly as they would for a JSON frame.
|
|
4883
|
+
*
|
|
4884
|
+
* Both ends of the socket MUST construct the adapter with the same domains in the same order — the
|
|
4885
|
+
* integer dictionary is positional. Mismatched dictionaries will route to the wrong action.
|
|
4886
|
+
*
|
|
4887
|
+
* Because `incoming` returns `undefined` for text frames, a binary server can still serve plain-JSON
|
|
4888
|
+
* clients on the same runtime (the connection falls back to its built-in JSON parser).
|
|
4889
|
+
*/
|
|
4890
|
+
function createBinaryWireAdapter(domains) {
|
|
4891
|
+
const { routeToInt, intToRoute } = buildActionRouteDictionary(domains);
|
|
4892
|
+
return {
|
|
4893
|
+
outgoing: (input) => {
|
|
4894
|
+
const json = input.action.toJsonObject();
|
|
4895
|
+
const routeKey = `${json.domain}:${json.id}`;
|
|
4896
|
+
const routeInt = routeToInt.get(routeKey);
|
|
4897
|
+
if (routeInt == null) throw new Error(`[binary-wire] Cannot pack unregistered action route: ${routeKey}`);
|
|
4898
|
+
const envelope = new Array(ENVELOPE_LENGTH);
|
|
4899
|
+
envelope[ENVELOPE.route] = routeInt;
|
|
4900
|
+
envelope[ENVELOPE.type] = PayloadTypeToInt[json.type];
|
|
4901
|
+
envelope[ENVELOPE.time] = json.time;
|
|
4902
|
+
envelope[ENVELOPE.cuid] = json.context.cuid;
|
|
4903
|
+
envelope[ENVELOPE.originClient] = json.context.originClient;
|
|
4904
|
+
envelope[ENVELOPE.payload] = extractWirePayload(json);
|
|
4905
|
+
return pack(envelope);
|
|
4906
|
+
},
|
|
4907
|
+
incoming: (frame) => {
|
|
4908
|
+
let buffer;
|
|
4909
|
+
if (frame instanceof ArrayBuffer) buffer = new Uint8Array(frame);
|
|
4910
|
+
else if (frame instanceof Uint8Array) buffer = frame;
|
|
4911
|
+
else return;
|
|
4912
|
+
try {
|
|
4913
|
+
const envelope = unpack(buffer);
|
|
4914
|
+
if (!Array.isArray(envelope) || envelope.length !== ENVELOPE_LENGTH) return void 0;
|
|
4915
|
+
const routeMeta = intToRoute[envelope[ENVELOPE.route]];
|
|
4916
|
+
const payloadType = ReversePayloadType[envelope[ENVELOPE.type]];
|
|
4917
|
+
if (routeMeta == null || payloadType == null) return void 0;
|
|
4918
|
+
const time = envelope[ENVELOPE.time];
|
|
4919
|
+
return assembleWireJson(routeMeta, payloadType, time, {
|
|
4920
|
+
cuid: envelope[ENVELOPE.cuid],
|
|
4921
|
+
timeCreated: time,
|
|
4922
|
+
routing: [],
|
|
4923
|
+
originClient: envelope[ENVELOPE.originClient]
|
|
4924
|
+
}, envelope[ENVELOPE.payload]);
|
|
4925
|
+
} catch (e) {
|
|
4926
|
+
console.error("[binary-wire] Failed to unpack binary action frame", e);
|
|
4927
|
+
return;
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4869
4930
|
};
|
|
4870
|
-
const carrier = options.carrier;
|
|
4871
|
-
if (isExchangeCarrierSource(carrier)) return ExchangeTransport.create({
|
|
4872
|
-
openCarrier: carrier.open,
|
|
4873
|
-
getTransportCacheKey: carrier.getCacheKey,
|
|
4874
|
-
available: options.available,
|
|
4875
|
-
getRouteInfo: carrier.getRouteInfo,
|
|
4876
|
-
label: carrier.carrierLabel,
|
|
4877
|
-
security
|
|
4878
|
-
});
|
|
4879
|
-
return LinkTransport.create({
|
|
4880
|
-
openChannel: carrier.open,
|
|
4881
|
-
createFormatMessage: options.channel.createCodec,
|
|
4882
|
-
getTransportCacheKey: carrier.getCacheKey,
|
|
4883
|
-
available: options.available,
|
|
4884
|
-
getRouteInfo: carrier.getRouteInfo,
|
|
4885
|
-
label: carrier.carrierLabel,
|
|
4886
|
-
security
|
|
4887
|
-
});
|
|
4888
4931
|
}
|
|
4889
4932
|
//#endregion
|
|
4890
|
-
export { AcceptorHandler, ActionCore, ActionDomain, ActionLocalHandler, ActionRootDomain, ActionRuntime, ActionSchema, ConnectionStateStore, ConnectorHandler, EActionPayloadType, EActionProgressType, EActionResponseMode, EErrId_NiceAction, EErrId_NiceTransport, EErrId_NiceTransport_WebSocket, EHandshakeMessageType, ERunningActionFinishedType, ERunningActionState, ERunningActionUpdateType, ESecurityLevel, ETransportShape, ETransportStatus, ExchangeAcceptor, ExchangeTransport, LinkTransport, PeerLinkHandler, RunningAction, RuntimeCoordinate, Transport, acceptChannel, acceptChannelConnections, actionSchema, connectChannel, createAcceptorHandler, createActionFetchHandler, createActionFrameCrypto, createActionRootDomain, createBinaryWireAdapter, createBinaryWireSessionFactory, createClientHandshake, createConnectionStateStore, createConnectorHandler, createHibernatableWsServerAdapter, createInMemoryChannelPair, createInMemoryTofuVerifyKeyResolver, createLocalHandler, createSecureAcceptorHandler, createServerHandshake, createStorageTofuVerifyKeyResolver, decodeActionFrame, decodeExchangeReply, decodeExchangeRequest, decodeHandshakeMessage, defineChannel, defineSecureChannel, encodeExchange, encodeHandshakeMessage, err_nice_action, err_nice_external_client, err_nice_transport, err_nice_transport_ws, httpAcceptorCarrier, httpCarrier, inMemoryCarrier, isActionPayload_Any_JsonObject, isActionPayload_Request_JsonObject, isActionPayload_Result_JsonObject, isExchangeAcceptorCarrier,
|
|
4933
|
+
export { AcceptorHandler, ActionCore, ActionDomain, ActionLocalHandler, ActionRootDomain, ActionRuntime, ActionSchema, ConnectionStateStore, ConnectorHandler, EActionPayloadType, EActionProgressType, EActionResponseMode, EErrId_NiceAction, EErrId_NiceTransport, EErrId_NiceTransport_WebSocket, EHandshakeMessageType, ERunningActionFinishedType, ERunningActionState, ERunningActionUpdateType, ESecurityLevel, ETransportShape, ETransportStatus, ExchangeAcceptor, ExchangeTransport, LinkTransport, PeerLinkHandler, RunningAction, RuntimeCoordinate, Transport, acceptChannel, acceptChannelConnections, actionSchema, connectChannel, createAcceptorHandler, createActionFetchHandler, createActionFrameCrypto, createActionRootDomain, createBinaryWireAdapter, createBinaryWireSessionFactory, createClientHandshake, createConnectionStateStore, createConnectorHandler, createHibernatableWsServerAdapter, createInMemoryChannelPair, createInMemoryTofuVerifyKeyResolver, createLocalHandler, createSecureAcceptorHandler, createServerHandshake, createStorageTofuVerifyKeyResolver, decodeActionFrame, decodeExchangeReply, decodeExchangeRequest, decodeHandshakeMessage, defineChannel, defineSecureChannel, encodeExchange, encodeHandshakeMessage, err_nice_action, err_nice_external_client, err_nice_transport, err_nice_transport_ws, httpAcceptorCarrier, httpCarrier, inMemoryCarrier, isActionPayload_Any_JsonObject, isActionPayload_Request_JsonObject, isActionPayload_Result_JsonObject, isExchangeAcceptorCarrier, rtcCarrier, rtcDataChannelByteChannel, runtimeLinkId, serveChannel, wsAcceptorCarrier, wsCarrier };
|
|
4891
4934
|
|
|
4892
4935
|
//# sourceMappingURL=index.mjs.map
|