@floegence/flowersec-core 2.5.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +46 -18
  2. package/dist/browser/connectSessionV3.d.ts +13 -0
  3. package/dist/browser/connectSessionV3.js +75 -0
  4. package/dist/browser/index.d.ts +3 -2
  5. package/dist/browser/index.js +2 -1
  6. package/dist/browser/v2.d.ts +3 -0
  7. package/dist/browser/v2.js +2 -0
  8. package/dist/facade.d.ts +15 -8
  9. package/dist/facade.js +5 -4
  10. package/dist/interop/proxyServerPeer.js +1 -1
  11. package/dist/interop/serverParityPeer.js +8 -1
  12. package/dist/node/acceptor.d.ts +10 -0
  13. package/dist/node/acceptor.js +45 -2
  14. package/dist/node/acceptorV3.d.ts +57 -0
  15. package/dist/node/acceptorV3.js +227 -0
  16. package/dist/node/connectSessionV3.d.ts +21 -0
  17. package/dist/node/connectSessionV3.js +73 -0
  18. package/dist/node/index.d.ts +15 -10
  19. package/dist/node/index.js +6 -5
  20. package/dist/node/nativeTransportAddon.d.ts +39 -1
  21. package/dist/node/nativeTransportAddon.js +29 -5
  22. package/dist/node/rawQuicAdapterV3.d.ts +10 -0
  23. package/dist/node/rawQuicAdapterV3.js +76 -0
  24. package/dist/node/rawQuicServerV3.d.ts +24 -0
  25. package/dist/node/rawQuicServerV3.js +34 -0
  26. package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
  27. package/dist/node/runtimeAuthorizationV3.js +126 -0
  28. package/dist/node/tunnelRuntimeV3.d.ts +57 -0
  29. package/dist/node/tunnelRuntimeV3.js +857 -0
  30. package/dist/node/v2.d.ts +11 -0
  31. package/dist/node/v2.js +6 -0
  32. package/dist/node/webSocketServerV3.d.ts +27 -0
  33. package/dist/node/webSocketServerV3.js +219 -0
  34. package/dist/public/streamHandlers.d.ts +3 -0
  35. package/dist/public/streamHandlers.js +6 -1
  36. package/dist/v2/index.d.ts +14 -0
  37. package/dist/v2/index.js +7 -0
  38. package/dist/v2/protocol.js +23 -32
  39. package/dist/v2/session.d.ts +5 -0
  40. package/dist/v2/session.js +90 -35
  41. package/dist/v3/admissionError.d.ts +4 -0
  42. package/dist/v3/admissionError.js +8 -0
  43. package/dist/v3/artifact.d.ts +145 -0
  44. package/dist/v3/artifact.js +1162 -0
  45. package/dist/v3/artifactLease.d.ts +17 -0
  46. package/dist/v3/artifactLease.js +109 -0
  47. package/dist/v3/browserRuntime.d.ts +37 -0
  48. package/dist/v3/browserRuntime.js +197 -0
  49. package/dist/v3/browserSessionRuntime.d.ts +2 -0
  50. package/dist/v3/browserSessionRuntime.js +21 -0
  51. package/dist/v3/capability.d.ts +32 -0
  52. package/dist/v3/capability.js +345 -0
  53. package/dist/v3/carrier.d.ts +84 -0
  54. package/dist/v3/carrier.js +411 -0
  55. package/dist/v3/connectionController.d.ts +87 -0
  56. package/dist/v3/connectionController.js +805 -0
  57. package/dist/v3/contract.d.ts +65 -0
  58. package/dist/v3/contract.js +3 -0
  59. package/dist/v3/controller.d.ts +43 -0
  60. package/dist/v3/controller.js +303 -0
  61. package/dist/v3/handshake.d.ts +81 -0
  62. package/dist/v3/handshake.js +471 -0
  63. package/dist/v3/jcs.d.ts +6 -0
  64. package/dist/v3/jcs.js +69 -0
  65. package/dist/v3/jsonPreflight.d.ts +1 -0
  66. package/dist/v3/jsonPreflight.js +251 -0
  67. package/dist/v3/nodeRuntime.d.ts +40 -0
  68. package/dist/v3/nodeRuntime.js +315 -0
  69. package/dist/v3/nodeSessionRuntime.d.ts +2 -0
  70. package/dist/v3/nodeSessionRuntime.js +13 -0
  71. package/dist/v3/protocol.d.ts +99 -0
  72. package/dist/v3/protocol.js +773 -0
  73. package/dist/v3/publicApi.d.ts +12 -0
  74. package/dist/v3/publicApi.js +46 -0
  75. package/dist/v3/publicSession.d.ts +1 -0
  76. package/dist/v3/publicSession.js +281 -0
  77. package/dist/v3/retryDisposition.d.ts +16 -0
  78. package/dist/v3/retryDisposition.js +30 -0
  79. package/dist/v3/runtimeAdapters.d.ts +9 -0
  80. package/dist/v3/runtimeAdapters.js +109 -0
  81. package/dist/v3/security.d.ts +34 -0
  82. package/dist/v3/security.js +100 -0
  83. package/dist/v3/serverAdmission.d.ts +38 -0
  84. package/dist/v3/serverAdmission.js +223 -0
  85. package/dist/v3/session.d.ts +318 -0
  86. package/dist/v3/session.js +2289 -0
  87. package/dist/v3/sessionConnector.d.ts +37 -0
  88. package/dist/v3/sessionConnector.js +440 -0
  89. package/dist/v3/streamLifetimeLedger.d.ts +31 -0
  90. package/dist/v3/streamLifetimeLedger.js +131 -0
  91. package/dist/v3/streamMetadata.d.ts +1 -0
  92. package/dist/v3/streamMetadata.js +7 -0
  93. package/dist/v3/transportConstants.d.ts +56 -0
  94. package/dist/v3/transportConstants.js +59 -0
  95. package/dist/v3/unreliableMessage.d.ts +44 -0
  96. package/dist/v3/unreliableMessage.js +284 -0
  97. package/dist/v3/webSocketCarrier.d.ts +31 -0
  98. package/dist/v3/webSocketCarrier.js +370 -0
  99. package/dist/v3/webTransportCarrier.d.ts +27 -0
  100. package/dist/v3/webTransportCarrier.js +534 -0
  101. package/package.json +4 -4
  102. package/sbom/cyclonedx.json +6 -6
  103. package/sbom/spdx.json +11 -11
