socketduct 0.1.15 → 0.1.17
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/.env.example +3 -0
- package/changelog.d/20260916165820-fix-deferred-socket-callback-guard.md +1 -0
- package/changelog.d/20260924120000-wan-route-classes.md +1 -0
- package/docs/architecture.md +16 -3
- package/docs/protocol.md +17 -5
- package/docs/reverse-gateway.md +68 -37
- package/docs/security.md +13 -0
- package/docs/socket-api.md +50 -21
- package/package.json +1 -1
- package/src/client/deferred-virtual-socket.js +18 -6
- package/src/client/multiplexed-transport-set.js +105 -65
- package/src/config/environment.js +24 -1
- package/src/reverse/client.js +31 -17
- package/src/reverse/common.js +1 -0
- package/src/reverse/hub.js +163 -32
- package/src/reverse/runtime.js +31 -6
- package/test/e2e/reverse-gateway-process.test.js +93 -0
- package/test/integration/reverse-route-class.test.js +373 -0
- package/test/unit/client/deferred-virtual-socket.test.js +170 -0
- package/test/unit/client/multiplexed-transport-set.test.js +42 -0
- package/test/unit/config/docker-gateway-files.test.js +3 -1
- package/test/unit/config/environment.test.js +7 -1
|
@@ -7,6 +7,7 @@ import {connect as tlsConnect} from "node:tls"
|
|
|
7
7
|
|
|
8
8
|
import {errorDiagnostics, logDiagnostic} from "../logging/structured-logger.js"
|
|
9
9
|
import {ReverseGatewayConnection} from "../reverse/client.js"
|
|
10
|
+
import {ROUTE_CLASS} from "../reverse/common.js"
|
|
10
11
|
import {DeferredVirtualSocket} from "./deferred-virtual-socket.js"
|
|
11
12
|
import {MultiplexedSocketductConnection} from "./multiplexed-connection.js"
|
|
12
13
|
|
|
@@ -26,10 +27,14 @@ const GATEWAY_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$/
|
|
|
26
27
|
* @typedef {object} GatewayConfiguration
|
|
27
28
|
* @property {string} gatewayId - Stable, privacy-safe gateway identity.
|
|
28
29
|
* @property {GatewayEndpoint} endpoint - Explicit gateway-pinned endpoint/factory.
|
|
30
|
+
* @typedef {object} RouteClassConfiguration
|
|
31
|
+
* @property {string} routeClass - Stable WAN route-class identity.
|
|
32
|
+
* @property {GatewayEndpoint} endpoint - Shared reverse-hub endpoint/factory.
|
|
29
33
|
* @typedef {object} TransportSetOptions
|
|
30
34
|
* @property {string} connectionId - Stable process/base connection identity.
|
|
31
35
|
* @property {string} [spoolDirectory] - Base directory for derived gateway spool namespaces.
|
|
32
|
-
* @property {GatewayConfiguration[]} gateways - Explicit configured gateways.
|
|
36
|
+
* @property {GatewayConfiguration[]} [gateways] - Explicit configured exact gateways.
|
|
37
|
+
* @property {RouteClassConfiguration[]} [routeClasses] - Explicit configured route-class members.
|
|
33
38
|
* @property {number} [minimumReadyMembers] - Required ready members for start(), default 1.
|
|
34
39
|
* @property {number} [maxTotalBufferBytes] - Set-wide retained data limit.
|
|
35
40
|
* @property {number} [maxTotalSpoolBytes] - Set-wide durable spool data limit.
|
|
@@ -58,9 +63,10 @@ const GATEWAY_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$/
|
|
|
58
63
|
* @property {() => unknown} [ref] - Reference resources.
|
|
59
64
|
* @property {() => unknown} [unref] - Unreference resources.
|
|
60
65
|
* @typedef {object} TransportSetDependencies
|
|
61
|
-
* @property {(options: import('./multiplexed-connection.js').MultiplexedConnectionOptions | import('../reverse/client.js').ReverseGatewayConnectionOptions,
|
|
66
|
+
* @property {(options: import('./multiplexed-connection.js').MultiplexedConnectionOptions | import('../reverse/client.js').ReverseGatewayConnectionOptions, memberIdentity: string) => TransportMemberConnection} [createMember] - Narrow test dependency.
|
|
62
67
|
* @typedef {object} TransportMember
|
|
63
|
-
* @property {string} gatewayId - Stable gateway ID.
|
|
68
|
+
* @property {string | undefined} gatewayId - Stable exact gateway ID.
|
|
69
|
+
* @property {string | undefined} routeClass - Stable route-class identity.
|
|
64
70
|
* @property {TransportMemberConnection} connection - Single-lane owner.
|
|
65
71
|
* @property {number} assignments - Virtual sockets assigned for life.
|
|
66
72
|
* @property {Error | undefined} startupError - Terminal startup error.
|
|
@@ -69,8 +75,8 @@ const GATEWAY_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$/
|
|
|
69
75
|
* @typedef {object} PendingOpen
|
|
70
76
|
* @property {import('./multiplexed-connection.js').VirtualConnectionOptions} options - Validated caller options.
|
|
71
77
|
* @property {DeferredVirtualSocket} socket - Immediate app-facing socket.
|
|
72
|
-
* @typedef {import('./virtual-socket.js').VirtualSocket & {readonly gatewayId: string | undefined}} GatewayVirtualSocket
|
|
73
|
-
* @typedef {import('node:tls').TLSSocket & {readonly gatewayId: string | undefined}} GatewayTlsSocket
|
|
78
|
+
* @typedef {import('./virtual-socket.js').VirtualSocket & {readonly gatewayId: string | undefined, readonly routeClass: string | undefined}} GatewayVirtualSocket
|
|
79
|
+
* @typedef {import('node:tls').TLSSocket & {readonly gatewayId: string | undefined, readonly routeClass: string | undefined}} GatewayTlsSocket
|
|
74
80
|
*/
|
|
75
81
|
|
|
76
82
|
/** No configured gateway is currently safe for a new virtual stream. */
|
|
@@ -239,11 +245,12 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
239
245
|
this.#maxPendingOpens = validated.maxPendingOpens
|
|
240
246
|
this.#budget = new AggregateTransportBudget(validated.limits)
|
|
241
247
|
this.#logger = options.logger
|
|
242
|
-
const createMember = dependencies.createMember ?? ((memberOptions) => "
|
|
248
|
+
const createMember = dependencies.createMember ?? ((memberOptions) => "hub" in memberOptions
|
|
243
249
|
? new ReverseGatewayConnection(memberOptions)
|
|
244
250
|
: new MultiplexedSocketductConnection(memberOptions))
|
|
245
|
-
this.#members = validated.
|
|
246
|
-
const
|
|
251
|
+
this.#members = validated.members.map(({gatewayId, routeClass, endpoint}) => {
|
|
252
|
+
const memberIdentity = /** @type {string} */ (gatewayId ?? routeClass)
|
|
253
|
+
const identity = gatewayId === undefined ? deriveRouteClassIdentity(options.connectionId, memberIdentity) : deriveGatewayIdentity(options.connectionId, gatewayId)
|
|
247
254
|
const memberOptions = endpoint.reverseHub === undefined
|
|
248
255
|
? /** @type {import('./multiplexed-connection.js').MultiplexedConnectionOptions} */ ({
|
|
249
256
|
...validated.memberOptions,
|
|
@@ -258,7 +265,7 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
258
265
|
})
|
|
259
266
|
: /** @type {import('../reverse/client.js').ReverseGatewayConnectionOptions} */ (/** @type {unknown} */ ({
|
|
260
267
|
...validated.memberOptions,
|
|
261
|
-
gatewayId,
|
|
268
|
+
...(gatewayId === undefined ? {routeClass} : {gatewayId}),
|
|
262
269
|
hub: endpoint.reverseHub,
|
|
263
270
|
tls: endpoint.tls,
|
|
264
271
|
...(endpoint.createPhysicalSocket === undefined ? {} : {createPhysicalSocket: endpoint.createPhysicalSocket}),
|
|
@@ -267,10 +274,10 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
267
274
|
aggregateBudget: this.#budget,
|
|
268
275
|
...(this.#logger === undefined ? {} : {logger: this.#logger})
|
|
269
276
|
}))
|
|
270
|
-
const connection = /** @type {TransportMemberConnection} */ (createMember(memberOptions,
|
|
277
|
+
const connection = /** @type {TransportMemberConnection} */ (createMember(memberOptions, memberIdentity))
|
|
271
278
|
validateMember(connection)
|
|
272
279
|
/** @type {TransportMember} */
|
|
273
|
-
const member = {gatewayId, connection, assignments: 0, startupError: undefined, terminalError: undefined, lastReadyGeneration: undefined}
|
|
280
|
+
const member = {gatewayId, routeClass, connection, assignments: 0, startupError: undefined, terminalError: undefined, lastReadyGeneration: undefined}
|
|
274
281
|
connection.on("stateChange", (state, error) => this.#memberStateChanged(member, state, error))
|
|
275
282
|
return member
|
|
276
283
|
})
|
|
@@ -321,8 +328,8 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
321
328
|
if (selection === undefined) return this.#deferConnection(options, socketOptions, callback)
|
|
322
329
|
try {
|
|
323
330
|
/** @type {((error: Error | null, socket: import('./virtual-socket.js').VirtualSocket) => void) | undefined} */
|
|
324
|
-
const memberCallback = callback === undefined ? undefined : (error, socket) => callback(error,
|
|
325
|
-
const socket =
|
|
331
|
+
const memberCallback = callback === undefined ? undefined : (error, socket) => callback(error, exposeAllocation(socket, memberAllocation(selection.member)))
|
|
332
|
+
const socket = exposeAllocation(/** @type {import('./virtual-socket.js').VirtualSocket} */ (selection.member.connection.createConnection(options, memberCallback)), memberAllocation(selection.member))
|
|
326
333
|
selection.member.assignments += 1
|
|
327
334
|
this.#virtualOpens += 1
|
|
328
335
|
return socket
|
|
@@ -405,9 +412,10 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
405
412
|
if (member === undefined) throw new Error(`Unknown gatewayId: ${options.gatewayId}`)
|
|
406
413
|
const virtualOptions = {...options}
|
|
407
414
|
delete /** @type {{gatewayId?: string}} */ (virtualOptions).gatewayId
|
|
415
|
+
const allocation = memberAllocation(member)
|
|
408
416
|
/** @type {((error: Error | null, socket: import('./virtual-socket.js').VirtualSocket) => void) | undefined} */
|
|
409
|
-
const memberCallback = callback === undefined ? undefined : (error, socket) => callback(error,
|
|
410
|
-
const socket =
|
|
417
|
+
const memberCallback = callback === undefined ? undefined : (error, socket) => callback(error, exposeAllocation(socket, allocation))
|
|
418
|
+
const socket = exposeAllocation(/** @type {import('./virtual-socket.js').VirtualSocket} */ (member.connection.registerRecoveredConnection({...virtualOptions, recover: true}, memberCallback)), allocation)
|
|
411
419
|
member.assignments += 1
|
|
412
420
|
this.#virtualOpens += 1
|
|
413
421
|
return socket
|
|
@@ -425,7 +433,7 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
425
433
|
const raw = this.createConnection(virtualOptions)
|
|
426
434
|
const tlsOptions = omitVirtualOptions(options)
|
|
427
435
|
const secure = tlsConnect({...tlsOptions, socket: raw})
|
|
428
|
-
const exposed =
|
|
436
|
+
const exposed = exposeAllocationGetters(secure, () => raw.gatewayId, () => raw.routeClass)
|
|
429
437
|
if (callback !== undefined) installTlsCallback(exposed, callback)
|
|
430
438
|
return exposed
|
|
431
439
|
}
|
|
@@ -442,12 +450,12 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
442
450
|
return this
|
|
443
451
|
}
|
|
444
452
|
|
|
445
|
-
/** @returns {{state: string, configuredMembers: number, connectingMembers: number, readyMembers: number, degradedMembers: number, closedMembers: number, minimumReadyMembers: number, pendingOpens: number, maxPendingOpens: number, virtualOpens: number, physicalConnects: number, reconnectAttempts: number, budgets: {dataBytes: number, spoolBytes: number, queuedBytes: number, maxDataBytes: number, maxSpoolBytes: number, maxQueuedBytes: number}, members: Array<{gatewayId
|
|
453
|
+
/** @returns {{state: string, configuredMembers: number, connectingMembers: number, readyMembers: number, degradedMembers: number, closedMembers: number, minimumReadyMembers: number, pendingOpens: number, maxPendingOpens: number, virtualOpens: number, physicalConnects: number, reconnectAttempts: number, budgets: {dataBytes: number, spoolBytes: number, queuedBytes: number, maxDataBytes: number, maxSpoolBytes: number, maxQueuedBytes: number}, members: Array<{gatewayId?: string, routeClass?: string, state: string, assignments: number, physicalConnects: number, reconnectAttempts: number, reconnectSuccesses: number, virtualStreams: number, queuedBytes: number, spoolBytes: number, spoolCleanupFailures: number, terminalErrorCode: string | null}>}} Privacy-safe aggregate and per-member status. */
|
|
446
454
|
status() {
|
|
447
455
|
const members = this.#members.map((member) => {
|
|
448
456
|
const status = member.connection.status()
|
|
449
457
|
return {
|
|
450
|
-
|
|
458
|
+
...memberAllocation(member),
|
|
451
459
|
state: status.physicalState,
|
|
452
460
|
assignments: member.assignments,
|
|
453
461
|
physicalConnects: status.physicalConnects,
|
|
@@ -544,7 +552,7 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
544
552
|
if (member !== undefined && typeof state === "string") {
|
|
545
553
|
const status = member.connection.status()
|
|
546
554
|
logDiagnostic(this.#logger, "transport_set.member_state", {
|
|
547
|
-
|
|
555
|
+
...memberAllocation(member),
|
|
548
556
|
state,
|
|
549
557
|
generation: status.connectionGeneration,
|
|
550
558
|
physicalConnects: status.physicalConnects,
|
|
@@ -553,7 +561,7 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
553
561
|
})
|
|
554
562
|
if (state === "ready" && member.lastReadyGeneration !== undefined && status.connectionGeneration > member.lastReadyGeneration) {
|
|
555
563
|
logDiagnostic(this.#logger, "transport_set.member_replaced", {
|
|
556
|
-
|
|
564
|
+
...memberAllocation(member),
|
|
557
565
|
generation: status.connectionGeneration,
|
|
558
566
|
physicalConnects: status.physicalConnects
|
|
559
567
|
})
|
|
@@ -605,8 +613,9 @@ export class MultiplexedSocketductTransportSet extends EventEmitter {
|
|
|
605
613
|
/** @type {import('./virtual-socket.js').VirtualSocket | undefined} */
|
|
606
614
|
let memberSocket
|
|
607
615
|
try {
|
|
608
|
-
|
|
609
|
-
|
|
616
|
+
const allocation = memberAllocation(selection.member)
|
|
617
|
+
memberSocket = exposeAllocation(selection.member.connection.createConnection(pending.options), allocation)
|
|
618
|
+
if (!pending.socket.assign(memberSocket, allocation)) {
|
|
610
619
|
this.#cursor = selection.previousCursor
|
|
611
620
|
continue
|
|
612
621
|
}
|
|
@@ -682,9 +691,13 @@ function errorClassification(error, fallback) {
|
|
|
682
691
|
function validateTransportSetOptions(options) {
|
|
683
692
|
if (typeof options !== "object" || options === null) throw new TypeError("Socketduct transport-set options are required")
|
|
684
693
|
if (typeof options.connectionId !== "string" || !STABLE_IDENTIFIER.test(options.connectionId)) throw new TypeError("Transport-set connectionId must be 8-128 URL-safe opaque characters")
|
|
685
|
-
|
|
694
|
+
const hasGateways = options.gateways !== undefined
|
|
695
|
+
const hasRouteClasses = options.routeClasses !== undefined
|
|
696
|
+
if (hasGateways === hasRouteClasses) throw new TypeError("Transport set requires exactly one non-empty gateways or routeClasses array")
|
|
697
|
+
const configuredMembers = hasGateways ? options.gateways : options.routeClasses
|
|
698
|
+
if (!Array.isArray(configuredMembers) || configuredMembers.length === 0) throw new TypeError("Transport-set members must be a non-empty array")
|
|
686
699
|
const minimumReadyMembers = options.minimumReadyMembers === undefined ? 1 : options.minimumReadyMembers
|
|
687
|
-
if (!Number.isSafeInteger(minimumReadyMembers) || minimumReadyMembers < 1 || minimumReadyMembers >
|
|
700
|
+
if (!Number.isSafeInteger(minimumReadyMembers) || minimumReadyMembers < 1 || minimumReadyMembers > configuredMembers.length) throw new RangeError("minimumReadyMembers must be between 1 and the configured member count")
|
|
688
701
|
const maxPendingOpens = positiveLimit(options.maxPendingOpens, DEFAULT_MAX_PENDING_OPENS, "maxPendingOpens")
|
|
689
702
|
const limits = {
|
|
690
703
|
maxDataBytes: positiveLimit(options.maxTotalBufferBytes, DEFAULT_TOTAL_BUFFER_BYTES, "maxTotalBufferBytes"),
|
|
@@ -693,38 +706,42 @@ function validateTransportSetOptions(options) {
|
|
|
693
706
|
}
|
|
694
707
|
const memberOptions = options.memberOptions === undefined ? {} : options.memberOptions
|
|
695
708
|
if (typeof memberOptions !== "object" || memberOptions === null || Array.isArray(memberOptions)) throw new TypeError("memberOptions must be an object")
|
|
696
|
-
for (const forbidden of ["relay", "hub", "gatewayId", "tls", "createPhysicalSocket", "connectionId", "spoolDirectory", "continuousReconnect", "aggregateBudget", "logger"]) {
|
|
709
|
+
for (const forbidden of ["relay", "hub", "gatewayId", "routeClass", "tls", "createPhysicalSocket", "connectionId", "spoolDirectory", "continuousReconnect", "aggregateBudget", "logger"]) {
|
|
697
710
|
if (Object.hasOwn(memberOptions, forbidden)) throw new TypeError(`memberOptions.${forbidden} is owned by the transport set`)
|
|
698
711
|
}
|
|
699
|
-
/** @type {
|
|
700
|
-
const
|
|
701
|
-
const
|
|
702
|
-
for (const raw of
|
|
703
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new TypeError("Each
|
|
712
|
+
/** @type {Array<{gatewayId: string | undefined, routeClass: string | undefined, endpoint: GatewayEndpoint}>} */
|
|
713
|
+
const members = []
|
|
714
|
+
const memberIds = new Set()
|
|
715
|
+
for (const raw of configuredMembers) {
|
|
716
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new TypeError("Each transport-set member must be an object")
|
|
704
717
|
for (const key of Object.keys(raw)) {
|
|
705
|
-
|
|
718
|
+
const identityKey = hasGateways ? "gatewayId" : "routeClass"
|
|
719
|
+
if (key !== identityKey && key !== "endpoint") throw new TypeError(`Unknown transport-set member field ${key}; identity and endpoint must be explicit`)
|
|
706
720
|
}
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (
|
|
712
|
-
|
|
721
|
+
const member = /** @type {Partial<GatewayConfiguration & RouteClassConfiguration>} */ (raw)
|
|
722
|
+
const memberId = hasGateways ? member.gatewayId : member.routeClass
|
|
723
|
+
const validMemberId = hasGateways ? typeof memberId === "string" && GATEWAY_ID.test(memberId) : typeof memberId === "string" && ROUTE_CLASS.test(memberId)
|
|
724
|
+
if (!validMemberId) throw new TypeError(hasGateways ? "gatewayId must be 2-64 URL-safe characters and cannot contain path separators" : "routeClass must be 1-64 URL-safe characters and cannot contain path separators")
|
|
725
|
+
if (memberIds.has(memberId)) throw new TypeError(`Duplicate ${hasGateways ? "gatewayId" : "routeClass"}: ${memberId}`)
|
|
726
|
+
memberIds.add(memberId)
|
|
727
|
+
if (typeof member.endpoint !== "object" || member.endpoint === null || Array.isArray(member.endpoint)) throw new TypeError(`Transport-set member ${memberId} requires an explicit endpoint`)
|
|
728
|
+
const endpoint = /** @type {Partial<GatewayEndpoint>} */ (member.endpoint)
|
|
713
729
|
for (const key of Object.keys(endpoint)) {
|
|
714
730
|
if (key !== "relay" && key !== "reverseHub" && key !== "tls" && key !== "createPhysicalSocket") throw new TypeError(`Unknown endpoint field: ${key}`)
|
|
715
731
|
}
|
|
716
|
-
if ((endpoint.relay === undefined) === (endpoint.reverseHub === undefined)) throw new TypeError(`
|
|
732
|
+
if ((endpoint.relay === undefined) === (endpoint.reverseHub === undefined)) throw new TypeError(`Transport-set member ${memberId} requires exactly one relay or reverseHub endpoint`)
|
|
733
|
+
if (hasRouteClasses && endpoint.reverseHub === undefined) throw new TypeError(`Route class ${memberId} requires a reverseHub endpoint`)
|
|
717
734
|
const selectedEndpoint = endpoint.relay ?? endpoint.reverseHub
|
|
718
|
-
if (typeof selectedEndpoint !== "object" || selectedEndpoint === null || typeof selectedEndpoint.host !== "string" || selectedEndpoint.host.length === 0 || !validPort(selectedEndpoint.port) || typeof selectedEndpoint.token !== "string" || selectedEndpoint.token.length === 0) throw new TypeError(`
|
|
719
|
-
if (endpoint.tls !== undefined && (typeof endpoint.tls !== "object" || endpoint.tls === null || Array.isArray(endpoint.tls))) throw new TypeError(`
|
|
720
|
-
if (endpoint.reverseHub !== undefined && endpoint.tls === undefined) throw new TypeError(`
|
|
721
|
-
if (endpoint.createPhysicalSocket !== undefined && typeof endpoint.createPhysicalSocket !== "function") throw new TypeError(`
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
for (let left = 0; left <
|
|
725
|
-
for (let right = left + 1; right <
|
|
726
|
-
const first = /** @type {
|
|
727
|
-
const second = /** @type {
|
|
735
|
+
if (typeof selectedEndpoint !== "object" || selectedEndpoint === null || typeof selectedEndpoint.host !== "string" || selectedEndpoint.host.length === 0 || !validPort(selectedEndpoint.port) || typeof selectedEndpoint.token !== "string" || selectedEndpoint.token.length === 0) throw new TypeError(`Transport-set member ${memberId} endpoint host, port, and token are required`)
|
|
736
|
+
if (endpoint.tls !== undefined && (typeof endpoint.tls !== "object" || endpoint.tls === null || Array.isArray(endpoint.tls))) throw new TypeError(`Transport-set member ${memberId} tls must be an object`)
|
|
737
|
+
if (endpoint.reverseHub !== undefined && endpoint.tls === undefined) throw new TypeError(`Transport-set member ${memberId} reverseHub requires verified TLS settings`)
|
|
738
|
+
if (endpoint.createPhysicalSocket !== undefined && typeof endpoint.createPhysicalSocket !== "function") throw new TypeError(`Transport-set member ${memberId} createPhysicalSocket must be a function`)
|
|
739
|
+
members.push({gatewayId: hasGateways ? memberId : undefined, routeClass: hasRouteClasses ? memberId : undefined, endpoint: /** @type {GatewayEndpoint} */ (endpoint)})
|
|
740
|
+
}
|
|
741
|
+
for (let left = 0; left < members.length; left += 1) {
|
|
742
|
+
for (let right = left + 1; right < members.length; right += 1) {
|
|
743
|
+
const first = /** @type {{endpoint: GatewayEndpoint}} */ (members[left]).endpoint
|
|
744
|
+
const second = /** @type {{endpoint: GatewayEndpoint}} */ (members[right]).endpoint
|
|
728
745
|
if (first.relay === undefined || second.relay === undefined) continue
|
|
729
746
|
const sameAddress = first.relay.host === second.relay.host && first.relay.port === second.relay.port
|
|
730
747
|
const sameFactory = first.createPhysicalSocket === second.createPhysicalSocket
|
|
@@ -734,7 +751,7 @@ function validateTransportSetOptions(options) {
|
|
|
734
751
|
const defaultSpoolDirectory = join(tmpdir(), `socketduct-${typeof process.getuid === "function" ? process.getuid() : "user"}`, "multiplexed-set")
|
|
735
752
|
if (options.spoolDirectory !== undefined && (typeof options.spoolDirectory !== "string" || options.spoolDirectory.length === 0)) throw new TypeError("spoolDirectory must be a non-empty string")
|
|
736
753
|
return {
|
|
737
|
-
|
|
754
|
+
members,
|
|
738
755
|
minimumReadyMembers,
|
|
739
756
|
maxPendingOpens,
|
|
740
757
|
limits,
|
|
@@ -752,6 +769,15 @@ function deriveGatewayIdentity(baseId, gatewayId) {
|
|
|
752
769
|
return {connectionId: `gateway_${digest}`, spoolName: `gateway-${digest}`}
|
|
753
770
|
}
|
|
754
771
|
|
|
772
|
+
/**
|
|
773
|
+
* @param {string} baseId - Base identity.
|
|
774
|
+
* @param {string} routeClass - Route-class identity.
|
|
775
|
+
*/
|
|
776
|
+
function deriveRouteClassIdentity(baseId, routeClass) {
|
|
777
|
+
const digest = createHash("sha256").update("socketduct-v3-route-class\0").update(baseId).update("\0").update(routeClass).digest("base64url")
|
|
778
|
+
return {connectionId: `route_${digest}`, spoolName: `route-${digest}`}
|
|
779
|
+
}
|
|
780
|
+
|
|
755
781
|
/**
|
|
756
782
|
* @param {unknown} value - Raw limit.
|
|
757
783
|
* @param {number} defaultValue - Default.
|
|
@@ -771,8 +797,8 @@ function validateReservation(bytes) {
|
|
|
771
797
|
/** @param {unknown} options - Ordinary open options. */
|
|
772
798
|
function validateOrdinaryOpenOptions(options) {
|
|
773
799
|
if (typeof options !== "object" || options === null) throw new TypeError("Virtual connection options are required")
|
|
774
|
-
if (Object.hasOwn(options, "gatewayId") || /** @type {{recover?: unknown}} */ (options).recover === true) {
|
|
775
|
-
throw new TypeError("
|
|
800
|
+
if (Object.hasOwn(options, "gatewayId") || Object.hasOwn(options, "routeClass") || /** @type {{recover?: unknown}} */ (options).recover === true) {
|
|
801
|
+
throw new TypeError("Transport member selection is set-owned; gateway-bound durable recovery must use recoverConnection before start")
|
|
776
802
|
}
|
|
777
803
|
}
|
|
778
804
|
|
|
@@ -802,28 +828,42 @@ function validateMember(member) {
|
|
|
802
828
|
/**
|
|
803
829
|
* @template {object} T
|
|
804
830
|
* @param {T} socket - Selected member socket.
|
|
805
|
-
* @param {string}
|
|
806
|
-
* @returns {T & {readonly gatewayId: string}} Socket with immutable public allocation metadata.
|
|
831
|
+
* @param {{gatewayId: string} | {routeClass: string}} allocation - Stable configured member identity.
|
|
832
|
+
* @returns {T & {readonly gatewayId: string | undefined, readonly routeClass: string | undefined}} Socket with immutable public allocation metadata.
|
|
807
833
|
*/
|
|
808
|
-
function
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
834
|
+
function exposeAllocation(socket, allocation) {
|
|
835
|
+
const values = "gatewayId" in allocation
|
|
836
|
+
? {gatewayId: allocation.gatewayId, routeClass: undefined}
|
|
837
|
+
: {gatewayId: undefined, routeClass: allocation.routeClass}
|
|
838
|
+
for (const key of /** @type {const} */ (["gatewayId", "routeClass"])) {
|
|
839
|
+
const existing = Object.getOwnPropertyDescriptor(socket, key)
|
|
840
|
+
if (existing !== undefined) {
|
|
841
|
+
if (/** @type {Record<string, unknown>} */ (socket)[key] !== values[key]) throw new Error("Socketduct member allocation metadata mismatch")
|
|
842
|
+
continue
|
|
843
|
+
}
|
|
844
|
+
Object.defineProperty(socket, key, {value: values[key], enumerable: true, writable: false, configurable: false})
|
|
845
|
+
}
|
|
846
|
+
return /** @type {T & {readonly gatewayId: string | undefined, readonly routeClass: string | undefined}} */ (socket)
|
|
816
847
|
}
|
|
817
848
|
|
|
818
849
|
/**
|
|
819
850
|
* @template {object} T
|
|
820
851
|
* @param {T} socket - Socket whose assignment is owned by another socket.
|
|
821
|
-
* @param {() => string | undefined} gatewayId - Live
|
|
822
|
-
* @
|
|
852
|
+
* @param {() => string | undefined} gatewayId - Live exact-gateway assignment lookup.
|
|
853
|
+
* @param {() => string | undefined} routeClass - Live route-class assignment lookup.
|
|
854
|
+
* @returns {T & {readonly gatewayId: string | undefined, readonly routeClass: string | undefined}} Socket with immutable public allocation metadata.
|
|
823
855
|
*/
|
|
824
|
-
function
|
|
856
|
+
function exposeAllocationGetters(socket, gatewayId, routeClass) {
|
|
825
857
|
Object.defineProperty(socket, "gatewayId", {get: gatewayId, enumerable: true, configurable: false})
|
|
826
|
-
|
|
858
|
+
Object.defineProperty(socket, "routeClass", {get: routeClass, enumerable: true, configurable: false})
|
|
859
|
+
return /** @type {T & {readonly gatewayId: string | undefined, readonly routeClass: string | undefined}} */ (socket)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** @param {TransportMember} member - Configured member. @returns {{gatewayId: string} | {routeClass: string}} */
|
|
863
|
+
function memberAllocation(member) {
|
|
864
|
+
if (member.gatewayId !== undefined) return {gatewayId: member.gatewayId}
|
|
865
|
+
if (member.routeClass !== undefined) return {routeClass: member.routeClass}
|
|
866
|
+
throw new Error("Socketduct transport-set member identity is unavailable")
|
|
827
867
|
}
|
|
828
868
|
|
|
829
869
|
/**
|
|
@@ -64,6 +64,7 @@ const SUPPORTED_PROTOCOL_VERSIONS = new Set([2, 3])
|
|
|
64
64
|
* @property {RelayTlsConfig} [relayTls] - Optional TLS configuration.
|
|
65
65
|
* @property {Array<{gatewayId: string, username: string}>} [reverseGatewayBindings] - Hub gateway identity bindings.
|
|
66
66
|
* @property {Array<{username: string, gatewayId: string, host: string, port: number}>} [reverseRoutes] - Hub client route policy.
|
|
67
|
+
* @property {Array<{username: string, routeClass: string, gatewayId: string, host: string, port: number}>} [reverseRouteClassRoutes] - Hub route-class resolution policy.
|
|
67
68
|
* @property {string} [reverseGatewayId] - Outbound gateway stable ID.
|
|
68
69
|
* @property {string} [reverseConnectionId] - Outbound gateway stable transport identity.
|
|
69
70
|
* @property {{host: string, port: number, token: string}} [reverseHub] - Outbound hub endpoint.
|
|
@@ -206,7 +207,8 @@ export function loadEnvironmentConfig(env = process.env) {
|
|
|
206
207
|
...(relayTls !== undefined ? {relayTls} : {}),
|
|
207
208
|
...(mode === "reverse-hub" ? {
|
|
208
209
|
reverseGatewayBindings: parseReverseGatewayBindings(env.SOCKETDUCT_REVERSE_GATEWAY_BINDINGS ?? ""),
|
|
209
|
-
reverseRoutes: parseReverseRoutes(env.SOCKETDUCT_REVERSE_ROUTES ?? "")
|
|
210
|
+
reverseRoutes: parseReverseRoutes(env.SOCKETDUCT_REVERSE_ROUTES ?? ""),
|
|
211
|
+
reverseRouteClassRoutes: parseReverseRouteClassRoutes(env.SOCKETDUCT_REVERSE_ROUTE_CLASS_ROUTES ?? "")
|
|
210
212
|
} : {}),
|
|
211
213
|
...(mode === "reverse-gateway" ? {
|
|
212
214
|
reverseGatewayId: env.SOCKETDUCT_REVERSE_GATEWAY_ID?.trim() ?? "",
|
|
@@ -252,6 +254,27 @@ function parseReverseRoutes(value) {
|
|
|
252
254
|
})
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
/**
|
|
258
|
+
* @param {string} value - Comma-separated username:routeClass:gatewayId:host:port routes.
|
|
259
|
+
* @returns {Array<{username: string, routeClass: string, gatewayId: string, host: string, port: number}>} Route-class routes.
|
|
260
|
+
*/
|
|
261
|
+
function parseReverseRouteClassRoutes(value) {
|
|
262
|
+
return splitList(value).map((entry) => {
|
|
263
|
+
const first = entry.indexOf(":")
|
|
264
|
+
const second = entry.indexOf(":", first + 1)
|
|
265
|
+
const third = entry.indexOf(":", second + 1)
|
|
266
|
+
const last = entry.lastIndexOf(":")
|
|
267
|
+
if (first <= 0 || second <= first + 1 || third <= second + 1 || last <= third + 1 || last === entry.length - 1) throw new Error("SOCKETDUCT_REVERSE_ROUTE_CLASS_ROUTES entries must use username:routeClass:gatewayId:host:port")
|
|
268
|
+
return {
|
|
269
|
+
username: entry.slice(0, first),
|
|
270
|
+
routeClass: entry.slice(first + 1, second),
|
|
271
|
+
gatewayId: entry.slice(second + 1, third),
|
|
272
|
+
host: entry.slice(third + 1, last),
|
|
273
|
+
port: parsePort(entry.slice(last + 1), "SOCKETDUCT_REVERSE_ROUTE_CLASS_ROUTES port", DEFAULT_RELAY_PORT)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
255
278
|
/**
|
|
256
279
|
* @param {string | undefined} value - Raw version.
|
|
257
280
|
* @returns {2 | 3} Selected protocol version.
|
package/src/reverse/client.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
parseTarget,
|
|
21
21
|
positiveInteger,
|
|
22
22
|
resolveVerifiedTls,
|
|
23
|
+
ROUTE_CLASS,
|
|
23
24
|
STABLE_IDENTIFIER,
|
|
24
25
|
writeSocket
|
|
25
26
|
} from "./common.js"
|
|
@@ -29,7 +30,8 @@ const DEFAULT_CONNECTION_BUFFER_BYTES = 32 * 1024 * 1024
|
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* @typedef {object} ReverseGatewayConnectionOptions
|
|
32
|
-
* @property {string} gatewayId - Registered
|
|
33
|
+
* @property {string} [gatewayId] - Registered exact gateway selected by this legacy member.
|
|
34
|
+
* @property {string} [routeClass] - Server-resolved route class selected by this member.
|
|
33
35
|
* @property {string} [connectionId] - Stable client transport identity.
|
|
34
36
|
* @property {{host: string, port: number, token: string}} hub - Server3 hub endpoint.
|
|
35
37
|
* @property {{caFile?: string, certFile?: string, keyFile?: string, servername?: string}} tls - Mandatory verified outer TLS.
|
|
@@ -82,7 +84,7 @@ const DEFAULT_CONNECTION_BUFFER_BYTES = 32 * 1024 * 1024
|
|
|
82
84
|
* @property {((error?: Error | null) => void) | undefined} endCallback
|
|
83
85
|
*/
|
|
84
86
|
|
|
85
|
-
/** Server3/TensorBuzz-facing virtual socket connection to
|
|
87
|
+
/** Server3/TensorBuzz-facing virtual socket connection to an exact gateway or server-resolved route class. */
|
|
86
88
|
export class ReverseGatewayConnection extends EventEmitter {
|
|
87
89
|
/** @type {ReverseGatewayConnectionOptions} */ #options
|
|
88
90
|
/** @type {string} */ #connectionId
|
|
@@ -180,11 +182,11 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
180
182
|
logDiagnostic(this.#options.logger, "virtual_stream.opening", {
|
|
181
183
|
streamId: id,
|
|
182
184
|
generation: this.#generation,
|
|
183
|
-
|
|
185
|
+
...this.#routeIdentityFields(),
|
|
184
186
|
recovered: false
|
|
185
187
|
})
|
|
186
188
|
if (callback !== undefined) installCallback(socket, callback)
|
|
187
|
-
void this.#sendControl(V3FrameType.OPEN, id, 0, {
|
|
189
|
+
void this.#sendControl(V3FrameType.OPEN, id, 0, {...this.#routeIdentityFields(), identifier, name, target, initialWindow: this.#initialWindow()}).catch((error) => socket._receiveError(asError(error)))
|
|
188
190
|
return socket
|
|
189
191
|
}
|
|
190
192
|
|
|
@@ -265,7 +267,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
265
267
|
logDiagnostic(this.#options.logger, error === null ? "virtual_stream.closed" : "virtual_stream.error", {
|
|
266
268
|
streamId,
|
|
267
269
|
generation: this.#generation,
|
|
268
|
-
|
|
270
|
+
...this.#routeIdentityFields(),
|
|
269
271
|
...(error === null ? {} : errorDiagnostics(error))
|
|
270
272
|
})
|
|
271
273
|
this.#retireStream(stream, error ?? new SocketductStreamError("Reverse stream is not writable", "ERR_STREAM_DESTROYED"))
|
|
@@ -297,7 +299,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
297
299
|
logDiagnostic(this.#options.logger, "transport.connecting", {
|
|
298
300
|
generation,
|
|
299
301
|
attempt: this.#physicalAttempts,
|
|
300
|
-
|
|
302
|
+
...this.#routeIdentityFields(),
|
|
301
303
|
reconnect: this.#physicalAttempts > 1
|
|
302
304
|
})
|
|
303
305
|
const tls = resolveVerifiedTls(this.#options.tls)
|
|
@@ -308,7 +310,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
308
310
|
logDiagnostic(this.#options.logger, "transport.failed", {
|
|
309
311
|
generation,
|
|
310
312
|
attempt: this.#physicalAttempts,
|
|
311
|
-
|
|
313
|
+
...this.#routeIdentityFields(),
|
|
312
314
|
...errorDiagnostics(error)
|
|
313
315
|
})
|
|
314
316
|
this.#scheduleReconnect(asError(error))
|
|
@@ -346,13 +348,14 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
346
348
|
authenticated = true
|
|
347
349
|
this.#codec = ZSTD_CODEC
|
|
348
350
|
this.#sendWindowLimit = Math.min(this.#initialWindow(), detail.initialWindow)
|
|
349
|
-
await writeSocket(physical, encodeV3Frame({type: V3FrameType.GATEWAY_SELECT, streamId: 0, sequence: generation}, encodeV3Control(
|
|
351
|
+
await writeSocket(physical, encodeV3Frame({type: V3FrameType.GATEWAY_SELECT, streamId: 0, sequence: generation}, encodeV3Control(this.#routeIdentityFields())))
|
|
350
352
|
continue
|
|
351
353
|
}
|
|
352
354
|
if (!selected) {
|
|
353
355
|
if (frame.type !== V3FrameType.GATEWAY_SELECTED) throw fatalError("GATEWAY_SELECTION_FAILED")
|
|
354
356
|
const detail = decodeV3Control(frame.payload)
|
|
355
|
-
|
|
357
|
+
const selection = this.#routeIdentityFields()
|
|
358
|
+
if (detail.generation !== generation || ("gatewayId" in selection ? detail.gatewayId !== selection.gatewayId || Object.hasOwn(detail, "routeClass") : detail.routeClass !== selection.routeClass || Object.hasOwn(detail, "gatewayId"))) throw fatalError("GATEWAY_SELECTION_FAILED")
|
|
356
359
|
selected = true
|
|
357
360
|
clearTimeout(timer)
|
|
358
361
|
this.#physicalConnects += 1
|
|
@@ -361,7 +364,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
361
364
|
logDiagnostic(this.#options.logger, "transport.connected", {
|
|
362
365
|
generation,
|
|
363
366
|
attempt: this.#physicalAttempts,
|
|
364
|
-
|
|
367
|
+
...this.#routeIdentityFields(),
|
|
365
368
|
physicalConnects: this.#physicalConnects,
|
|
366
369
|
reconnect: this.#physicalConnects > 1
|
|
367
370
|
})
|
|
@@ -369,7 +372,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
369
372
|
logDiagnostic(this.#options.logger, "transport.reconnect_succeeded", {
|
|
370
373
|
generation,
|
|
371
374
|
attempt: this.#reconnectAttempts,
|
|
372
|
-
|
|
375
|
+
...this.#routeIdentityFields(),
|
|
373
376
|
physicalConnects: this.#physicalConnects
|
|
374
377
|
})
|
|
375
378
|
}
|
|
@@ -397,7 +400,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
397
400
|
if (!this.#closing) {
|
|
398
401
|
logDiagnostic(this.#options.logger, "transport.disconnected", {
|
|
399
402
|
generation,
|
|
400
|
-
|
|
403
|
+
...this.#routeIdentityFields(),
|
|
401
404
|
virtualStreams: this.#streams.size,
|
|
402
405
|
...errorDiagnostics(attemptError ?? lost, "GATEWAY_DISCONNECTED")
|
|
403
406
|
})
|
|
@@ -411,7 +414,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
411
414
|
logDiagnostic(this.#options.logger, "transport.failed", {
|
|
412
415
|
generation,
|
|
413
416
|
attempt: this.#physicalAttempts,
|
|
414
|
-
|
|
417
|
+
...this.#routeIdentityFields(),
|
|
415
418
|
...errorDiagnostics(terminalError, "GATEWAY_DISCONNECTED")
|
|
416
419
|
})
|
|
417
420
|
this.#rejectConnect?.(terminalError)
|
|
@@ -441,7 +444,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
441
444
|
logDiagnostic(this.#options.logger, "virtual_stream.opened", {
|
|
442
445
|
streamId: stream.id,
|
|
443
446
|
generation: this.#generation,
|
|
444
|
-
|
|
447
|
+
...this.#routeIdentityFields(),
|
|
445
448
|
recovered: false
|
|
446
449
|
})
|
|
447
450
|
stream.socket._markConnected()
|
|
@@ -474,7 +477,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
474
477
|
logDiagnostic(this.#options.logger, "virtual_stream.closed", {
|
|
475
478
|
streamId: stream.id,
|
|
476
479
|
generation: this.#generation,
|
|
477
|
-
|
|
480
|
+
...this.#routeIdentityFields()
|
|
478
481
|
})
|
|
479
482
|
this.#retireStream(stream, new SocketductStreamError("Reverse stream closed before pending writes completed", "ERR_STREAM_DESTROYED"))
|
|
480
483
|
stream.socket._receiveEnd()
|
|
@@ -652,7 +655,7 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
652
655
|
generation: this.#generation,
|
|
653
656
|
attempt: this.#reconnectAttempts,
|
|
654
657
|
delayMs: delay,
|
|
655
|
-
|
|
658
|
+
...this.#routeIdentityFields(),
|
|
656
659
|
...errorDiagnostics(error, "GATEWAY_DISCONNECTED")
|
|
657
660
|
})
|
|
658
661
|
this.#reconnectDelay = Math.min(this.#reconnectDelay * 2, this.#options.maxReconnectDelayMs ?? 30_000)
|
|
@@ -673,13 +676,24 @@ export class ReverseGatewayConnection extends EventEmitter {
|
|
|
673
676
|
|
|
674
677
|
#settleConnect() { this.#connectPromise = undefined; this.#resolveConnect = undefined; this.#rejectConnect = undefined }
|
|
675
678
|
#setState(/** @type {'configured' | 'connecting' | 'ready' | 'reconnecting' | 'closed'} */ state, /** @type {Error | undefined} */ error = undefined) { if (this.#state === state) return; this.#state = state; this.emit("stateChange", state, error) }
|
|
679
|
+
/** @returns {{gatewayId: string} | {routeClass: string}} Stable physical route identity. */
|
|
680
|
+
#routeIdentityFields() {
|
|
681
|
+
return this.#options.routeClass === undefined
|
|
682
|
+
? {gatewayId: /** @type {string} */ (this.#options.gatewayId)}
|
|
683
|
+
: {routeClass: this.#options.routeClass}
|
|
684
|
+
}
|
|
676
685
|
#initialWindow() { return this.#options.initialWindowBytes ?? DEFAULT_WINDOW }
|
|
677
686
|
#maxFrameBytes() { return Math.min(this.#options.maxFrameBytes ?? DEFAULT_FRAME_BYTES, getMaxDecompressedBytes(this.#options.compressionLimits), 64 * 1024) }
|
|
678
687
|
}
|
|
679
688
|
|
|
680
689
|
/** @param {ReverseGatewayConnectionOptions} options - Options. */
|
|
681
690
|
function validateOptions(options) {
|
|
682
|
-
if (typeof options !== "object" || options === null
|
|
691
|
+
if (typeof options !== "object" || options === null) throw new TypeError("Reverse gateway connection options are required")
|
|
692
|
+
const hasGatewayId = Object.hasOwn(options, "gatewayId")
|
|
693
|
+
const hasRouteClass = Object.hasOwn(options, "routeClass")
|
|
694
|
+
if (hasGatewayId === hasRouteClass) throw new TypeError("Reverse gateway connection requires exactly one gatewayId or routeClass")
|
|
695
|
+
if (hasGatewayId && (typeof options.gatewayId !== "string" || !GATEWAY_ID.test(options.gatewayId))) throw new TypeError("Reverse gateway connection requires a valid gatewayId")
|
|
696
|
+
if (hasRouteClass && (typeof options.routeClass !== "string" || !ROUTE_CLASS.test(options.routeClass))) throw new TypeError("Reverse gateway connection requires a valid routeClass")
|
|
683
697
|
if (options.connectionId !== undefined && (typeof options.connectionId !== "string" || !STABLE_IDENTIFIER.test(options.connectionId))) throw new TypeError("Reverse gateway connectionId must be 8-128 URL-safe characters")
|
|
684
698
|
if (typeof options.hub !== "object" || options.hub === null || typeof options.hub.host !== "string" || options.hub.host.length === 0 || !Number.isInteger(options.hub.port) || options.hub.port < 1 || options.hub.port > 65_535 || typeof options.hub.token !== "string" || options.hub.token.length === 0) throw new TypeError("Reverse gateway connection requires hub host, port and token")
|
|
685
699
|
resolveVerifiedTls(options.tls)
|
package/src/reverse/common.js
CHANGED
|
@@ -3,6 +3,7 @@ import {readFileSync} from "node:fs"
|
|
|
3
3
|
import {connect as tlsConnect} from "node:tls"
|
|
4
4
|
|
|
5
5
|
export const GATEWAY_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$/
|
|
6
|
+
export const ROUTE_CLASS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/
|
|
6
7
|
export const STABLE_IDENTIFIER = /^[A-Za-z0-9_-]{8,128}$/
|
|
7
8
|
export const DEFAULT_WINDOW = 256 * 1024
|
|
8
9
|
export const DEFAULT_FRAME_BYTES = 16 * 1024
|