@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,12 @@
1
+ import { type ArtifactLeaseV3 } from "./artifactLease.js";
2
+ export declare class ArtifactHandleV3 {
3
+ private readonly artifactHandleBrand;
4
+ private constructor();
5
+ }
6
+ export type ArtifactParseErrorCodeV3 = "artifact_too_large" | "invalid_artifact";
7
+ export declare class ArtifactParseErrorV3 extends Error {
8
+ readonly code: ArtifactParseErrorCodeV3;
9
+ constructor(code: ArtifactParseErrorCodeV3);
10
+ }
11
+ export declare function parseArtifactV3(input: string | Uint8Array): ArtifactHandleV3;
12
+ export declare function createArtifactLeaseV3(artifact: ArtifactHandleV3, commitSpend: (signal?: AbortSignal) => Promise<void>, retireCleanup?: () => Promise<void>): ArtifactLeaseV3;
@@ -0,0 +1,46 @@
1
+ import { ArtifactV3Error, decodeArtifactV3JSON, validateArtifactV3, } from "./artifact.js";
2
+ import { createArtifactLeaseV3Internal, } from "./artifactLease.js";
3
+ const artifacts = new WeakMap();
4
+ export class ArtifactHandleV3 {
5
+ constructor() { }
6
+ }
7
+ export class ArtifactParseErrorV3 extends Error {
8
+ code;
9
+ constructor(code) {
10
+ super(`Flowersec v3 artifact parsing failed (code=${code})`);
11
+ this.code = code;
12
+ this.name = "ArtifactError";
13
+ }
14
+ }
15
+ export function parseArtifactV3(input) {
16
+ try {
17
+ const value = decodeArtifactV3JSON(input);
18
+ const handle = new ArtifactHandleV3();
19
+ artifacts.set(handle, value);
20
+ return Object.freeze(handle);
21
+ }
22
+ catch (error) {
23
+ throw new ArtifactParseErrorV3(error instanceof ArtifactV3Error && error.code === "artifact_too_large"
24
+ ? "artifact_too_large"
25
+ : "invalid_artifact");
26
+ }
27
+ }
28
+ export function createArtifactLeaseV3(artifact, commitSpend, retireCleanup) {
29
+ const value = artifacts.get(artifact);
30
+ if (value === undefined)
31
+ throw new ArtifactParseErrorV3("invalid_artifact");
32
+ try {
33
+ validateArtifactV3(value);
34
+ return createArtifactLeaseV3Internal(value, commitSpend, retireCleanup);
35
+ }
36
+ catch {
37
+ throw new ArtifactParseErrorV3("invalid_artifact");
38
+ }
39
+ }
40
+ /** @internal */
41
+ export function unwrapArtifactHandleV3(handle) {
42
+ const value = artifacts.get(handle);
43
+ if (value === undefined)
44
+ throw new ArtifactParseErrorV3("invalid_artifact");
45
+ return value;
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,281 @@
1
+ import { SessionError } from "./contract.js";
2
+ import { createStreamMetadataV3, streamMetadataValuesV3 } from "./streamMetadata.js";
3
+ import { assertRpcTypeId } from "../rpc/validate.js";
4
+ /** @internal */
5
+ export function projectSessionV3(session) {
6
+ const notificationOwner = {
7
+ subscriptions: new Set(),
8
+ closed: false,
9
+ };
10
+ const clearNotificationSubscriptions = () => {
11
+ notificationOwner.closed = true;
12
+ for (const unsubscribe of [...notificationOwner.subscriptions])
13
+ unsubscribe();
14
+ };
15
+ const rpc = projectRpcPeerV3(session.rpc, notificationOwner);
16
+ const unreliable = session.unreliableMessages;
17
+ void session.termination.then(clearNotificationSubscriptions, clearNotificationSubscriptions);
18
+ return Object.freeze({
19
+ rpc,
20
+ ...(unreliable === undefined ? {} : { unreliableMessages: unreliable }),
21
+ async openStream(kind, options) {
22
+ try {
23
+ const internalOptions = options === undefined ? undefined : {
24
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
25
+ ...(options.metadata === undefined ? {} : { metadata: streamMetadataValuesV3(options.metadata) }),
26
+ };
27
+ return projectByteStreamV3(await session.openStream(kind, internalOptions));
28
+ }
29
+ catch (error) {
30
+ throw redactSessionError(error);
31
+ }
32
+ },
33
+ async acceptStream(options) {
34
+ try {
35
+ const incoming = await session.acceptStream(options);
36
+ return Object.freeze({
37
+ kind: incoming.kind,
38
+ metadata: createStreamMetadataV3(incoming.metadata),
39
+ stream: projectByteStreamV3(incoming.stream),
40
+ });
41
+ }
42
+ catch (error) {
43
+ throw redactSessionError(error);
44
+ }
45
+ },
46
+ async rekey(options) {
47
+ try {
48
+ await session.rekey(options);
49
+ }
50
+ catch (error) {
51
+ throw redactSessionError(error);
52
+ }
53
+ },
54
+ async probeLiveness(options) {
55
+ try {
56
+ return await session.probeLiveness(options);
57
+ }
58
+ catch (error) {
59
+ throw redactSessionError(error);
60
+ }
61
+ },
62
+ async waitTermination(options) {
63
+ try {
64
+ const { error } = await raceWithSignal(session.waitTermination(), options?.signal);
65
+ return Object.freeze({ error: redactSessionError(error) });
66
+ }
67
+ catch (error) {
68
+ throw redactSessionError(error);
69
+ }
70
+ },
71
+ async close() {
72
+ try {
73
+ await session.close();
74
+ }
75
+ catch (error) {
76
+ throw redactSessionError(error);
77
+ }
78
+ finally {
79
+ clearNotificationSubscriptions();
80
+ }
81
+ },
82
+ });
83
+ }
84
+ function projectByteStreamV3(stream) {
85
+ return Object.freeze({
86
+ kind: stream.kind,
87
+ get terminalError() {
88
+ return stream.terminalError === undefined ? undefined : redactSessionError(stream.terminalError);
89
+ },
90
+ async read(options) {
91
+ try {
92
+ return await stream.read(options);
93
+ }
94
+ catch (error) {
95
+ throw redactSessionError(error);
96
+ }
97
+ },
98
+ async write(data, options) {
99
+ try {
100
+ return await stream.write(data, options);
101
+ }
102
+ catch (error) {
103
+ throw redactSessionError(error);
104
+ }
105
+ },
106
+ async closeWrite() {
107
+ try {
108
+ await stream.closeWrite();
109
+ }
110
+ catch (error) {
111
+ throw redactSessionError(error);
112
+ }
113
+ },
114
+ async reset() {
115
+ try {
116
+ await stream.reset();
117
+ }
118
+ catch (error) {
119
+ throw redactSessionError(error);
120
+ }
121
+ },
122
+ async close() {
123
+ try {
124
+ await stream.close();
125
+ }
126
+ catch (error) {
127
+ throw redactSessionError(error);
128
+ }
129
+ },
130
+ });
131
+ }
132
+ function projectRpcPeerV3(peer, notificationOwner) {
133
+ return Object.freeze({
134
+ async call(typeId, payload, decodeResponse, options) {
135
+ try {
136
+ assertRpcTypeId(typeId);
137
+ assertJsonValue(payload);
138
+ const result = await peer.call(typeId, payload, options?.signal);
139
+ if (result.error !== undefined) {
140
+ return Object.freeze({ ok: false, error: Object.freeze({ ...result.error }) });
141
+ }
142
+ assertJsonValue(result.payload);
143
+ return Object.freeze({ ok: true, payload: decodeResponse(result.payload) });
144
+ }
145
+ catch (error) {
146
+ throw redactSessionError(error);
147
+ }
148
+ },
149
+ async notify(typeId, payload, options) {
150
+ try {
151
+ assertRpcTypeId(typeId);
152
+ assertJsonValue(payload);
153
+ if (options?.signal?.aborted)
154
+ throw options.signal.reason ?? new DOMException("The operation was aborted", "AbortError");
155
+ await raceWithSignal(peer.notify(typeId, payload), options?.signal);
156
+ }
157
+ catch (error) {
158
+ throw redactSessionError(error);
159
+ }
160
+ },
161
+ onNotify(typeId, decodePayload, handler) {
162
+ assertRpcTypeId(typeId);
163
+ if (notificationOwner.closed)
164
+ return () => undefined;
165
+ const unsubscribe = peer.onNotify(typeId, (payload) => {
166
+ let decoded;
167
+ try {
168
+ assertJsonValue(payload);
169
+ decoded = decodePayload(payload);
170
+ }
171
+ catch {
172
+ return;
173
+ }
174
+ try {
175
+ void Promise.resolve(handler(decoded)).catch(() => undefined);
176
+ }
177
+ catch {
178
+ // Notification handlers are isolated from RPC serving.
179
+ }
180
+ });
181
+ let subscribed = true;
182
+ const cancel = () => {
183
+ if (!subscribed)
184
+ return;
185
+ subscribed = false;
186
+ notificationOwner.subscriptions.delete(cancel);
187
+ unsubscribe();
188
+ };
189
+ notificationOwner.subscriptions.add(cancel);
190
+ return cancel;
191
+ },
192
+ });
193
+ }
194
+ function assertJsonValue(value) {
195
+ validateJsonValue(value, new Set());
196
+ }
197
+ function validateJsonValue(value, ancestors) {
198
+ if (value === null || typeof value === "string" || typeof value === "boolean")
199
+ return;
200
+ if (typeof value === "number") {
201
+ if (Number.isFinite(value))
202
+ return;
203
+ throw new TypeError("RPC payload contains a non-finite number");
204
+ }
205
+ if (typeof value !== "object")
206
+ throw new TypeError("RPC payload is not a JSON value");
207
+ if (ancestors.has(value))
208
+ throw new TypeError("RPC payload contains a cycle");
209
+ ancestors.add(value);
210
+ try {
211
+ if (Array.isArray(value)) {
212
+ for (let index = 0; index < value.length; index += 1) {
213
+ if (!(index in value))
214
+ throw new TypeError("RPC payload contains a sparse array");
215
+ validateJsonValue(value[index], ancestors);
216
+ }
217
+ return;
218
+ }
219
+ const prototype = Object.getPrototypeOf(value);
220
+ if (prototype !== Object.prototype && prototype !== null) {
221
+ throw new TypeError("RPC payload contains a non-JSON object");
222
+ }
223
+ for (const key of Object.keys(value)) {
224
+ validateJsonValue(value[key], ancestors);
225
+ }
226
+ }
227
+ finally {
228
+ ancestors.delete(value);
229
+ }
230
+ }
231
+ async function raceWithSignal(operation, signal) {
232
+ if (signal === undefined)
233
+ return await operation;
234
+ if (signal.aborted)
235
+ throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
236
+ return await new Promise((resolve, reject) => {
237
+ const abort = () => reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError"));
238
+ signal.addEventListener("abort", abort, { once: true });
239
+ void operation.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
240
+ });
241
+ }
242
+ function redactSessionError(error) {
243
+ if (error instanceof SessionError)
244
+ return error;
245
+ return new SessionError(sessionErrorCode(error));
246
+ }
247
+ function sessionErrorCode(error) {
248
+ if (error instanceof DOMException && error.name === "AbortError")
249
+ return "canceled";
250
+ if (error instanceof Error) {
251
+ if (error.name === "AbortError")
252
+ return "canceled";
253
+ if (error.name === "TimeoutError")
254
+ return "timeout";
255
+ const code = error.code;
256
+ if (error.name === "CarrierError" || typeof code === "string") {
257
+ if (code === "closed" || code === "carrier_closed")
258
+ return "closed";
259
+ if (code === "aborted" || code === "operation_aborted")
260
+ return "canceled";
261
+ if (code === "reset" || code === "stream_reset")
262
+ return "stream_reset";
263
+ }
264
+ if (error.name === "SessionV3Error") {
265
+ const code = error.code;
266
+ switch (code) {
267
+ case "aborted": return "canceled";
268
+ case "timeout": return "timeout";
269
+ case "closed": return "closed";
270
+ case "going_away": return "going_away";
271
+ case "open_rejected": return "stream_rejected";
272
+ case "stream_reset": return "stream_reset";
273
+ case "resource_exhausted": return "resource_exhausted";
274
+ case "rekey_failed": return "rekey_failed";
275
+ case "liveness_failed": return "liveness_failed";
276
+ default: return "operation_failed";
277
+ }
278
+ }
279
+ }
280
+ return "operation_failed";
281
+ }
@@ -0,0 +1,16 @@
1
+ import type { SessionErrorCode } from "./contract.js";
2
+ import type { ConnectErrorCode } from "../public/connectError.js";
3
+ export type RetryDisposition = Readonly<{
4
+ kind: "terminal";
5
+ }> | Readonly<{
6
+ kind: "retryable";
7
+ }> | Readonly<{
8
+ kind: "retry_after";
9
+ notBeforeUnixMilliseconds: number;
10
+ }>;
11
+ export declare function retryDispositionForConnectError(error: Readonly<{
12
+ code: ConnectErrorCode;
13
+ }>): RetryDisposition;
14
+ export declare function retryDispositionForSessionError(error: Readonly<{
15
+ code: SessionErrorCode;
16
+ }>): RetryDisposition;
@@ -0,0 +1,30 @@
1
+ export function retryDispositionForConnectError(error) {
2
+ switch (error.code) {
3
+ case "expired_artifact":
4
+ case "resolve_failed":
5
+ case "credential_spend_failed":
6
+ case "connection_failed":
7
+ case "timeout":
8
+ case "handshake_failed":
9
+ case "rpc_failed":
10
+ case "resource_exhausted":
11
+ case "not_connected":
12
+ return Object.freeze({ kind: "retryable" });
13
+ default:
14
+ return Object.freeze({ kind: "terminal" });
15
+ }
16
+ }
17
+ export function retryDispositionForSessionError(error) {
18
+ switch (error.code) {
19
+ case "closed":
20
+ case "going_away":
21
+ case "timeout":
22
+ case "resource_exhausted":
23
+ case "stream_reset":
24
+ case "rekey_failed":
25
+ case "liveness_failed":
26
+ return Object.freeze({ kind: "retryable" });
27
+ default:
28
+ return Object.freeze({ kind: "terminal" });
29
+ }
30
+ }
@@ -0,0 +1,9 @@
1
+ import { type WebSocketLike } from "../ws-client/binaryTransport.js";
2
+ import type { ArtifactV3, CanonicalArtifactCandidateV3 } from "./artifact.js";
3
+ import { type NativeCarrierSessionV3 } from "./carrier.js";
4
+ import type { ReadyAdmissionTransportV3 } from "./sessionConnector.js";
5
+ export type WebSocketLikeV3 = WebSocketLike & Readonly<{
6
+ protocol?: string;
7
+ }>;
8
+ export declare function readyWebSocketAdmissionV3(candidate: CanonicalArtifactCandidateV3, artifact: ArtifactV3, socket: WebSocketLikeV3, signal: AbortSignal): Promise<ReadyAdmissionTransportV3>;
9
+ export declare function readyNativeAdmissionV3(candidate: CanonicalArtifactCandidateV3, carrier: NativeCarrierSessionV3): ReadyAdmissionTransportV3;
@@ -0,0 +1,109 @@
1
+ import { WebSocketBinaryTransport } from "../ws-client/binaryTransport.js";
2
+ import { adaptNativeCarrierSessionV3, } from "./carrier.js";
3
+ import { TransportFailureV3 } from "./security.js";
4
+ import { createWebSocketCarrierSessionV3 } from "./webSocketCarrier.js";
5
+ import { websocketSubprotocolForPathV3 } from "./transportConstants.js";
6
+ export async function readyWebSocketAdmissionV3(candidate, artifact, socket, signal) {
7
+ const subprotocol = websocketSubprotocolForPathV3(artifact.path.kind);
8
+ await waitForWebSocketOpen(socket, subprotocol, signal);
9
+ const transport = new WebSocketBinaryTransport(socket);
10
+ let finalized = false;
11
+ return {
12
+ candidate,
13
+ openAdmissionChannel: async () => ({
14
+ framing: "message",
15
+ write: async (data, operationSignal) => await transport.writeBinary(data, operationSignal === undefined ? {} : { signal: operationSignal }),
16
+ read: async (operationSignal) => await transport.readBinary(operationSignal === undefined ? {} : { signal: operationSignal }),
17
+ abort: () => transport.close(),
18
+ }),
19
+ finalize: () => {
20
+ if (finalized)
21
+ throw new TransportFailureV3("connection_failed");
22
+ finalized = true;
23
+ return createWebSocketCarrierSessionV3(transport, {
24
+ path: artifact.path.kind,
25
+ client: artifact.path.kind !== "tunnel" || artifact.path.role !== 2,
26
+ inboundBidirectionalStreamCapacity: artifact.session.max_inbound_streams + 2,
27
+ });
28
+ },
29
+ close: async () => transport.close(),
30
+ abort: () => transport.close(),
31
+ };
32
+ }
33
+ export function readyNativeAdmissionV3(candidate, carrier) {
34
+ let finalized = false;
35
+ return {
36
+ candidate,
37
+ openAdmissionChannel: async (signal) => {
38
+ const options = signal === undefined ? {} : { signal };
39
+ const stream = carrier.kind === "webtransport"
40
+ ? await carrier.acceptStream(options)
41
+ : await carrier.openStream(options);
42
+ return {
43
+ framing: "stream",
44
+ stream,
45
+ abort: (error) => stream.abort(error),
46
+ };
47
+ },
48
+ finalize: () => {
49
+ if (finalized)
50
+ throw new TransportFailureV3("connection_failed");
51
+ finalized = true;
52
+ return adaptNativeCarrierSessionV3(carrier);
53
+ },
54
+ close: async () => await carrier.close(),
55
+ abort: () => carrier.abort({ code: 6, reason: "candidate aborted" }),
56
+ };
57
+ }
58
+ function waitForWebSocketOpen(socket, expectedProtocol, signal) {
59
+ if (signal.aborted)
60
+ return Promise.reject(signal.reason);
61
+ if (socket.readyState === 1) {
62
+ if (socket.protocol === expectedProtocol)
63
+ return Promise.resolve();
64
+ closeWebSocketAfterProtocolMismatch(socket);
65
+ return Promise.reject(new TransportFailureV3("connection_failed"));
66
+ }
67
+ return new Promise((resolve, reject) => {
68
+ let settled = false;
69
+ const cleanup = () => {
70
+ socket.removeEventListener("open", open);
71
+ socket.removeEventListener("error", error);
72
+ socket.removeEventListener("close", close);
73
+ signal.removeEventListener("abort", abort);
74
+ };
75
+ const finish = (failure) => {
76
+ if (settled)
77
+ return;
78
+ settled = true;
79
+ cleanup();
80
+ failure === undefined ? resolve() : reject(failure);
81
+ };
82
+ const open = () => {
83
+ if (socket.protocol === expectedProtocol) {
84
+ finish();
85
+ return;
86
+ }
87
+ closeWebSocketAfterProtocolMismatch(socket);
88
+ finish(new TransportFailureV3("connection_failed"));
89
+ };
90
+ const error = (event) => finish(new TransportFailureV3("connection_failed", undefined, event));
91
+ const close = () => finish(new TransportFailureV3("connection_failed"));
92
+ const abort = () => {
93
+ socket.close();
94
+ finish(signal.reason);
95
+ };
96
+ socket.addEventListener("open", open);
97
+ socket.addEventListener("error", error);
98
+ socket.addEventListener("close", close);
99
+ signal.addEventListener("abort", abort, { once: true });
100
+ if (signal.aborted)
101
+ abort();
102
+ });
103
+ }
104
+ function closeWebSocketAfterProtocolMismatch(socket) {
105
+ try {
106
+ socket.close();
107
+ }
108
+ catch { /* best effort cleanup after a failed handshake */ }
109
+ }
@@ -0,0 +1,34 @@
1
+ import type { CertificatePinV3, TransportSecurityPolicyV3 } from "./artifact.js";
2
+ export type TransportFailureCodeV3 = "invalid_artifact" | "expired_artifact" | "tls_unsupported" | "tls_policy_expired" | "tls_failed" | "connection_failed";
3
+ export type TransportFailureDetailV3 = "ca_untrusted" | "pin_mismatch" | "unknown" | "browser_pin_opaque";
4
+ export declare class TransportFailureV3 extends Error {
5
+ readonly code: TransportFailureCodeV3;
6
+ readonly detail?: TransportFailureDetailV3 | undefined;
7
+ constructor(code: TransportFailureCodeV3, detail?: TransportFailureDetailV3 | undefined, cause?: unknown);
8
+ }
9
+ export type PublicConnectErrorCodeV3 = "artifact_invalid" | "expired_artifact" | "transport_security_unsupported" | "transport_security_failed" | "connection_failed";
10
+ export type RetryDispositionV3 = Readonly<{
11
+ kind: "terminal";
12
+ }> | Readonly<{
13
+ kind: "retryable";
14
+ }> | Readonly<{
15
+ kind: "retry_after";
16
+ absoluteUnixMilliseconds: number;
17
+ }>;
18
+ export declare class ConnectErrorV3 extends Error {
19
+ readonly code: PublicConnectErrorCodeV3;
20
+ readonly disposition: RetryDispositionV3;
21
+ constructor(code: PublicConnectErrorCodeV3, disposition: RetryDispositionV3);
22
+ }
23
+ export type TransportSecuritySnapshotV3 = Readonly<{
24
+ mode: "ca";
25
+ }> | Readonly<{
26
+ mode: "pin";
27
+ activePins: readonly CertificatePinV3[];
28
+ activeLeafDerSHA256: readonly Uint8Array[];
29
+ }>;
30
+ export declare function snapshotTransportSecurityPolicyV3(policy: TransportSecurityPolicyV3, attemptNowUnixSeconds: number, supportedModes: readonly ("ca" | "pin")[]): TransportSecuritySnapshotV3;
31
+ export declare function projectTransportFailureV3(failure: TransportFailureV3, _policyMode: "ca" | "pin"): ConnectErrorV3;
32
+ export declare function validateRetryDispositionV3(value: RetryDispositionV3): RetryDispositionV3;
33
+ export declare function aggregateRetryDispositionsV3(values: readonly RetryDispositionV3[]): RetryDispositionV3;
34
+ export declare function controllerBackoffMillisecondsV3(consecutiveFailure: number): number;
@@ -0,0 +1,100 @@
1
+ import { base64urlDecode } from "../utils/base64url.js";
2
+ export class TransportFailureV3 extends Error {
3
+ code;
4
+ detail;
5
+ constructor(code, detail, cause) {
6
+ super("Flowersec v3 transport attempt failed", { cause });
7
+ this.code = code;
8
+ this.detail = detail;
9
+ this.name = "TransportFailureV3";
10
+ }
11
+ }
12
+ export class ConnectErrorV3 extends Error {
13
+ code;
14
+ disposition;
15
+ constructor(code, disposition) {
16
+ super(`Flowersec connection failed (code=${code})`);
17
+ this.code = code;
18
+ this.disposition = disposition;
19
+ this.name = "ConnectError";
20
+ }
21
+ }
22
+ export function snapshotTransportSecurityPolicyV3(policy, attemptNowUnixSeconds, supportedModes) {
23
+ if (!Number.isSafeInteger(attemptNowUnixSeconds) || attemptNowUnixSeconds < 0) {
24
+ throw new TransportFailureV3("invalid_artifact");
25
+ }
26
+ if (!supportedModes.includes(policy.mode))
27
+ throw new TransportFailureV3("tls_unsupported");
28
+ if (policy.mode === "ca")
29
+ return Object.freeze({ mode: "ca" });
30
+ const activePins = policy.pins.filter((pin) => attemptNowUnixSeconds < pin.not_after_unix_s);
31
+ if (activePins.length === 0)
32
+ throw new TransportFailureV3("tls_policy_expired");
33
+ let decoded;
34
+ try {
35
+ decoded = activePins.map((pin) => {
36
+ const bytes = base64urlDecode(pin.value_b64u);
37
+ if (bytes.length !== 32)
38
+ throw new Error("invalid pin length");
39
+ return bytes;
40
+ });
41
+ }
42
+ catch (error) {
43
+ throw new TransportFailureV3("invalid_artifact", undefined, error);
44
+ }
45
+ return Object.freeze({
46
+ mode: "pin",
47
+ activePins: Object.freeze([...activePins]),
48
+ activeLeafDerSHA256: Object.freeze(decoded),
49
+ });
50
+ }
51
+ export function projectTransportFailureV3(failure, _policyMode) {
52
+ switch (failure.code) {
53
+ case "invalid_artifact":
54
+ return new ConnectErrorV3("artifact_invalid", terminal());
55
+ case "expired_artifact":
56
+ return new ConnectErrorV3("expired_artifact", retryable());
57
+ case "tls_unsupported":
58
+ return new ConnectErrorV3("transport_security_unsupported", terminal());
59
+ case "tls_policy_expired":
60
+ case "tls_failed":
61
+ return new ConnectErrorV3("transport_security_failed", terminal());
62
+ case "connection_failed":
63
+ return new ConnectErrorV3("connection_failed", retryable());
64
+ }
65
+ }
66
+ export function validateRetryDispositionV3(value) {
67
+ if (value.kind === "terminal")
68
+ return terminal();
69
+ if (value.kind === "retryable")
70
+ return retryable();
71
+ if (value.kind !== "retry_after" || !Number.isSafeInteger(value.absoluteUnixMilliseconds) ||
72
+ value.absoluteUnixMilliseconds < 0 || value.absoluteUnixMilliseconds > 253_402_300_799_999) {
73
+ throw new ConnectErrorV3("artifact_invalid", terminal());
74
+ }
75
+ return Object.freeze({ kind: "retry_after", absoluteUnixMilliseconds: value.absoluteUnixMilliseconds });
76
+ }
77
+ export function aggregateRetryDispositionsV3(values) {
78
+ let latest;
79
+ let canRetry = false;
80
+ for (const input of values) {
81
+ const value = validateRetryDispositionV3(input);
82
+ if (value.kind === "retry_after")
83
+ latest = Math.max(latest ?? 0, value.absoluteUnixMilliseconds);
84
+ else if (value.kind === "retryable")
85
+ canRetry = true;
86
+ }
87
+ if (latest !== undefined)
88
+ return Object.freeze({ kind: "retry_after", absoluteUnixMilliseconds: latest });
89
+ return canRetry ? retryable() : terminal();
90
+ }
91
+ export function controllerBackoffMillisecondsV3(consecutiveFailure) {
92
+ if (!Number.isSafeInteger(consecutiveFailure) || consecutiveFailure < 1) {
93
+ throw new RangeError("consecutive failure ordinal must be positive");
94
+ }
95
+ if (consecutiveFailure >= 8)
96
+ return 30_000;
97
+ return Math.min(250 * 2 ** (consecutiveFailure - 1), 30_000);
98
+ }
99
+ const terminal = () => Object.freeze({ kind: "terminal" });
100
+ const retryable = () => Object.freeze({ kind: "retryable" });