@@ -0,0 +1,251 @@
1
+ const encoder = new TextEncoder();
2
+ export function preflightJSONV3(text, scanScopedPayloads = false) {
3
+ new JSONPreflightScanner(text, scanScopedPayloads).scan();
4
+ }
5
+ class JSONPreflightScanner {
6
+ text;
7
+ scanScopedPayloads;
8
+ index = 0;
9
+ constructor(text, scanScopedPayloads) {
10
+ this.text = text;
11
+ this.scanScopedPayloads = scanScopedPayloads;
12
+ }
13
+ scan() {
14
+ this.value(0, []);
15
+ this.whitespace();
16
+ if (this.index !== this.text.length)
17
+ throw new Error("trailing JSON value");
18
+ }
19
+ value(depth, path) {
20
+ if (depth > 128)
21
+ throw new Error("JSON nesting is too deep");
22
+ this.whitespace();
23
+ if (this.scanScopedPayloads && isScopedPayloadPath(path)) {
24
+ this.scopedValue(1, { nodes: 0 }, true);
25
+ return;
26
+ }
27
+ const char = this.text[this.index];
28
+ if (char === "{")
29
+ return this.object(depth, path);
30
+ if (char === "[")
31
+ return this.array(depth, path);
32
+ if (char === '"') {
33
+ this.string();
34
+ return;
35
+ }
36
+ if (char === "t")
37
+ return this.literal("true");
38
+ if (char === "f")
39
+ return this.literal("false");
40
+ if (char === "n")
41
+ return this.literal("null");
42
+ this.number(false);
43
+ }
44
+ object(depth, path) {
45
+ this.index += 1;
46
+ this.whitespace();
47
+ const seen = new Set();
48
+ if (this.text[this.index] === "}") {
49
+ this.index += 1;
50
+ return;
51
+ }
52
+ while (true) {
53
+ this.whitespace();
54
+ if (this.text[this.index] !== '"')
55
+ throw new Error("JSON object key is not a string");
56
+ const key = this.string();
57
+ if (seen.has(key))
58
+ throw new Error(`duplicate JSON field ${JSON.stringify(key)}`);
59
+ seen.add(key);
60
+ this.whitespace();
61
+ if (this.text[this.index] !== ":")
62
+ throw new Error("missing JSON colon");
63
+ this.index += 1;
64
+ this.value(depth + 1, [...path, key]);
65
+ this.whitespace();
66
+ const next = this.text[this.index];
67
+ if (next === "}") {
68
+ this.index += 1;
69
+ return;
70
+ }
71
+ if (next !== ",")
72
+ throw new Error("invalid JSON object separator");
73
+ this.index += 1;
74
+ }
75
+ }
76
+ array(depth, path) {
77
+ this.index += 1;
78
+ this.whitespace();
79
+ if (this.text[this.index] === "]") {
80
+ this.index += 1;
81
+ return;
82
+ }
83
+ let item = 0;
84
+ while (true) {
85
+ this.value(depth + 1, [...path, item]);
86
+ item += 1;
87
+ this.whitespace();
88
+ const next = this.text[this.index];
89
+ if (next === "]") {
90
+ this.index += 1;
91
+ return;
92
+ }
93
+ if (next !== ",")
94
+ throw new Error("invalid JSON array separator");
95
+ this.index += 1;
96
+ }
97
+ }
98
+ scopedValue(depth, limits, requireObject = false) {
99
+ this.whitespace();
100
+ if (depth > 16)
101
+ throw new Error("scope payload depth exceeds 16");
102
+ limits.nodes += 1;
103
+ if (limits.nodes > 256)
104
+ throw new Error("scope payload node count exceeds 256");
105
+ const char = this.text[this.index];
106
+ if (requireObject && char !== "{")
107
+ throw new Error("scope payload root is not an object");
108
+ if (char === "{")
109
+ return this.scopedObject(depth, limits);
110
+ if (char === "[")
111
+ return this.scopedArray(depth, limits);
112
+ if (char === '"') {
113
+ if (encoder.encode(this.string()).length > 1_024)
114
+ throw new Error("scope payload string exceeds 1024 bytes");
115
+ return;
116
+ }
117
+ if (char === "t")
118
+ return this.literal("true");
119
+ if (char === "f")
120
+ return this.literal("false");
121
+ if (char === "n")
122
+ return this.literal("null");
123
+ this.number(true);
124
+ }
125
+ scopedObject(depth, limits) {
126
+ this.index += 1;
127
+ this.whitespace();
128
+ const seen = new Set();
129
+ if (this.text[this.index] === "}") {
130
+ this.index += 1;
131
+ return;
132
+ }
133
+ let members = 0;
134
+ while (true) {
135
+ this.whitespace();
136
+ if (this.text[this.index] !== '"')
137
+ throw new Error("JSON object key is not a string");
138
+ const key = this.string();
139
+ if (encoder.encode(key).length > 128)
140
+ throw new Error("scope payload key exceeds 128 bytes");
141
+ if (seen.has(key))
142
+ throw new Error(`duplicate JSON field ${JSON.stringify(key)}`);
143
+ seen.add(key);
144
+ members += 1;
145
+ if (members > 64)
146
+ throw new Error("scope payload object exceeds 64 members");
147
+ this.whitespace();
148
+ if (this.text[this.index] !== ":")
149
+ throw new Error("missing JSON colon");
150
+ this.index += 1;
151
+ this.scopedValue(depth + 1, limits);
152
+ this.whitespace();
153
+ const next = this.text[this.index];
154
+ if (next === "}") {
155
+ this.index += 1;
156
+ return;
157
+ }
158
+ if (next !== ",")
159
+ throw new Error("invalid JSON object separator");
160
+ this.index += 1;
161
+ }
162
+ }
163
+ scopedArray(depth, limits) {
164
+ this.index += 1;
165
+ this.whitespace();
166
+ if (this.text[this.index] === "]") {
167
+ this.index += 1;
168
+ return;
169
+ }
170
+ let elements = 0;
171
+ while (true) {
172
+ elements += 1;
173
+ if (elements > 64)
174
+ throw new Error("scope payload array exceeds 64 elements");
175
+ this.scopedValue(depth + 1, limits);
176
+ this.whitespace();
177
+ const next = this.text[this.index];
178
+ if (next === "]") {
179
+ this.index += 1;
180
+ return;
181
+ }
182
+ if (next !== ",")
183
+ throw new Error("invalid JSON array separator");
184
+ this.index += 1;
185
+ }
186
+ }
187
+ string() {
188
+ const start = this.index;
189
+ this.index += 1;
190
+ while (this.index < this.text.length) {
191
+ const code = this.text.charCodeAt(this.index);
192
+ if (code === 0x22) {
193
+ this.index += 1;
194
+ return JSON.parse(this.text.slice(start, this.index));
195
+ }
196
+ if (code < 0x20)
197
+ throw new Error("control character in JSON string");
198
+ if (code === 0x5c) {
199
+ this.index += 1;
200
+ const escaped = this.text[this.index];
201
+ if (escaped === "u") {
202
+ const digits = this.text.slice(this.index + 1, this.index + 5);
203
+ if (!/^[0-9a-fA-F]{4}$/.test(digits))
204
+ throw new Error("invalid JSON unicode escape");
205
+ this.index += 5;
206
+ continue;
207
+ }
208
+ if (escaped === undefined || !'"\\/bfnrt'.includes(escaped)) {
209
+ throw new Error("invalid JSON escape");
210
+ }
211
+ }
212
+ this.index += 1;
213
+ }
214
+ throw new Error("unterminated JSON string");
215
+ }
216
+ number(canonicalInteger) {
217
+ const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(this.text.slice(this.index));
218
+ if (match === null)
219
+ throw new Error("invalid JSON value");
220
+ const raw = match[0];
221
+ if (canonicalInteger) {
222
+ if (!/^-?(?:0|[1-9]\d*)$/.test(raw) || raw === "-0") {
223
+ throw new Error("scope payload number is not a canonical integer");
224
+ }
225
+ try {
226
+ const value = BigInt(raw);
227
+ if (value < -9007199254740991n || value > 9007199254740991n) {
228
+ throw new Error("scope payload integer is outside the signed safe range");
229
+ }
230
+ }
231
+ catch (error) {
232
+ if (error instanceof Error && error.message.includes("scope payload"))
233
+ throw error;
234
+ throw new Error("scope payload integer is outside the signed safe range");
235
+ }
236
+ }
237
+ this.index += raw.length;
238
+ }
239
+ literal(value) {
240
+ if (!this.text.startsWith(value, this.index))
241
+ throw new Error("invalid JSON literal");
242
+ this.index += value.length;
243
+ }
244
+ whitespace() {
245
+ while (/[\u0009\u000a\u000d\u0020]/.test(this.text[this.index] ?? ""))
246
+ this.index += 1;
247
+ }
248
+ }
249
+ function isScopedPayloadPath(path) {
250
+ return path.length === 3 && path[0] === "scoped" && typeof path[1] === "number" && path[2] === "payload";
251
+ }
@@ -0,0 +1,40 @@
1
+ import { type TLSSocket } from "node:tls";
2
+ import type { WebSocketLike } from "../ws-client/binaryTransport.js";
3
+ import type { CanonicalArtifactCandidateV3 } from "./artifact.js";
4
+ import { type RuntimeCapabilityTupleV3 } from "./capability.js";
5
+ import { type TransportSecuritySnapshotV3 } from "./security.js";
6
+ export declare const NODE_RUNTIME_CAPABILITY_V3: Readonly<{
7
+ language: string;
8
+ runtime: string;
9
+ schemaVersion: 3;
10
+ tuples: readonly RuntimeCapabilityTupleV3[];
11
+ unsupported: readonly import("./capability.js").UnsupportedRuntimeCarrierV3[];
12
+ }>;
13
+ export declare function detectNodeRuntimeCapabilityV3(nativeTransportAvailable?: boolean, _webSocketOriginAvailable?: boolean): Readonly<{
14
+ language: string;
15
+ runtime: string;
16
+ schemaVersion: 3;
17
+ tuples: readonly RuntimeCapabilityTupleV3[];
18
+ unsupported: readonly import("./capability.js").UnsupportedRuntimeCarrierV3[];
19
+ }>;
20
+ export type NodeTLSRootsV3 = string | Uint8Array | readonly (string | Uint8Array)[];
21
+ export type NodeWebSocketV3 = WebSocketLike & Readonly<{
22
+ protocol?: string;
23
+ }>;
24
+ export declare function connectNodeTLSSocketV3(candidate: CanonicalArtifactCandidateV3, attemptNowUnixSeconds: number, options?: Readonly<{
25
+ roots?: NodeTLSRootsV3;
26
+ signal?: AbortSignal;
27
+ timeoutMilliseconds?: number;
28
+ nowUnixMilliseconds?: () => number;
29
+ }>): Promise<TLSSocket>;
30
+ export declare function connectNodeWebSocketV3(candidate: CanonicalArtifactCandidateV3, attemptNowUnixSeconds: number, options: Readonly<{
31
+ origin: string;
32
+ roots?: NodeTLSRootsV3;
33
+ signal?: AbortSignal;
34
+ timeoutMilliseconds?: number;
35
+ maxPayload?: number;
36
+ }>): Promise<NodeWebSocketV3>;
37
+ export declare function verifyPinnedLeafCertificateV3(leafDER: Uint8Array, policy: Extract<TransportSecuritySnapshotV3, {
38
+ mode: "pin";
39
+ }>, nowUnixMilliseconds: number): void;
40
+ export declare function isNodeCertificateTrustErrorV3(code: string | undefined): boolean;
@@ -0,0 +1,315 @@
1
+ import { X509Certificate, constants, createHash, timingSafeEqual } from "node:crypto";
2
+ import { createRequire } from "node:module";
3
+ import { checkServerIdentity as tlsCheckServerIdentity, connect as tlsConnect, } from "node:tls";
4
+ import { defineRuntimeCapabilityDescriptorV3 } from "./capability.js";
5
+ import { snapshotTransportSecurityPolicyV3, TransportFailureV3 } from "./security.js";
6
+ import { FLOWERSEC_V3_PATHS, FLOWERSEC_V3_WIRE_PROFILES, websocketSubprotocolForPathV3, } from "./transportConstants.js";
7
+ const websocketTuples = tuplesFor("websocket", false);
8
+ const rawQuicTuples = tuplesFor("raw_quic", true);
9
+ export const NODE_RUNTIME_CAPABILITY_V3 = defineRuntimeCapabilityDescriptorV3("node", websocketTuples, [
10
+ { carrier: "raw_quic", reason: "node_native_transport_unavailable" },
11
+ { carrier: "webtransport", reason: "node_webtransport_driver_unavailable" },
12
+ ]);
13
+ const NODE_RUNTIME_WITH_NATIVE_CAPABILITY_V3 = defineRuntimeCapabilityDescriptorV3("node", [...rawQuicTuples, ...websocketTuples], [{ carrier: "webtransport", reason: "node_webtransport_driver_unavailable" }]);
14
+ export function detectNodeRuntimeCapabilityV3(nativeTransportAvailable = false, _webSocketOriginAvailable = true) {
15
+ return nativeTransportAvailable ? NODE_RUNTIME_WITH_NATIVE_CAPABILITY_V3 : NODE_RUNTIME_CAPABILITY_V3;
16
+ }
17
+ export async function connectNodeTLSSocketV3(candidate, attemptNowUnixSeconds, options = {}) {
18
+ if (candidate.carrier !== "websocket")
19
+ throw new TransportFailureV3("invalid_artifact");
20
+ const url = new URL(candidate.normalized_url);
21
+ if (url.href !== candidate.normalized_url || url.protocol !== "wss:" ||
22
+ url.pathname !== pathFromProfile(candidate.wire_profile) || url.username !== "" || url.password !== "" ||
23
+ url.search !== "" || url.hash !== "")
24
+ throw new TransportFailureV3("invalid_artifact");
25
+ const policy = snapshotTransportSecurityPolicyV3(candidate.tls, attemptNowUnixSeconds, ["ca", "pin"]);
26
+ const port = url.port === "" ? 443 : Number(url.port);
27
+ const host = unbracket(url.hostname);
28
+ const tlsOptions = {
29
+ host,
30
+ port,
31
+ minVersion: "TLSv1.3",
32
+ maxVersion: "TLSv1.3",
33
+ ALPNProtocols: ["http/1.1"],
34
+ secureOptions: constants.SSL_OP_NO_TICKET,
35
+ rejectUnauthorized: policy.mode === "ca",
36
+ ...(isIPAddress(host)
37
+ ? { checkServerIdentity: (_servername, certificate) => tlsCheckServerIdentity(host, certificate) }
38
+ : { servername: host }),
39
+ ...(policy.mode === "ca" && options.roots !== undefined ? { ca: options.roots } : {}),
40
+ };
41
+ const timeout = options.timeoutMilliseconds ?? 10_000;
42
+ if (!Number.isSafeInteger(timeout) || timeout < 1) {
43
+ throw new TransportFailureV3("invalid_artifact");
44
+ }
45
+ const socket = tlsConnect(tlsOptions);
46
+ return await new Promise((resolve, reject) => {
47
+ let settled = false;
48
+ const timer = setTimeout(() => fail(new TransportFailureV3("connection_failed")), timeout);
49
+ const abort = () => fail(options.signal?.reason ?? new Error("aborted"));
50
+ const cleanup = () => {
51
+ clearTimeout(timer);
52
+ options.signal?.removeEventListener("abort", abort);
53
+ socket.removeListener("error", onError);
54
+ };
55
+ const fail = (error) => {
56
+ if (settled)
57
+ return;
58
+ settled = true;
59
+ cleanup();
60
+ socket.destroy();
61
+ reject(error);
62
+ };
63
+ const onError = (error) => {
64
+ if (policy.mode === "ca" && isNodeCertificateTrustErrorV3(error.code)) {
65
+ fail(new TransportFailureV3("tls_failed", "ca_untrusted", error));
66
+ }
67
+ else if (policy.mode === "pin" && isTLSProtocolError(error.code)) {
68
+ fail(new TransportFailureV3("tls_failed", "unknown", error));
69
+ }
70
+ else {
71
+ fail(new TransportFailureV3("connection_failed", undefined, error));
72
+ }
73
+ };
74
+ socket.once("error", onError);
75
+ socket.once("secureConnect", () => {
76
+ if (settled)
77
+ return;
78
+ try {
79
+ if (policy.mode === "pin") {
80
+ const peer = socket.getPeerX509Certificate();
81
+ if (peer === undefined)
82
+ throw new TransportFailureV3("tls_failed", "unknown");
83
+ verifyPinnedLeafCertificateV3(peer.raw, policy, (options.nowUnixMilliseconds ?? Date.now)());
84
+ }
85
+ }
86
+ catch (error) {
87
+ fail(error instanceof TransportFailureV3 ? error : new TransportFailureV3("tls_failed", "unknown", error));
88
+ return;
89
+ }
90
+ settled = true;
91
+ cleanup();
92
+ resolve(socket);
93
+ });
94
+ if (options.signal?.aborted === true)
95
+ abort();
96
+ else
97
+ options.signal?.addEventListener("abort", abort, { once: true });
98
+ });
99
+ }
100
+ export async function connectNodeWebSocketV3(candidate, attemptNowUnixSeconds, options) {
101
+ const origin = validateOrigin(options.origin);
102
+ const maxPayload = options.maxPayload ?? 1_048_576;
103
+ if (!Number.isSafeInteger(maxPayload) || maxPayload < 1) {
104
+ throw new TransportFailureV3("invalid_artifact");
105
+ }
106
+ const socket = await connectNodeTLSSocketV3(candidate, attemptNowUnixSeconds, {
107
+ ...(options.roots === undefined ? {} : { roots: options.roots }),
108
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
109
+ ...(options.timeoutMilliseconds === undefined ? {} : { timeoutMilliseconds: options.timeoutMilliseconds }),
110
+ });
111
+ if (options.signal?.aborted === true) {
112
+ socket.destroy();
113
+ throw options.signal.reason;
114
+ }
115
+ const require = createRequire(import.meta.url);
116
+ const wsModule = require("ws");
117
+ const Constructor = typeof wsModule === "function" ? wsModule : wsModule.WebSocket;
118
+ if (Constructor === undefined) {
119
+ socket.destroy();
120
+ throw new TransportFailureV3("tls_unsupported");
121
+ }
122
+ const subprotocol = candidate.wire_profile === FLOWERSEC_V3_WIRE_PROFILES.direct
123
+ ? websocketSubprotocolForPathV3("direct")
124
+ : candidate.wire_profile === FLOWERSEC_V3_WIRE_PROFILES.tunnel
125
+ ? websocketSubprotocolForPathV3("tunnel")
126
+ : invalidProfile();
127
+ let webSocket;
128
+ try {
129
+ webSocket = new Constructor(candidate.normalized_url, [subprotocol], {
130
+ createConnection: () => socket,
131
+ followRedirects: false,
132
+ headers: { Origin: origin },
133
+ maxPayload,
134
+ perMessageDeflate: false,
135
+ });
136
+ }
137
+ catch (error) {
138
+ socket.destroy();
139
+ throw new TransportFailureV3("connection_failed", undefined, error);
140
+ }
141
+ try {
142
+ await waitForWebSocketOpen(webSocket, subprotocol, options.signal);
143
+ return webSocket;
144
+ }
145
+ catch (error) {
146
+ try {
147
+ webSocket.close();
148
+ }
149
+ catch { /* best effort */ }
150
+ socket.destroy();
151
+ throw error instanceof TransportFailureV3
152
+ ? error
153
+ : new TransportFailureV3("connection_failed", undefined, error);
154
+ }
155
+ }
156
+ export function verifyPinnedLeafCertificateV3(leafDER, policy, nowUnixMilliseconds) {
157
+ if (!Number.isSafeInteger(nowUnixMilliseconds) || nowUnixMilliseconds < 0) {
158
+ throw new TransportFailureV3("tls_failed", "unknown");
159
+ }
160
+ let certificate;
161
+ try {
162
+ certificate = new X509Certificate(leafDER);
163
+ const notBefore = Date.parse(certificate.validFrom);
164
+ const notAfter = Date.parse(certificate.validTo);
165
+ const details = certificate.publicKey.asymmetricKeyDetails;
166
+ if (!isX509V3CertificateDER(certificate.raw) ||
167
+ !Number.isFinite(notBefore) || !Number.isFinite(notAfter) || nowUnixMilliseconds < notBefore ||
168
+ nowUnixMilliseconds >= notAfter || notAfter - notBefore > 1_209_600_000 ||
169
+ certificate.publicKey.asymmetricKeyType !== "ec" || details?.namedCurve !== "prime256v1") {
170
+ throw new Error("certificate profile");
171
+ }
172
+ }
173
+ catch (error) {
174
+ throw new TransportFailureV3("tls_failed", "unknown", error);
175
+ }
176
+ const digest = createHash("sha256").update(certificate.raw).digest();
177
+ let matched = 0;
178
+ for (const pin of policy.activeLeafDerSHA256) {
179
+ if (pin.length === digest.length && timingSafeEqual(pin, digest))
180
+ matched |= 1;
181
+ }
182
+ if (matched === 0) {
183
+ throw new TransportFailureV3("tls_failed", "pin_mismatch");
184
+ }
185
+ }
186
+ function isX509V3CertificateDER(der) {
187
+ const certificate = readDERElement(der, 0, 0x30);
188
+ if (certificate === undefined || certificate.next !== der.length)
189
+ return false;
190
+ const tbsCertificate = readDERElement(certificate.value, 0, 0x30);
191
+ if (tbsCertificate === undefined)
192
+ return false;
193
+ const explicitVersion = readDERElement(tbsCertificate.value, 0, 0xa0);
194
+ if (explicitVersion === undefined)
195
+ return false;
196
+ const version = readDERElement(explicitVersion.value, 0, 0x02);
197
+ return version !== undefined && version.next === explicitVersion.value.length &&
198
+ version.value.length === 1 && version.value[0] === 2;
199
+ }
200
+ function readDERElement(bytes, offset, expectedTag) {
201
+ if (offset >= bytes.length || bytes[offset] !== expectedTag)
202
+ return undefined;
203
+ let cursor = offset + 1;
204
+ if (cursor >= bytes.length)
205
+ return undefined;
206
+ const firstLength = bytes[cursor++] ?? 0;
207
+ let length = firstLength;
208
+ if ((firstLength & 0x80) !== 0) {
209
+ const count = firstLength & 0x7f;
210
+ if (count === 0 || count > 4 || cursor + count > bytes.length)
211
+ return undefined;
212
+ length = 0;
213
+ for (let index = 0; index < count; index += 1) {
214
+ length = length * 256 + (bytes[cursor++] ?? 0);
215
+ }
216
+ }
217
+ if (cursor + length > bytes.length)
218
+ return undefined;
219
+ return { value: bytes.subarray(cursor, cursor + length), next: cursor + length };
220
+ }
221
+ function tuplesFor(carrier, datagrams) {
222
+ return Object.freeze([
223
+ { carrier, datagrams, migration: false, networkMode: "dial", path: "direct", reliableStreams: true, securityModes: ["ca", "pin"], sessionRole: "client" },
224
+ { carrier, datagrams, migration: false, networkMode: "dial", path: "tunnel", reliableStreams: true, securityModes: ["ca", "pin"], sessionRole: "client" },
225
+ { carrier, datagrams, migration: false, networkMode: "dial", path: "tunnel", reliableStreams: true, securityModes: ["ca", "pin"], sessionRole: "server" },
226
+ { carrier, datagrams, migration: false, networkMode: "listen", path: "direct", reliableStreams: true, securityModes: [], sessionRole: "server" },
227
+ ]);
228
+ }
229
+ function pathFromProfile(profile) {
230
+ if (profile === FLOWERSEC_V3_WIRE_PROFILES.direct)
231
+ return FLOWERSEC_V3_PATHS.websocket.direct;
232
+ if (profile === FLOWERSEC_V3_WIRE_PROFILES.tunnel)
233
+ return FLOWERSEC_V3_PATHS.websocket.tunnel;
234
+ throw new TransportFailureV3("invalid_artifact");
235
+ }
236
+ function unbracket(host) {
237
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
238
+ }
239
+ function isIPAddress(host) {
240
+ return /^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":");
241
+ }
242
+ export function isNodeCertificateTrustErrorV3(code) {
243
+ return code !== undefined && NODE_CERTIFICATE_TRUST_ERROR_CODES.has(code);
244
+ }
245
+ const NODE_CERTIFICATE_TRUST_ERROR_CODES = new Set([
246
+ "CERT_HAS_EXPIRED",
247
+ "CERT_NOT_YET_VALID",
248
+ "CERT_REJECTED",
249
+ "CERT_REVOKED",
250
+ "CERT_SIGNATURE_FAILURE",
251
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
252
+ "ERR_TLS_CERT_ALTNAME_INVALID",
253
+ "INVALID_CA",
254
+ "INVALID_PURPOSE",
255
+ "PATH_LENGTH_EXCEEDED",
256
+ "SELF_SIGNED_CERT_IN_CHAIN",
257
+ "UNABLE_TO_DECRYPT_CERT_SIGNATURE",
258
+ "UNABLE_TO_GET_ISSUER_CERT",
259
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
260
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
261
+ ]);
262
+ function isTLSProtocolError(code) {
263
+ return code?.startsWith("ERR_SSL_") === true || code?.startsWith("ERR_TLS_") === true;
264
+ }
265
+ function validateOrigin(raw) {
266
+ let value;
267
+ try {
268
+ value = new URL(raw);
269
+ }
270
+ catch {
271
+ throw new TransportFailureV3("invalid_artifact");
272
+ }
273
+ if ((value.protocol !== "https:" && value.protocol !== "http:") || value.origin !== raw ||
274
+ value.username !== "" || value.password !== "")
275
+ throw new TransportFailureV3("invalid_artifact");
276
+ return value.origin;
277
+ }
278
+ function invalidProfile() {
279
+ throw new TransportFailureV3("invalid_artifact");
280
+ }
281
+ async function waitForWebSocketOpen(socket, expectedProtocol, signal) {
282
+ if (socket.readyState === 1) {
283
+ if (socket.protocol !== expectedProtocol)
284
+ throw new TransportFailureV3("connection_failed");
285
+ return;
286
+ }
287
+ await new Promise((resolve, reject) => {
288
+ let settled = false;
289
+ const cleanup = () => {
290
+ socket.removeEventListener("open", open);
291
+ socket.removeEventListener("error", error);
292
+ socket.removeEventListener("close", close);
293
+ signal?.removeEventListener("abort", abort);
294
+ };
295
+ const finish = (failure) => {
296
+ if (settled)
297
+ return;
298
+ settled = true;
299
+ cleanup();
300
+ failure === undefined ? resolve() : reject(failure);
301
+ };
302
+ const open = () => socket.protocol === expectedProtocol
303
+ ? finish()
304
+ : finish(new TransportFailureV3("connection_failed"));
305
+ const error = (event) => finish(new TransportFailureV3("connection_failed", undefined, event));
306
+ const close = () => finish(new TransportFailureV3("connection_failed"));
307
+ const abort = () => finish(signal?.reason);
308
+ socket.addEventListener("open", open);
309
+ socket.addEventListener("error", error);
310
+ socket.addEventListener("close", close);
311
+ signal?.addEventListener("abort", abort, { once: true });
312
+ if (signal?.aborted === true)
313
+ abort();
314
+ });
315
+ }
@@ -0,0 +1,2 @@
1
+ import type { SessionProtocolRuntimeV3 } from "./session.js";
2
+ export declare const nodeSessionRuntimeV3: SessionProtocolRuntimeV3;
@@ -0,0 +1,13 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { performance } from "node:perf_hooks";
3
+ export const nodeSessionRuntimeV3 = Object.freeze({
4
+ entropy(length) {
5
+ if (!Number.isSafeInteger(length) || length < 1 || length > 65_536) {
6
+ throw new RangeError("entropy length must be an integer from 1 to 65536");
7
+ }
8
+ return Uint8Array.from(randomBytes(length));
9
+ },
10
+ monotonicMilliseconds() {
11
+ return performance.now();
12
+ },
13
+ });