@floegence/flowersec-core 3.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -7
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/browser/index.d.ts +6 -2
- package/dist/browser/index.js +3 -1
- package/dist/facade.d.ts +25 -8
- package/dist/facade.js +15 -4
- package/dist/interop/serverParityPeer.js +219 -3
- package/dist/node/index.d.ts +26 -10
- package/dist/node/index.js +12 -4
- package/dist/public/contract.d.ts +6 -3
- package/dist/v2/protocol.js +2 -0
- package/dist/v2/session.d.ts +4 -0
- package/dist/v2/session.js +15 -2
- package/dist/v2/unreliableMessage.js +8 -6
- package/dist/v3/connectionController.d.ts +10 -1
- package/dist/v3/connectionController.js +70 -32
- package/dist/v3/controller.js +14 -4
- package/dist/v3/protocol.js +2 -0
- package/dist/v3/security.d.ts +15 -4
- package/dist/v3/security.js +32 -12
- package/dist/v3/session.d.ts +4 -0
- package/dist/v3/session.js +15 -2
- package/dist/v3/unreliableMessage.js +8 -6
- package/package.json +5 -5
- package/sbom/cyclonedx.json +31 -31
- package/sbom/spdx.json +36 -36
package/dist/v2/session.js
CHANGED
|
@@ -140,6 +140,7 @@ export class SessionV2 {
|
|
|
140
140
|
idleWatchdogStarted = false;
|
|
141
141
|
idleTimerCancel;
|
|
142
142
|
terminationState = deferred();
|
|
143
|
+
terminationController = new AbortController();
|
|
143
144
|
rpcRouter;
|
|
144
145
|
constructor(carrier, control, controlReader, config, material) {
|
|
145
146
|
this.carrier = carrier;
|
|
@@ -269,6 +270,9 @@ export class SessionV2 {
|
|
|
269
270
|
receiveDirectionValue() {
|
|
270
271
|
return this.receiveDirection;
|
|
271
272
|
}
|
|
273
|
+
terminationSignal() {
|
|
274
|
+
return this.terminationController.signal;
|
|
275
|
+
}
|
|
272
276
|
async sendStreamRecord(stream, type, payload, signal) {
|
|
273
277
|
this.assertOpen();
|
|
274
278
|
const inner = encodeInnerRecordV2(type, payload);
|
|
@@ -1080,6 +1084,7 @@ export class SessionV2 {
|
|
|
1080
1084
|
fail(error, abortCarrier = true) {
|
|
1081
1085
|
if (this.lifecycle === "closed")
|
|
1082
1086
|
return;
|
|
1087
|
+
this.terminationController.abort(error);
|
|
1083
1088
|
this.controlTerminalSealed = true;
|
|
1084
1089
|
this.beginClosing();
|
|
1085
1090
|
const normalPeerCarrierClose = this.sessionCloseCommitted && error instanceof CarrierError && error.code === "closed";
|
|
@@ -1147,6 +1152,7 @@ class EncryptedStreamV2 {
|
|
|
1147
1152
|
pendingSendRekey;
|
|
1148
1153
|
lastSendRekeyACK;
|
|
1149
1154
|
receiveRekey;
|
|
1155
|
+
receiveRekeyChanged = deferred();
|
|
1150
1156
|
constructor(session, carrier, id, kind, sendEpoch, receiveEpoch, permitRelease, reader) {
|
|
1151
1157
|
this.session = session;
|
|
1152
1158
|
this.carrier = carrier;
|
|
@@ -1256,7 +1262,7 @@ class EncryptedStreamV2 {
|
|
|
1256
1262
|
await raceAbort(pending.acknowledged.promise, signal);
|
|
1257
1263
|
return;
|
|
1258
1264
|
}
|
|
1259
|
-
await raceAbort(
|
|
1265
|
+
await raceAbort(this.receiveRekeyChanged.promise, signal);
|
|
1260
1266
|
if (this.terminalError !== undefined || this.remoteFIN)
|
|
1261
1267
|
return;
|
|
1262
1268
|
}
|
|
@@ -1266,6 +1272,11 @@ class EncryptedStreamV2 {
|
|
|
1266
1272
|
this.receiveRekey = undefined;
|
|
1267
1273
|
}
|
|
1268
1274
|
}
|
|
1275
|
+
notifyReceiveRekeyChanged() {
|
|
1276
|
+
const changed = this.receiveRekeyChanged;
|
|
1277
|
+
this.receiveRekeyChanged = deferred();
|
|
1278
|
+
changed.resolve();
|
|
1279
|
+
}
|
|
1269
1280
|
startPump() {
|
|
1270
1281
|
if (this.pumpStarted)
|
|
1271
1282
|
return;
|
|
@@ -1283,6 +1294,7 @@ class EncryptedStreamV2 {
|
|
|
1283
1294
|
this.pendingSendRekey = undefined;
|
|
1284
1295
|
pendingSendRekey?.armed.reject(error);
|
|
1285
1296
|
pendingSendRekey?.done.reject(error);
|
|
1297
|
+
this.notifyReceiveRekeyChanged();
|
|
1286
1298
|
this.opened.reject(error);
|
|
1287
1299
|
this.data.fail(error);
|
|
1288
1300
|
return true;
|
|
@@ -1412,11 +1424,12 @@ class EncryptedStreamV2 {
|
|
|
1412
1424
|
this.receiveSequence = 0n;
|
|
1413
1425
|
const pending = { transition, epoch: nextEpoch, acknowledged: deferred() };
|
|
1414
1426
|
this.receiveRekey = pending;
|
|
1427
|
+
this.notifyReceiveRekeyChanged();
|
|
1415
1428
|
await this.send(InnerTypeV2.StreamKeyUpdateACK, encodeStreamKeyUpdateACKV2({
|
|
1416
1429
|
logicalStreamID: this.id,
|
|
1417
1430
|
transition,
|
|
1418
1431
|
epoch: nextEpoch,
|
|
1419
|
-
}));
|
|
1432
|
+
}), this.session.terminationSignal());
|
|
1420
1433
|
pending.acknowledged.resolve();
|
|
1421
1434
|
}
|
|
1422
1435
|
receiveStreamKeyUpdateACK(payload) {
|
|
@@ -34,10 +34,12 @@ class InternalUnreliableMessageChannelV2 {
|
|
|
34
34
|
}
|
|
35
35
|
async send(message, options) {
|
|
36
36
|
throwIfAborted(options.signal);
|
|
37
|
-
if (!(message instanceof Uint8Array) || message.byteLength < 1
|
|
38
|
-
message.byteLength > UNRELIABLE_MESSAGE_MAX_PLAINTEXT_BYTES_V2) {
|
|
37
|
+
if (!(message instanceof Uint8Array) || message.byteLength < 1) {
|
|
39
38
|
throw new UnreliableMessageError("invalid_message");
|
|
40
39
|
}
|
|
40
|
+
if (message.byteLength > UNRELIABLE_MESSAGE_MAX_PLAINTEXT_BYTES_V2) {
|
|
41
|
+
throw new UnreliableMessageError("too_large");
|
|
42
|
+
}
|
|
41
43
|
const payload = message.slice();
|
|
42
44
|
const expiresAt = requireFutureExpiry(options.expiresAtUnixMs, this.now());
|
|
43
45
|
if (expiresAt === undefined)
|
|
@@ -70,8 +72,8 @@ class InternalUnreliableMessageChannelV2 {
|
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
catch (error) {
|
|
73
|
-
if (error
|
|
74
|
-
throw
|
|
75
|
+
if (isAbortError(error))
|
|
76
|
+
throw new UnreliableMessageError("canceled");
|
|
75
77
|
throw new UnreliableMessageError("operation_failed");
|
|
76
78
|
}
|
|
77
79
|
finally {
|
|
@@ -87,7 +89,7 @@ class InternalUnreliableMessageChannelV2 {
|
|
|
87
89
|
}
|
|
88
90
|
catch (error) {
|
|
89
91
|
if (isAbortError(error))
|
|
90
|
-
throw
|
|
92
|
+
throw new UnreliableMessageError("canceled");
|
|
91
93
|
throw new UnreliableMessageError("closed");
|
|
92
94
|
}
|
|
93
95
|
const decoded = decodeHeader(wire);
|
|
@@ -219,7 +221,7 @@ function requireFutureExpiry(value, now) {
|
|
|
219
221
|
}
|
|
220
222
|
function throwIfAborted(signal) {
|
|
221
223
|
if (signal?.aborted === true)
|
|
222
|
-
throw new
|
|
224
|
+
throw new UnreliableMessageError("canceled");
|
|
223
225
|
}
|
|
224
226
|
function isAbortError(error) {
|
|
225
227
|
return error instanceof DOMException && error.name === "AbortError" ||
|
|
@@ -57,6 +57,14 @@ export type ConnectionControllerSnapshotV3<Session extends ManagedSessionV3 = Ma
|
|
|
57
57
|
failure?: ConnectionControllerFailureV3;
|
|
58
58
|
retryDisposition?: RetryDispositionV3;
|
|
59
59
|
}>;
|
|
60
|
+
export type ConnectionDiagnosticV3 = Readonly<{
|
|
61
|
+
state: ConnectionControllerStateV3;
|
|
62
|
+
attempt: number;
|
|
63
|
+
failure?: ConnectionControllerFailureV3;
|
|
64
|
+
retryDisposition?: RetryDispositionV3;
|
|
65
|
+
}>;
|
|
66
|
+
/** Produces a stable diagnostic value without retaining a Session or transport detail. */
|
|
67
|
+
export declare function connectionDiagnosticV3(snapshot: ConnectionControllerSnapshotV3): ConnectionDiagnosticV3;
|
|
60
68
|
export type ConnectionControllerOptionsV3 = Readonly<{
|
|
61
69
|
maximumAttempts?: number;
|
|
62
70
|
clock?: ControllerClockV3;
|
|
@@ -68,7 +76,8 @@ export declare class ConnectionControllerV3Error extends Error {
|
|
|
68
76
|
readonly code: "failed" | "closed" | "canceled";
|
|
69
77
|
readonly failure?: ConnectionControllerFailureV3 | undefined;
|
|
70
78
|
readonly retryDisposition?: RetryDispositionV3 | undefined;
|
|
71
|
-
|
|
79
|
+
readonly diagnostic: ConnectionDiagnosticV3;
|
|
80
|
+
constructor(code: "failed" | "closed" | "canceled", failure?: ConnectionControllerFailureV3 | undefined, retryDisposition?: RetryDispositionV3 | undefined, diagnostic?: ConnectionDiagnosticV3);
|
|
72
81
|
}
|
|
73
82
|
export declare class ConnectionControllerV3<Session extends ManagedSessionV3 = ManagedSessionV3> {
|
|
74
83
|
#private;
|
|
@@ -8,16 +8,40 @@ const ARTIFACT_SOURCE_FAILURE_CODES_V3 = new Set([
|
|
|
8
8
|
"connection_failed",
|
|
9
9
|
"expired_artifact",
|
|
10
10
|
]);
|
|
11
|
+
/** Produces a stable diagnostic value without retaining a Session or transport detail. */
|
|
12
|
+
export function connectionDiagnosticV3(snapshot) {
|
|
13
|
+
const failure = snapshot.failure === undefined
|
|
14
|
+
? undefined
|
|
15
|
+
: Object.freeze({ phase: snapshot.failure.phase, code: snapshot.failure.code });
|
|
16
|
+
const retryDisposition = snapshot.retryDisposition === undefined
|
|
17
|
+
? undefined
|
|
18
|
+
: validateRetryDispositionV3(snapshot.retryDisposition);
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
state: snapshot.state,
|
|
21
|
+
attempt: snapshot.attempt,
|
|
22
|
+
...(failure === undefined ? {} : { failure }),
|
|
23
|
+
...(retryDisposition === undefined ? {} : { retryDisposition }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
11
26
|
export class ConnectionControllerV3Error extends Error {
|
|
12
27
|
code;
|
|
13
28
|
failure;
|
|
14
29
|
retryDisposition;
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
diagnostic;
|
|
31
|
+
constructor(code, failure, retryDisposition, diagnostic) {
|
|
32
|
+
super(`Flowersec connection controller stopped (code=${code})`);
|
|
17
33
|
this.code = code;
|
|
18
34
|
this.failure = failure;
|
|
19
35
|
this.retryDisposition = retryDisposition;
|
|
20
36
|
this.name = "ConnectionControllerError";
|
|
37
|
+
this.diagnostic = diagnostic ?? Object.freeze({
|
|
38
|
+
state: code === "failed" ? "failed" : code === "closed" ? "closed" : "idle",
|
|
39
|
+
attempt: 0,
|
|
40
|
+
...(failure === undefined ? {} : { failure }),
|
|
41
|
+
...(retryDisposition === undefined ? {} : {
|
|
42
|
+
retryDisposition: validateRetryDispositionV3(retryDisposition),
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
21
45
|
}
|
|
22
46
|
}
|
|
23
47
|
export class ConnectionControllerV3 {
|
|
@@ -95,15 +119,22 @@ export class ConnectionControllerV3 {
|
|
|
95
119
|
if (this.currentSession !== undefined)
|
|
96
120
|
return this.currentSession;
|
|
97
121
|
if (this.#state === "failed") {
|
|
98
|
-
|
|
122
|
+
const snapshot = this.#snapshot();
|
|
123
|
+
throw new ConnectionControllerV3Error("failed", this.#failure, this.#disposition, connectionDiagnosticV3(snapshot));
|
|
99
124
|
}
|
|
100
125
|
if (this.#state === "closed") {
|
|
101
|
-
|
|
126
|
+
const snapshot = this.#snapshot();
|
|
127
|
+
throw new ConnectionControllerV3Error("closed", this.#failure, this.#disposition, connectionDiagnosticV3(snapshot));
|
|
128
|
+
}
|
|
129
|
+
if (options.signal?.aborted === true) {
|
|
130
|
+
throw new ConnectionControllerV3Error("canceled", undefined, undefined, connectionDiagnosticV3(this.#snapshot()));
|
|
102
131
|
}
|
|
103
|
-
if (options.signal?.aborted === true)
|
|
104
|
-
throw new ConnectionControllerV3Error("canceled");
|
|
105
132
|
return await new Promise((resolve, reject) => {
|
|
133
|
+
let settled = false;
|
|
106
134
|
const finish = (session, error) => {
|
|
135
|
+
if (settled)
|
|
136
|
+
return;
|
|
137
|
+
settled = true;
|
|
107
138
|
this.#listeners.delete(listener);
|
|
108
139
|
options.signal?.removeEventListener("abort", canceled);
|
|
109
140
|
session === undefined ? reject(error) : resolve(session);
|
|
@@ -113,15 +144,22 @@ export class ConnectionControllerV3 {
|
|
|
113
144
|
finish(snapshot.currentSession);
|
|
114
145
|
}
|
|
115
146
|
else if (snapshot.state === "failed") {
|
|
116
|
-
finish(undefined, new ConnectionControllerV3Error("failed", snapshot.failure, snapshot.retryDisposition));
|
|
147
|
+
finish(undefined, new ConnectionControllerV3Error("failed", snapshot.failure, snapshot.retryDisposition, connectionDiagnosticV3(snapshot)));
|
|
117
148
|
}
|
|
118
149
|
else if (snapshot.state === "closed") {
|
|
119
|
-
finish(undefined, new ConnectionControllerV3Error("closed", snapshot.failure, snapshot.retryDisposition));
|
|
150
|
+
finish(undefined, new ConnectionControllerV3Error("closed", snapshot.failure, snapshot.retryDisposition, connectionDiagnosticV3(snapshot)));
|
|
120
151
|
}
|
|
121
152
|
};
|
|
122
|
-
const canceled = () =>
|
|
153
|
+
const canceled = () => {
|
|
154
|
+
const snapshot = this.#snapshot();
|
|
155
|
+
finish(undefined, new ConnectionControllerV3Error("canceled", undefined, undefined, connectionDiagnosticV3(snapshot)));
|
|
156
|
+
};
|
|
123
157
|
this.#listeners.add(listener);
|
|
124
158
|
options.signal?.addEventListener("abort", canceled, { once: true });
|
|
159
|
+
if (options.signal?.aborted === true)
|
|
160
|
+
canceled();
|
|
161
|
+
else
|
|
162
|
+
listener(this.#snapshot());
|
|
125
163
|
});
|
|
126
164
|
}
|
|
127
165
|
close() {
|
|
@@ -190,10 +228,10 @@ export class ConnectionControllerV3 {
|
|
|
190
228
|
if (acquisition.kind === "failure") {
|
|
191
229
|
const error = sourceFailure(acquisition);
|
|
192
230
|
const ordinal = this.#cycle.recordFailedAcquisitionOrLease();
|
|
193
|
-
this.#recordFailure("artifact", error.code, error.
|
|
194
|
-
if (error.
|
|
231
|
+
this.#recordFailure("artifact", error.code, error.retryDisposition);
|
|
232
|
+
if (error.retryDisposition.kind === "terminal" || this.#attemptBudgetExhausted())
|
|
195
233
|
return;
|
|
196
|
-
pendingDisposition = error.
|
|
234
|
+
pendingDisposition = error.retryDisposition;
|
|
197
235
|
void ordinal;
|
|
198
236
|
continue;
|
|
199
237
|
}
|
|
@@ -222,7 +260,7 @@ export class ConnectionControllerV3 {
|
|
|
222
260
|
await retireArtifactLeaseV3(claim);
|
|
223
261
|
this.#cycle.recordFailedAcquisitionOrLease();
|
|
224
262
|
const error = new ConnectErrorV3("artifact_invalid", { kind: "terminal" });
|
|
225
|
-
this.#recordFailure("connect", error.code, error.
|
|
263
|
+
this.#recordFailure("connect", error.code, error.retryDisposition);
|
|
226
264
|
return;
|
|
227
265
|
}
|
|
228
266
|
try {
|
|
@@ -232,12 +270,12 @@ export class ConnectionControllerV3 {
|
|
|
232
270
|
await retireArtifactLeaseV3(claim);
|
|
233
271
|
this.#cycle.recordFailedAcquisitionOrLease();
|
|
234
272
|
const error = new ConnectErrorV3("expired_artifact", { kind: "retryable" });
|
|
235
|
-
this.#recordFailure("connect", error.code, error.
|
|
273
|
+
this.#recordFailure("connect", error.code, error.retryDisposition);
|
|
236
274
|
if (this.#attemptBudgetExhausted())
|
|
237
275
|
return;
|
|
238
276
|
next = "primary";
|
|
239
277
|
replacementContext = undefined;
|
|
240
|
-
pendingDisposition = error.
|
|
278
|
+
pendingDisposition = error.retryDisposition;
|
|
241
279
|
continue;
|
|
242
280
|
}
|
|
243
281
|
const candidates = next === "replacement"
|
|
@@ -249,7 +287,7 @@ export class ConnectionControllerV3 {
|
|
|
249
287
|
const error = next === "replacement"
|
|
250
288
|
? replacementTerminal(replacementContext)
|
|
251
289
|
: this.#cycle.blockedPolicyTerminal();
|
|
252
|
-
this.#recordFailure("connect", error.code, error.
|
|
290
|
+
this.#recordFailure("connect", error.code, error.retryDisposition);
|
|
253
291
|
return;
|
|
254
292
|
}
|
|
255
293
|
let rawResult;
|
|
@@ -319,12 +357,12 @@ export class ConnectionControllerV3 {
|
|
|
319
357
|
let sessionFailure;
|
|
320
358
|
try {
|
|
321
359
|
sessionFailure = this.#projectSessionFailure(termination.error);
|
|
322
|
-
validateRetryDispositionV3(sessionFailure.
|
|
360
|
+
validateRetryDispositionV3(sessionFailure.retryDisposition);
|
|
323
361
|
}
|
|
324
362
|
catch {
|
|
325
363
|
sessionFailure = new ConnectErrorV3("connection_failed", { kind: "terminal" });
|
|
326
364
|
}
|
|
327
|
-
const disposition = sessionFailure.
|
|
365
|
+
const disposition = sessionFailure.retryDisposition;
|
|
328
366
|
this.#cycle.recordFailedAcquisitionOrLease();
|
|
329
367
|
this.#recordFailure("session", sessionFailure.code, disposition);
|
|
330
368
|
if (disposition.kind === "terminal")
|
|
@@ -345,13 +383,13 @@ export class ConnectionControllerV3 {
|
|
|
345
383
|
const error = next === "replacement" && result.error.code !== "expired_artifact"
|
|
346
384
|
? replacementTerminal(replacementContext)
|
|
347
385
|
: result.error;
|
|
348
|
-
const disposition = error.
|
|
386
|
+
const disposition = error.retryDisposition;
|
|
349
387
|
this.#recordFailure("connect", error.code, disposition);
|
|
350
388
|
if (disposition.kind === "terminal" || this.#attemptBudgetExhausted())
|
|
351
389
|
return;
|
|
352
390
|
next = "primary";
|
|
353
391
|
replacementContext = undefined;
|
|
354
|
-
pendingDisposition = result.error.
|
|
392
|
+
pendingDisposition = result.error.retryDisposition;
|
|
355
393
|
continue;
|
|
356
394
|
}
|
|
357
395
|
if (result.kind === "post_spend_failure") {
|
|
@@ -365,12 +403,12 @@ export class ConnectionControllerV3 {
|
|
|
365
403
|
};
|
|
366
404
|
}
|
|
367
405
|
this.#cycle.recordFailedAcquisitionOrLease();
|
|
368
|
-
this.#recordFailure("connect", postSpendResult.error.code, postSpendResult.error.
|
|
369
|
-
if (postSpendResult.error.
|
|
406
|
+
this.#recordFailure("connect", postSpendResult.error.code, postSpendResult.error.retryDisposition);
|
|
407
|
+
if (postSpendResult.error.retryDisposition.kind === "terminal" || this.#attemptBudgetExhausted())
|
|
370
408
|
return;
|
|
371
409
|
next = "primary";
|
|
372
410
|
replacementContext = undefined;
|
|
373
|
-
pendingDisposition = postSpendResult.error.
|
|
411
|
+
pendingDisposition = postSpendResult.error.retryDisposition;
|
|
374
412
|
continue;
|
|
375
413
|
}
|
|
376
414
|
if (artifactLeaseStateV3(claim) !== "claimed") {
|
|
@@ -382,17 +420,17 @@ export class ConnectionControllerV3 {
|
|
|
382
420
|
this.#cycle.recordFailedAcquisitionOrLease();
|
|
383
421
|
if (isExpired(artifact, this.#nowUnixSeconds)) {
|
|
384
422
|
const error = new ConnectErrorV3("expired_artifact", { kind: "retryable" });
|
|
385
|
-
this.#recordFailure("connect", error.code, error.
|
|
423
|
+
this.#recordFailure("connect", error.code, error.retryDisposition);
|
|
386
424
|
if (this.#attemptBudgetExhausted())
|
|
387
425
|
return;
|
|
388
426
|
next = "primary";
|
|
389
427
|
replacementContext = undefined;
|
|
390
|
-
pendingDisposition = error.
|
|
428
|
+
pendingDisposition = error.retryDisposition;
|
|
391
429
|
continue;
|
|
392
430
|
}
|
|
393
431
|
if (next === "replacement") {
|
|
394
432
|
const error = replacementTerminal(replacementContext);
|
|
395
|
-
this.#recordFailure("connect", error.code, error.
|
|
433
|
+
this.#recordFailure("connect", error.code, error.retryDisposition);
|
|
396
434
|
return;
|
|
397
435
|
}
|
|
398
436
|
const triggerKeys = blockPolicyRefreshTriggersV3(artifact.path.kind, result.failures, this.#cycle);
|
|
@@ -406,14 +444,14 @@ export class ConnectionControllerV3 {
|
|
|
406
444
|
}
|
|
407
445
|
if (triggerKeys.size > 0 && refresh.code === "connection_failed") {
|
|
408
446
|
const terminal = this.#cycle.blockedPolicyTerminal();
|
|
409
|
-
this.#recordFailure("connect", terminal.code, terminal.
|
|
447
|
+
this.#recordFailure("connect", terminal.code, terminal.retryDisposition);
|
|
410
448
|
return;
|
|
411
449
|
}
|
|
412
|
-
this.#recordFailure("connect", refresh.code, refresh.
|
|
413
|
-
if (refresh.
|
|
450
|
+
this.#recordFailure("connect", refresh.code, refresh.retryDisposition);
|
|
451
|
+
if (refresh.retryDisposition.kind === "terminal" || this.#attemptBudgetExhausted())
|
|
414
452
|
return;
|
|
415
453
|
next = "primary";
|
|
416
|
-
pendingDisposition = refresh.
|
|
454
|
+
pendingDisposition = refresh.retryDisposition;
|
|
417
455
|
}
|
|
418
456
|
}
|
|
419
457
|
async #acquire() {
|
|
@@ -467,7 +505,7 @@ export class ConnectionControllerV3 {
|
|
|
467
505
|
return {
|
|
468
506
|
kind: "failure",
|
|
469
507
|
code: error.code,
|
|
470
|
-
disposition: validateRetryDispositionV3(error.
|
|
508
|
+
disposition: validateRetryDispositionV3(error.retryDisposition),
|
|
471
509
|
};
|
|
472
510
|
}
|
|
473
511
|
catch {
|
|
@@ -696,7 +734,7 @@ function normalizeLeaseAttemptResultV3(value, candidates, capability) {
|
|
|
696
734
|
const error = Reflect.get(value, "error");
|
|
697
735
|
if (!(error instanceof ConnectErrorV3) || !CONNECT_ERROR_CODES_V3.has(error.code))
|
|
698
736
|
return undefined;
|
|
699
|
-
const normalizedError = new ConnectErrorV3(error.code, validateRetryDispositionV3(error.
|
|
737
|
+
const normalizedError = new ConnectErrorV3(error.code, validateRetryDispositionV3(error.retryDisposition));
|
|
700
738
|
return Object.freeze({ kind, error: normalizedError });
|
|
701
739
|
}
|
|
702
740
|
catch {
|
package/dist/v3/controller.js
CHANGED
|
@@ -206,7 +206,7 @@ export class ControllerRetryWaitV3 {
|
|
|
206
206
|
this.#waiting = true;
|
|
207
207
|
this.#manual = false;
|
|
208
208
|
this.#absoluteDeadline = validated.kind === "retry_after"
|
|
209
|
-
? validated.
|
|
209
|
+
? validated.notBeforeUnixMilliseconds
|
|
210
210
|
: undefined;
|
|
211
211
|
try {
|
|
212
212
|
while (!signal.aborted) {
|
|
@@ -264,11 +264,21 @@ function saturatingDifferenceMilliseconds(deadline, now) {
|
|
|
264
264
|
function validateControllerWaitDisposition(value) {
|
|
265
265
|
if (value.kind === "terminal" || value.kind === "retryable")
|
|
266
266
|
return value;
|
|
267
|
-
if (value.kind !== "retry_after"
|
|
268
|
-
value.absoluteUnixMilliseconds < 0 || value.absoluteUnixMilliseconds > 253_402_300_799_999) {
|
|
267
|
+
if (value.kind !== "retry_after") {
|
|
269
268
|
throw new ConnectErrorV3("artifact_invalid", { kind: "terminal" });
|
|
270
269
|
}
|
|
271
|
-
|
|
270
|
+
const deadline = value.notBeforeUnixMilliseconds ?? value.absoluteUnixMilliseconds;
|
|
271
|
+
if (deadline === undefined || !Number.isSafeInteger(deadline) || deadline < 0 ||
|
|
272
|
+
deadline > 253_402_300_799_999 || value.notBeforeUnixMilliseconds !== undefined &&
|
|
273
|
+
value.absoluteUnixMilliseconds !== undefined &&
|
|
274
|
+
value.notBeforeUnixMilliseconds !== value.absoluteUnixMilliseconds) {
|
|
275
|
+
throw new ConnectErrorV3("artifact_invalid", { kind: "terminal" });
|
|
276
|
+
}
|
|
277
|
+
return Object.freeze({
|
|
278
|
+
kind: "retry_after",
|
|
279
|
+
notBeforeUnixMilliseconds: deadline,
|
|
280
|
+
absoluteUnixMilliseconds: deadline,
|
|
281
|
+
});
|
|
272
282
|
}
|
|
273
283
|
function controllerBackoffForWait(consecutiveFailure) {
|
|
274
284
|
if (!Number.isSafeInteger(consecutiveFailure) || consecutiveFailure < 1) {
|
package/dist/v3/protocol.js
CHANGED
|
@@ -405,6 +405,8 @@ export function decodeStreamKeyUpdateACKV3(raw) {
|
|
|
405
405
|
assertLogicalStreamID(value.logicalStreamID);
|
|
406
406
|
if (value.transition === 0n)
|
|
407
407
|
throw new ProtocolV3Error("stream rekey transition must be non-zero");
|
|
408
|
+
if (value.epoch === 0)
|
|
409
|
+
throw new ProtocolV3Error("stream rekey epoch must be non-zero");
|
|
408
410
|
return value;
|
|
409
411
|
}
|
|
410
412
|
export function buildRecordAAD(h3, logicalStreamID, direction, rawHeader) {
|
package/dist/v3/security.d.ts
CHANGED
|
@@ -13,12 +13,22 @@ export type RetryDispositionV3 = Readonly<{
|
|
|
13
13
|
kind: "retryable";
|
|
14
14
|
}> | Readonly<{
|
|
15
15
|
kind: "retry_after";
|
|
16
|
+
notBeforeUnixMilliseconds: number;
|
|
17
|
+
/** @deprecated Use notBeforeUnixMilliseconds. */
|
|
18
|
+
absoluteUnixMilliseconds?: number;
|
|
19
|
+
}>;
|
|
20
|
+
type RetryDispositionInputV3 = RetryDispositionV3 | Readonly<{
|
|
21
|
+
kind: "retry_after";
|
|
22
|
+
/** @deprecated Use notBeforeUnixMilliseconds. */
|
|
16
23
|
absoluteUnixMilliseconds: number;
|
|
24
|
+
notBeforeUnixMilliseconds?: number;
|
|
17
25
|
}>;
|
|
18
26
|
export declare class ConnectErrorV3 extends Error {
|
|
19
27
|
readonly code: PublicConnectErrorCodeV3;
|
|
20
|
-
readonly
|
|
21
|
-
constructor(code: PublicConnectErrorCodeV3, disposition:
|
|
28
|
+
readonly retryDisposition: RetryDispositionV3;
|
|
29
|
+
constructor(code: PublicConnectErrorCodeV3, disposition: RetryDispositionInputV3);
|
|
30
|
+
/** @deprecated Use retryDisposition. */
|
|
31
|
+
get disposition(): RetryDispositionV3;
|
|
22
32
|
}
|
|
23
33
|
export type TransportSecuritySnapshotV3 = Readonly<{
|
|
24
34
|
mode: "ca";
|
|
@@ -29,6 +39,7 @@ export type TransportSecuritySnapshotV3 = Readonly<{
|
|
|
29
39
|
}>;
|
|
30
40
|
export declare function snapshotTransportSecurityPolicyV3(policy: TransportSecurityPolicyV3, attemptNowUnixSeconds: number, supportedModes: readonly ("ca" | "pin")[]): TransportSecuritySnapshotV3;
|
|
31
41
|
export declare function projectTransportFailureV3(failure: TransportFailureV3, _policyMode: "ca" | "pin"): ConnectErrorV3;
|
|
32
|
-
export declare function validateRetryDispositionV3(value:
|
|
33
|
-
export declare function aggregateRetryDispositionsV3(values: readonly
|
|
42
|
+
export declare function validateRetryDispositionV3(value: RetryDispositionInputV3): RetryDispositionV3;
|
|
43
|
+
export declare function aggregateRetryDispositionsV3(values: readonly RetryDispositionInputV3[]): RetryDispositionV3;
|
|
34
44
|
export declare function controllerBackoffMillisecondsV3(consecutiveFailure: number): number;
|
|
45
|
+
export {};
|
package/dist/v3/security.js
CHANGED
|
@@ -11,13 +11,15 @@ export class TransportFailureV3 extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
export class ConnectErrorV3 extends Error {
|
|
13
13
|
code;
|
|
14
|
-
|
|
14
|
+
retryDisposition;
|
|
15
15
|
constructor(code, disposition) {
|
|
16
16
|
super(`Flowersec connection failed (code=${code})`);
|
|
17
17
|
this.code = code;
|
|
18
|
-
this.disposition = disposition;
|
|
19
18
|
this.name = "ConnectError";
|
|
19
|
+
this.retryDisposition = validateRetryDispositionValueV3(disposition);
|
|
20
20
|
}
|
|
21
|
+
/** @deprecated Use retryDisposition. */
|
|
22
|
+
get disposition() { return this.retryDisposition; }
|
|
21
23
|
}
|
|
22
24
|
export function snapshotTransportSecurityPolicyV3(policy, attemptNowUnixSeconds, supportedModes) {
|
|
23
25
|
if (!Number.isSafeInteger(attemptNowUnixSeconds) || attemptNowUnixSeconds < 0) {
|
|
@@ -64,28 +66,26 @@ export function projectTransportFailureV3(failure, _policyMode) {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
export function validateRetryDispositionV3(value) {
|
|
67
|
-
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (value.kind !== "retry_after" || !Number.isSafeInteger(value.absoluteUnixMilliseconds) ||
|
|
72
|
-
value.absoluteUnixMilliseconds < 0 || value.absoluteUnixMilliseconds > 253_402_300_799_999) {
|
|
69
|
+
try {
|
|
70
|
+
return validateRetryDispositionValueV3(value);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
73
|
throw new ConnectErrorV3("artifact_invalid", terminal());
|
|
74
74
|
}
|
|
75
|
-
return Object.freeze({ kind: "retry_after", absoluteUnixMilliseconds: value.absoluteUnixMilliseconds });
|
|
76
75
|
}
|
|
77
76
|
export function aggregateRetryDispositionsV3(values) {
|
|
78
77
|
let latest;
|
|
79
78
|
let canRetry = false;
|
|
80
79
|
for (const input of values) {
|
|
81
80
|
const value = validateRetryDispositionV3(input);
|
|
82
|
-
if (value.kind === "retry_after")
|
|
83
|
-
latest = Math.max(latest ?? 0, value.
|
|
81
|
+
if (value.kind === "retry_after") {
|
|
82
|
+
latest = Math.max(latest ?? 0, value.notBeforeUnixMilliseconds);
|
|
83
|
+
}
|
|
84
84
|
else if (value.kind === "retryable")
|
|
85
85
|
canRetry = true;
|
|
86
86
|
}
|
|
87
87
|
if (latest !== undefined)
|
|
88
|
-
return
|
|
88
|
+
return retryAfter(latest);
|
|
89
89
|
return canRetry ? retryable() : terminal();
|
|
90
90
|
}
|
|
91
91
|
export function controllerBackoffMillisecondsV3(consecutiveFailure) {
|
|
@@ -98,3 +98,23 @@ export function controllerBackoffMillisecondsV3(consecutiveFailure) {
|
|
|
98
98
|
}
|
|
99
99
|
const terminal = () => Object.freeze({ kind: "terminal" });
|
|
100
100
|
const retryable = () => Object.freeze({ kind: "retryable" });
|
|
101
|
+
const retryAfter = (notBeforeUnixMilliseconds) => Object.freeze({
|
|
102
|
+
kind: "retry_after",
|
|
103
|
+
notBeforeUnixMilliseconds,
|
|
104
|
+
absoluteUnixMilliseconds: notBeforeUnixMilliseconds,
|
|
105
|
+
});
|
|
106
|
+
function validateRetryDispositionValueV3(value) {
|
|
107
|
+
if (value.kind === "terminal")
|
|
108
|
+
return terminal();
|
|
109
|
+
if (value.kind === "retryable")
|
|
110
|
+
return retryable();
|
|
111
|
+
const legacy = value.absoluteUnixMilliseconds;
|
|
112
|
+
const canonical = value.notBeforeUnixMilliseconds;
|
|
113
|
+
const deadline = canonical ?? legacy;
|
|
114
|
+
if (deadline === undefined ||
|
|
115
|
+
canonical !== undefined && legacy !== undefined && canonical !== legacy ||
|
|
116
|
+
!Number.isSafeInteger(deadline) || deadline < 0 || deadline > 253_402_300_799_999) {
|
|
117
|
+
throw new TypeError("invalid Flowersec retry-after deadline");
|
|
118
|
+
}
|
|
119
|
+
return retryAfter(deadline);
|
|
120
|
+
}
|
package/dist/v3/session.d.ts
CHANGED
|
@@ -136,6 +136,7 @@ export declare class SessionV3 implements SessionV3Contract {
|
|
|
136
136
|
private idleDeadlineMs;
|
|
137
137
|
private idleTimerCancel;
|
|
138
138
|
private readonly terminationState;
|
|
139
|
+
private readonly terminationController;
|
|
139
140
|
private readonly rpcRouter;
|
|
140
141
|
constructor(carrier: CarrierSessionV3, control: CarrierStreamV3, controlReader: ExactReader, config: SessionConfigV3, material: HandshakeMaterial);
|
|
141
142
|
openStream(kind: string, options?: InternalStreamOpenOptionsV3): Promise<ByteStreamV3>;
|
|
@@ -150,6 +151,7 @@ export declare class SessionV3 implements SessionV3Contract {
|
|
|
150
151
|
installReceiveRoots(epoch: number, roots: EpochRootsV3): void;
|
|
151
152
|
transcriptHash(): Uint8Array;
|
|
152
153
|
receiveDirectionValue(): DirectionV3;
|
|
154
|
+
terminationSignal(): AbortSignal;
|
|
153
155
|
sendStreamRecord(stream: EncryptedStreamV3, type: InnerTypeV3, payload: Uint8Array, signal?: AbortSignal): Promise<void>;
|
|
154
156
|
readStreamRecord(stream: EncryptedStreamV3): Promise<Readonly<{
|
|
155
157
|
type: InnerTypeV3;
|
|
@@ -244,6 +246,7 @@ declare class EncryptedStreamV3 implements ByteStreamV3 {
|
|
|
244
246
|
private receiveRekey;
|
|
245
247
|
private readonly sendMaterials;
|
|
246
248
|
private readonly receiveMaterials;
|
|
249
|
+
private receiveRekeyChanged;
|
|
247
250
|
constructor(session: SessionV3, carrier: CarrierStreamV3, id: bigint, kind: string, sendEpoch: number, receiveEpoch: number, permitRelease: () => void, reader?: ExactReader);
|
|
248
251
|
read(options?: OperationOptionsV3): Promise<Uint8Array | null>;
|
|
249
252
|
write(payload: Uint8Array, options?: OperationOptionsV3): Promise<number>;
|
|
@@ -259,6 +262,7 @@ declare class EncryptedStreamV3 implements ByteStreamV3 {
|
|
|
259
262
|
}>;
|
|
260
263
|
waitReceiveRekey(transition: bigint, epoch: number, signal?: AbortSignal): Promise<void>;
|
|
261
264
|
publishReceiveRekey(transition: bigint, epoch: number): void;
|
|
265
|
+
private notifyReceiveRekeyChanged;
|
|
262
266
|
startPump(): void;
|
|
263
267
|
markOpen(): void;
|
|
264
268
|
markTerminal(error: Error): boolean;
|
package/dist/v3/session.js
CHANGED
|
@@ -143,6 +143,7 @@ export class SessionV3 {
|
|
|
143
143
|
idleDeadlineMs;
|
|
144
144
|
idleTimerCancel;
|
|
145
145
|
terminationState = deferred();
|
|
146
|
+
terminationController = new AbortController();
|
|
146
147
|
rpcRouter;
|
|
147
148
|
constructor(carrier, control, controlReader, config, material) {
|
|
148
149
|
this.carrier = carrier;
|
|
@@ -279,6 +280,9 @@ export class SessionV3 {
|
|
|
279
280
|
receiveDirectionValue() {
|
|
280
281
|
return this.receiveDirection;
|
|
281
282
|
}
|
|
283
|
+
terminationSignal() {
|
|
284
|
+
return this.terminationController.signal;
|
|
285
|
+
}
|
|
282
286
|
async sendStreamRecord(stream, type, payload, signal) {
|
|
283
287
|
this.assertOpen();
|
|
284
288
|
const inner = encodeInnerRecordV3(type, payload);
|
|
@@ -1192,6 +1196,7 @@ export class SessionV3 {
|
|
|
1192
1196
|
fail(error, abortCarrier = true) {
|
|
1193
1197
|
if (this.lifecycle === "closed")
|
|
1194
1198
|
return;
|
|
1199
|
+
this.terminationController.abort(error);
|
|
1195
1200
|
this.controlTerminalSealed = true;
|
|
1196
1201
|
this.beginClosing();
|
|
1197
1202
|
const pendingSessionRekey = this.pendingSessionRekey;
|
|
@@ -1265,6 +1270,7 @@ class EncryptedStreamV3 {
|
|
|
1265
1270
|
receiveRekey;
|
|
1266
1271
|
sendMaterials = new Map();
|
|
1267
1272
|
receiveMaterials = new Map();
|
|
1273
|
+
receiveRekeyChanged = deferred();
|
|
1268
1274
|
constructor(session, carrier, id, kind, sendEpoch, receiveEpoch, permitRelease, reader) {
|
|
1269
1275
|
this.session = session;
|
|
1270
1276
|
this.carrier = carrier;
|
|
@@ -1374,7 +1380,7 @@ class EncryptedStreamV3 {
|
|
|
1374
1380
|
await raceAbort(pending.acknowledged.promise, signal);
|
|
1375
1381
|
return;
|
|
1376
1382
|
}
|
|
1377
|
-
await raceAbort(
|
|
1383
|
+
await raceAbort(this.receiveRekeyChanged.promise, signal);
|
|
1378
1384
|
if (this.terminalError !== undefined || this.remoteFIN)
|
|
1379
1385
|
return;
|
|
1380
1386
|
}
|
|
@@ -1384,6 +1390,11 @@ class EncryptedStreamV3 {
|
|
|
1384
1390
|
this.receiveRekey = undefined;
|
|
1385
1391
|
}
|
|
1386
1392
|
}
|
|
1393
|
+
notifyReceiveRekeyChanged() {
|
|
1394
|
+
const changed = this.receiveRekeyChanged;
|
|
1395
|
+
this.receiveRekeyChanged = deferred();
|
|
1396
|
+
changed.resolve();
|
|
1397
|
+
}
|
|
1387
1398
|
startPump() {
|
|
1388
1399
|
if (this.pumpStarted)
|
|
1389
1400
|
return;
|
|
@@ -1401,6 +1412,7 @@ class EncryptedStreamV3 {
|
|
|
1401
1412
|
this.pendingSendRekey = undefined;
|
|
1402
1413
|
pendingSendRekey?.armed.reject(error);
|
|
1403
1414
|
pendingSendRekey?.done.reject(error);
|
|
1415
|
+
this.notifyReceiveRekeyChanged();
|
|
1404
1416
|
this.opened.reject(error);
|
|
1405
1417
|
this.data.fail(error);
|
|
1406
1418
|
return true;
|
|
@@ -1558,11 +1570,12 @@ class EncryptedStreamV3 {
|
|
|
1558
1570
|
pruneRecordMaterials(this.receiveMaterials, new Set(this.priorACK === undefined ? [nextEpoch] : [header.epoch, nextEpoch]));
|
|
1559
1571
|
const pending = { transition, epoch: nextEpoch, acknowledged: deferred() };
|
|
1560
1572
|
this.receiveRekey = pending;
|
|
1573
|
+
this.notifyReceiveRekeyChanged();
|
|
1561
1574
|
await this.send(InnerTypeV3.StreamKeyUpdateACK, encodeStreamKeyUpdateACKV3({
|
|
1562
1575
|
logicalStreamID: this.id,
|
|
1563
1576
|
transition,
|
|
1564
1577
|
epoch: nextEpoch,
|
|
1565
|
-
}));
|
|
1578
|
+
}), this.session.terminationSignal());
|
|
1566
1579
|
pending.acknowledged.resolve();
|
|
1567
1580
|
}
|
|
1568
1581
|
receiveStreamKeyUpdateACK(payload) {
|