@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.
@@ -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(new Promise((resolve) => setTimeout(resolve, 0)), signal);
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 instanceof DOMException && error.name === "AbortError")
74
- throw error;
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 error;
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 DOMException("operation aborted", "AbortError");
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
- constructor(code: "failed" | "closed" | "canceled", failure?: ConnectionControllerFailureV3 | undefined, retryDisposition?: RetryDispositionV3 | undefined);
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
- constructor(code, failure, retryDisposition) {
16
- super(`Flowersec v3 connection controller stopped (code=${code})`);
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
- throw new ConnectionControllerV3Error("failed", this.#failure, this.#disposition);
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
- throw new ConnectionControllerV3Error("closed", this.#failure, this.#disposition);
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 = () => finish(undefined, new ConnectionControllerV3Error("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.disposition);
194
- if (error.disposition.kind === "terminal" || this.#attemptBudgetExhausted())
231
+ this.#recordFailure("artifact", error.code, error.retryDisposition);
232
+ if (error.retryDisposition.kind === "terminal" || this.#attemptBudgetExhausted())
195
233
  return;
196
- pendingDisposition = error.disposition;
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.disposition);
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.disposition);
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.disposition;
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.disposition);
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.disposition);
360
+ validateRetryDispositionV3(sessionFailure.retryDisposition);
323
361
  }
324
362
  catch {
325
363
  sessionFailure = new ConnectErrorV3("connection_failed", { kind: "terminal" });
326
364
  }
327
- const disposition = sessionFailure.disposition;
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.disposition;
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.disposition;
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.disposition);
369
- if (postSpendResult.error.disposition.kind === "terminal" || this.#attemptBudgetExhausted())
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.disposition;
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.disposition);
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.disposition;
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.disposition);
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.disposition);
447
+ this.#recordFailure("connect", terminal.code, terminal.retryDisposition);
410
448
  return;
411
449
  }
412
- this.#recordFailure("connect", refresh.code, refresh.disposition);
413
- if (refresh.disposition.kind === "terminal" || this.#attemptBudgetExhausted())
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.disposition;
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.disposition),
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.disposition));
737
+ const normalizedError = new ConnectErrorV3(error.code, validateRetryDispositionV3(error.retryDisposition));
700
738
  return Object.freeze({ kind, error: normalizedError });
701
739
  }
702
740
  catch {
@@ -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.absoluteUnixMilliseconds
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" || !Number.isSafeInteger(value.absoluteUnixMilliseconds) ||
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
- return value;
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) {
@@ -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) {
@@ -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 disposition: RetryDispositionV3;
21
- constructor(code: PublicConnectErrorCodeV3, disposition: RetryDispositionV3);
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: RetryDispositionV3): RetryDispositionV3;
33
- export declare function aggregateRetryDispositionsV3(values: readonly RetryDispositionV3[]): RetryDispositionV3;
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 {};
@@ -11,13 +11,15 @@ export class TransportFailureV3 extends Error {
11
11
  }
12
12
  export class ConnectErrorV3 extends Error {
13
13
  code;
14
- disposition;
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
- 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) {
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.absoluteUnixMilliseconds);
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 Object.freeze({ kind: "retry_after", absoluteUnixMilliseconds: latest });
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
+ }
@@ -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;
@@ -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(new Promise((resolve) => setTimeout(resolve, 0)), signal);
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) {