@floegence/flowersec-core 2.5.3 → 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 (119) 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/framing/jsonframe.d.ts +1 -1
  11. package/dist/framing/jsonframe.js +3 -1
  12. package/dist/interop/proxyServerPeer.js +1 -1
  13. package/dist/interop/serverParityPeer.js +8 -1
  14. package/dist/node/acceptor.d.ts +10 -0
  15. package/dist/node/acceptor.js +53 -7
  16. package/dist/node/acceptorV3.d.ts +57 -0
  17. package/dist/node/acceptorV3.js +227 -0
  18. package/dist/node/connectSessionV3.d.ts +21 -0
  19. package/dist/node/connectSessionV3.js +73 -0
  20. package/dist/node/index.d.ts +15 -10
  21. package/dist/node/index.js +6 -5
  22. package/dist/node/nativeTransportAddon.d.ts +39 -1
  23. package/dist/node/nativeTransportAddon.js +29 -5
  24. package/dist/node/rawQuicAdapterV3.d.ts +10 -0
  25. package/dist/node/rawQuicAdapterV3.js +76 -0
  26. package/dist/node/rawQuicServerV3.d.ts +24 -0
  27. package/dist/node/rawQuicServerV3.js +34 -0
  28. package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
  29. package/dist/node/runtimeAuthorizationV3.js +126 -0
  30. package/dist/node/tunnelRuntimeV3.d.ts +57 -0
  31. package/dist/node/tunnelRuntimeV3.js +857 -0
  32. package/dist/node/v2.d.ts +11 -0
  33. package/dist/node/v2.js +6 -0
  34. package/dist/node/webSocketServerV3.d.ts +27 -0
  35. package/dist/node/webSocketServerV3.js +219 -0
  36. package/dist/proxy/runtime.js +39 -6
  37. package/dist/proxy/serviceWorker.d.ts +2 -0
  38. package/dist/proxy/serviceWorker.js +63 -7
  39. package/dist/proxy/windowBridge.d.ts +1 -0
  40. package/dist/proxy/windowBridge.js +93 -33
  41. package/dist/proxy/wsPatch.d.ts +1 -0
  42. package/dist/proxy/wsPatch.js +39 -6
  43. package/dist/public/streamHandlers.d.ts +3 -0
  44. package/dist/public/streamHandlers.js +6 -1
  45. package/dist/rpc/client.js +9 -7
  46. package/dist/rpc/server.js +44 -20
  47. package/dist/rpc/validate.d.ts +1 -0
  48. package/dist/rpc/validate.js +18 -6
  49. package/dist/v2/index.d.ts +14 -0
  50. package/dist/v2/index.js +7 -0
  51. package/dist/v2/protocol.js +23 -32
  52. package/dist/v2/publicSession.js +4 -0
  53. package/dist/v2/session.d.ts +5 -0
  54. package/dist/v2/session.js +94 -35
  55. package/dist/v3/admissionError.d.ts +4 -0
  56. package/dist/v3/admissionError.js +8 -0
  57. package/dist/v3/artifact.d.ts +145 -0
  58. package/dist/v3/artifact.js +1162 -0
  59. package/dist/v3/artifactLease.d.ts +17 -0
  60. package/dist/v3/artifactLease.js +109 -0
  61. package/dist/v3/browserRuntime.d.ts +37 -0
  62. package/dist/v3/browserRuntime.js +197 -0
  63. package/dist/v3/browserSessionRuntime.d.ts +2 -0
  64. package/dist/v3/browserSessionRuntime.js +21 -0
  65. package/dist/v3/capability.d.ts +32 -0
  66. package/dist/v3/capability.js +345 -0
  67. package/dist/v3/carrier.d.ts +84 -0
  68. package/dist/v3/carrier.js +411 -0
  69. package/dist/v3/connectionController.d.ts +87 -0
  70. package/dist/v3/connectionController.js +805 -0
  71. package/dist/v3/contract.d.ts +65 -0
  72. package/dist/v3/contract.js +3 -0
  73. package/dist/v3/controller.d.ts +43 -0
  74. package/dist/v3/controller.js +303 -0
  75. package/dist/v3/handshake.d.ts +81 -0
  76. package/dist/v3/handshake.js +471 -0
  77. package/dist/v3/jcs.d.ts +6 -0
  78. package/dist/v3/jcs.js +69 -0
  79. package/dist/v3/jsonPreflight.d.ts +1 -0
  80. package/dist/v3/jsonPreflight.js +251 -0
  81. package/dist/v3/nodeRuntime.d.ts +40 -0
  82. package/dist/v3/nodeRuntime.js +315 -0
  83. package/dist/v3/nodeSessionRuntime.d.ts +2 -0
  84. package/dist/v3/nodeSessionRuntime.js +13 -0
  85. package/dist/v3/protocol.d.ts +99 -0
  86. package/dist/v3/protocol.js +773 -0
  87. package/dist/v3/publicApi.d.ts +12 -0
  88. package/dist/v3/publicApi.js +46 -0
  89. package/dist/v3/publicSession.d.ts +1 -0
  90. package/dist/v3/publicSession.js +281 -0
  91. package/dist/v3/retryDisposition.d.ts +16 -0
  92. package/dist/v3/retryDisposition.js +30 -0
  93. package/dist/v3/runtimeAdapters.d.ts +9 -0
  94. package/dist/v3/runtimeAdapters.js +109 -0
  95. package/dist/v3/security.d.ts +34 -0
  96. package/dist/v3/security.js +100 -0
  97. package/dist/v3/serverAdmission.d.ts +38 -0
  98. package/dist/v3/serverAdmission.js +223 -0
  99. package/dist/v3/session.d.ts +318 -0
  100. package/dist/v3/session.js +2289 -0
  101. package/dist/v3/sessionConnector.d.ts +37 -0
  102. package/dist/v3/sessionConnector.js +440 -0
  103. package/dist/v3/streamLifetimeLedger.d.ts +31 -0
  104. package/dist/v3/streamLifetimeLedger.js +131 -0
  105. package/dist/v3/streamMetadata.d.ts +1 -0
  106. package/dist/v3/streamMetadata.js +7 -0
  107. package/dist/v3/transportConstants.d.ts +56 -0
  108. package/dist/v3/transportConstants.js +59 -0
  109. package/dist/v3/unreliableMessage.d.ts +44 -0
  110. package/dist/v3/unreliableMessage.js +284 -0
  111. package/dist/v3/webSocketCarrier.d.ts +31 -0
  112. package/dist/v3/webSocketCarrier.js +370 -0
  113. package/dist/v3/webTransportCarrier.d.ts +27 -0
  114. package/dist/v3/webTransportCarrier.js +534 -0
  115. package/dist/yamux/stream.d.ts +3 -0
  116. package/dist/yamux/stream.js +17 -4
  117. package/package.json +4 -4
  118. package/sbom/cyclonedx.json +6 -6
  119. package/sbom/spdx.json +11 -11
@@ -0,0 +1,805 @@
1
+ import { canonicalizeCandidatesV3, validateArtifactV3, } from "./artifact.js";
2
+ import { artifactLeaseStateV3, claimArtifactLeaseV3, claimedArtifactV3, retireArtifactLeaseV3, } from "./artifactLease.js";
3
+ import { ControllerCycleStateV3, ControllerRetryWaitV3, aggregateCandidateFailuresV3, blockPolicyRefreshTriggersV3, endpointKeyV3, filterBlockedCandidatesV3, selectReplacementCandidatesV3, } from "./controller.js";
4
+ import { ConnectErrorV3, TransportFailureV3, validateRetryDispositionV3, } from "./security.js";
5
+ import { validateRuntimeCapabilityDescriptorV3, } from "./capability.js";
6
+ const ARTIFACT_SOURCE_FAILURE_CODES_V3 = new Set([
7
+ "artifact_invalid",
8
+ "connection_failed",
9
+ "expired_artifact",
10
+ ]);
11
+ export class ConnectionControllerV3Error extends Error {
12
+ code;
13
+ failure;
14
+ retryDisposition;
15
+ constructor(code, failure, retryDisposition) {
16
+ super(`Flowersec v3 connection controller stopped (code=${code})`);
17
+ this.code = code;
18
+ this.failure = failure;
19
+ this.retryDisposition = retryDisposition;
20
+ this.name = "ConnectionControllerError";
21
+ }
22
+ }
23
+ export class ConnectionControllerV3 {
24
+ #source;
25
+ #connector;
26
+ #maximumAttempts;
27
+ #nowUnixSeconds;
28
+ #retry;
29
+ #capabilitySnapshot;
30
+ #projectSessionFailure;
31
+ #lifetime = new AbortController();
32
+ #listeners = new Set();
33
+ #cycle;
34
+ #state = "idle";
35
+ #session;
36
+ #connectedAttempt = 0;
37
+ #failure;
38
+ #disposition;
39
+ #scheduler;
40
+ #closeOperation;
41
+ constructor(source, connector, options) {
42
+ if (source === null || typeof source !== "object" || typeof source.acquire !== "function") {
43
+ throw new TypeError("Flowersec v3 artifact source must provide acquire()");
44
+ }
45
+ if (typeof connector !== "function")
46
+ throw new TypeError("Flowersec v3 lease connector is required");
47
+ const maximumAttempts = options.maximumAttempts ?? 0;
48
+ if (!Number.isSafeInteger(maximumAttempts) || maximumAttempts < 0) {
49
+ throw new TypeError("maximumAttempts must be a non-negative safe integer");
50
+ }
51
+ this.#source = source;
52
+ this.#connector = connector;
53
+ this.#maximumAttempts = maximumAttempts;
54
+ this.#cycle = new ControllerCycleStateV3(maximumAttempts);
55
+ this.#nowUnixSeconds = options.nowUnixSeconds ?? (() => Math.floor(Date.now() / 1_000));
56
+ this.#retry = new ControllerRetryWaitV3(options.clock);
57
+ if (typeof options.capabilitySnapshot !== "function") {
58
+ throw new TypeError("Flowersec v3 capability snapshot factory is required");
59
+ }
60
+ this.#capabilitySnapshot = options.capabilitySnapshot;
61
+ this.#projectSessionFailure = options.projectSessionFailure ?? (() => new ConnectErrorV3("connection_failed", { kind: "terminal" }));
62
+ }
63
+ get state() { return this.#state; }
64
+ get currentSession() { return this.#state === "connected" ? this.#session : undefined; }
65
+ get failure() { return this.#failure; }
66
+ start() {
67
+ if (this.#state !== "idle" || this.#scheduler !== undefined)
68
+ return;
69
+ let resolveScheduler;
70
+ let rejectScheduler;
71
+ this.#scheduler = new Promise((resolve, reject) => {
72
+ resolveScheduler = resolve;
73
+ rejectScheduler = reject;
74
+ });
75
+ void this.#run().then(resolveScheduler, rejectScheduler);
76
+ }
77
+ retryNow() {
78
+ return this.#state === "waiting" && this.#retry.retryNow();
79
+ }
80
+ subscribe(listener) {
81
+ if (typeof listener !== "function")
82
+ throw new TypeError("connection listener must be a function");
83
+ this.#listeners.add(listener);
84
+ try {
85
+ listener(this.#snapshot());
86
+ }
87
+ catch (error) {
88
+ this.#listeners.delete(listener);
89
+ throw error;
90
+ }
91
+ return () => { this.#listeners.delete(listener); };
92
+ }
93
+ async waitForSession(options = {}) {
94
+ if (this.currentSession !== undefined)
95
+ return this.currentSession;
96
+ if (this.#state === "failed") {
97
+ throw new ConnectionControllerV3Error("failed", this.#failure, this.#disposition);
98
+ }
99
+ if (this.#state === "closed") {
100
+ throw new ConnectionControllerV3Error("closed", this.#failure, this.#disposition);
101
+ }
102
+ if (options.signal?.aborted === true)
103
+ throw new ConnectionControllerV3Error("canceled");
104
+ return await new Promise((resolve, reject) => {
105
+ const finish = (session, error) => {
106
+ this.#listeners.delete(listener);
107
+ options.signal?.removeEventListener("abort", canceled);
108
+ session === undefined ? reject(error) : resolve(session);
109
+ };
110
+ const listener = (snapshot) => {
111
+ if (snapshot.state === "connected" && snapshot.currentSession !== undefined) {
112
+ finish(snapshot.currentSession);
113
+ }
114
+ else if (snapshot.state === "failed") {
115
+ finish(undefined, new ConnectionControllerV3Error("failed", snapshot.failure, snapshot.retryDisposition));
116
+ }
117
+ else if (snapshot.state === "closed") {
118
+ finish(undefined, new ConnectionControllerV3Error("closed", snapshot.failure, snapshot.retryDisposition));
119
+ }
120
+ };
121
+ const canceled = () => finish(undefined, new ConnectionControllerV3Error("canceled"));
122
+ this.#listeners.add(listener);
123
+ options.signal?.addEventListener("abort", canceled, { once: true });
124
+ });
125
+ }
126
+ close() {
127
+ if (this.#closeOperation !== undefined)
128
+ return this.#closeOperation;
129
+ let resolveClose;
130
+ const closeOperation = new Promise((resolve) => { resolveClose = resolve; });
131
+ this.#closeOperation = closeOperation;
132
+ const active = this.#session;
133
+ this.#session = undefined;
134
+ this.#disposition = undefined;
135
+ this.#lifetime.abort(new ConnectionControllerV3Error("closed", this.#failure));
136
+ this.#transition("closed");
137
+ void Promise.allSettled([
138
+ this.#scheduler ?? Promise.resolve(),
139
+ active?.close() ?? Promise.resolve(),
140
+ ]).then(() => resolveClose());
141
+ return closeOperation;
142
+ }
143
+ async #run() {
144
+ let next = "primary";
145
+ let pendingDisposition;
146
+ let replacementContext;
147
+ while (!this.#lifetime.signal.aborted) {
148
+ if (pendingDisposition !== undefined) {
149
+ this.#disposition = pendingDisposition;
150
+ const waitOperation = this.#retry.wait(pendingDisposition, this.#cycle.snapshot().consecutiveFailures, this.#lifetime.signal);
151
+ this.#transition("waiting");
152
+ const continued = await waitOperation;
153
+ if (!continued)
154
+ return;
155
+ pendingDisposition = undefined;
156
+ }
157
+ if (!this.#cycle.beginAcquisition()) {
158
+ this.#forceTerminalDisposition();
159
+ return;
160
+ }
161
+ this.#disposition = undefined;
162
+ this.#transition("connecting");
163
+ if (this.#lifetime.signal.aborted)
164
+ return;
165
+ let capability;
166
+ try {
167
+ capability = this.#capabilitySnapshot();
168
+ validateRuntimeCapabilityDescriptorV3(capability);
169
+ }
170
+ catch {
171
+ this.#recordFailure("artifact", "artifact_invalid", { kind: "terminal" });
172
+ return;
173
+ }
174
+ const acquisition = await this.#acquire();
175
+ if (this.#lifetime.signal.aborted) {
176
+ if (acquisition.kind === "lease")
177
+ await this.#claimAndRetire(acquisition.lease);
178
+ return;
179
+ }
180
+ if (acquisition.kind === "failure") {
181
+ const error = sourceFailure(acquisition);
182
+ const ordinal = this.#cycle.recordFailedAcquisitionOrLease();
183
+ this.#recordFailure("artifact", error.code, error.disposition);
184
+ if (error.disposition.kind === "terminal" || this.#attemptBudgetExhausted())
185
+ return;
186
+ pendingDisposition = error.disposition;
187
+ void ordinal;
188
+ continue;
189
+ }
190
+ let claim;
191
+ try {
192
+ claim = claimArtifactLeaseV3(acquisition.lease);
193
+ }
194
+ catch {
195
+ this.#cycle.recordFailedAcquisitionOrLease();
196
+ this.#recordFailure("artifact", "artifact_invalid", { kind: "terminal" });
197
+ return;
198
+ }
199
+ if (next === "replacement" && !this.#cycle.claimReplacementQuota()) {
200
+ await retireArtifactLeaseV3(claim);
201
+ this.#cycle.recordFailedAcquisitionOrLease();
202
+ this.#recordFailure("connect", replacementTerminal(replacementContext).code, { kind: "terminal" });
203
+ return;
204
+ }
205
+ const artifact = claimedArtifactV3(claim);
206
+ let allCandidates;
207
+ try {
208
+ validateArtifactV3(artifact);
209
+ allCandidates = canonicalizeCandidatesV3(artifact.path.kind, artifact.path.candidates).candidates;
210
+ }
211
+ catch {
212
+ await retireArtifactLeaseV3(claim);
213
+ this.#cycle.recordFailedAcquisitionOrLease();
214
+ const error = new ConnectErrorV3("artifact_invalid", { kind: "terminal" });
215
+ this.#recordFailure("artifact", error.code, error.disposition);
216
+ return;
217
+ }
218
+ try {
219
+ this.#assertArtifactFresh(artifact);
220
+ }
221
+ catch {
222
+ await retireArtifactLeaseV3(claim);
223
+ this.#cycle.recordFailedAcquisitionOrLease();
224
+ const error = new ConnectErrorV3("expired_artifact", { kind: "retryable" });
225
+ this.#recordFailure("artifact", error.code, error.disposition);
226
+ if (this.#attemptBudgetExhausted())
227
+ return;
228
+ next = "primary";
229
+ replacementContext = undefined;
230
+ pendingDisposition = error.disposition;
231
+ continue;
232
+ }
233
+ const candidates = next === "replacement"
234
+ ? selectReplacementCandidatesV3(artifact.path.kind, replacementContext?.candidates ?? [], allCandidates, replacementContext?.triggerKeys ?? new Set(), replacementContext?.failedKeys ?? new Set(), this.#cycle)
235
+ : filterBlockedCandidatesV3(artifact.path.kind, allCandidates, this.#cycle);
236
+ if (candidates.length === 0) {
237
+ await retireArtifactLeaseV3(claim);
238
+ this.#cycle.recordFailedAcquisitionOrLease();
239
+ const error = next === "replacement"
240
+ ? replacementTerminal(replacementContext)
241
+ : this.#cycle.blockedPolicyTerminal();
242
+ this.#recordFailure("connect", error.code, error.disposition);
243
+ return;
244
+ }
245
+ let rawResult;
246
+ try {
247
+ rawResult = await this.#connector({
248
+ kind: next,
249
+ artifact,
250
+ candidates,
251
+ claim,
252
+ signal: this.#lifetime.signal,
253
+ capability,
254
+ assertArtifactFresh: () => this.#assertArtifactFresh(artifact),
255
+ });
256
+ }
257
+ catch {
258
+ rawResult = {
259
+ kind: "post_spend_failure",
260
+ error: new ConnectErrorV3("connection_failed", { kind: "terminal" }),
261
+ };
262
+ }
263
+ let result = normalizeLeaseAttemptResultV3(rawResult, candidates, capability);
264
+ if (result === undefined) {
265
+ const malformedSession = sessionFromMalformedConnectorResultV3(rawResult);
266
+ if (artifactLeaseStateV3(claim) === "claimed")
267
+ await retireArtifactLeaseV3(claim);
268
+ if (malformedSession !== undefined)
269
+ await closeManagedSessionV3(malformedSession);
270
+ if (this.#lifetime.signal.aborted)
271
+ return;
272
+ this.#cycle.recordFailedAcquisitionOrLease();
273
+ this.#recordFailure("connect", "artifact_invalid", { kind: "terminal" });
274
+ return;
275
+ }
276
+ if (this.#lifetime.signal.aborted) {
277
+ if (result.kind === "established") {
278
+ if (artifactLeaseStateV3(claim) === "claimed")
279
+ await retireArtifactLeaseV3(claim);
280
+ await closeManagedSessionV3(result.session);
281
+ }
282
+ else if (artifactLeaseStateV3(claim) === "claimed") {
283
+ await retireArtifactLeaseV3(claim);
284
+ }
285
+ return;
286
+ }
287
+ if (result.kind === "established") {
288
+ if (artifactLeaseStateV3(claim) !== "consumed") {
289
+ if (artifactLeaseStateV3(claim) === "claimed")
290
+ await retireArtifactLeaseV3(claim);
291
+ this.#cycle.recordFailedAcquisitionOrLease();
292
+ this.#recordFailure("connect", "artifact_invalid", { kind: "terminal" });
293
+ await closeManagedSessionV3(result.session);
294
+ return;
295
+ }
296
+ this.#connectedAttempt = this.#cycle.snapshot().attempts;
297
+ this.#cycle.established();
298
+ this.#failure = undefined;
299
+ this.#disposition = undefined;
300
+ this.#session = result.session;
301
+ this.#transition("connected");
302
+ const termination = await this.#waitTermination(result.session);
303
+ if (termination === undefined || this.#lifetime.signal.aborted)
304
+ return;
305
+ this.#session = undefined;
306
+ this.#connectedAttempt = 0;
307
+ await closeManagedSessionV3(result.session);
308
+ this.#cycle = new ControllerCycleStateV3(this.#maximumAttempts);
309
+ let sessionFailure;
310
+ try {
311
+ sessionFailure = this.#projectSessionFailure(termination.error);
312
+ validateRetryDispositionV3(sessionFailure.disposition);
313
+ }
314
+ catch {
315
+ sessionFailure = new ConnectErrorV3("connection_failed", { kind: "terminal" });
316
+ }
317
+ const disposition = sessionFailure.disposition;
318
+ this.#cycle.recordFailedAcquisitionOrLease();
319
+ this.#recordFailure("session", sessionFailure.code, disposition);
320
+ if (disposition.kind === "terminal")
321
+ return;
322
+ next = "primary";
323
+ replacementContext = undefined;
324
+ pendingDisposition = disposition;
325
+ continue;
326
+ }
327
+ if (result.kind === "pre_spend_failure") {
328
+ if (artifactLeaseStateV3(claim) !== "claimed") {
329
+ this.#cycle.recordFailedAcquisitionOrLease();
330
+ this.#recordFailure("connect", "artifact_invalid", { kind: "terminal" });
331
+ return;
332
+ }
333
+ await retireArtifactLeaseV3(claim);
334
+ this.#cycle.recordFailedAcquisitionOrLease();
335
+ const error = next === "replacement" && result.error.code !== "expired_artifact"
336
+ ? replacementTerminal(replacementContext)
337
+ : result.error;
338
+ const disposition = error.disposition;
339
+ this.#recordFailure("connect", error.code, disposition);
340
+ if (disposition.kind === "terminal" || this.#attemptBudgetExhausted())
341
+ return;
342
+ next = "primary";
343
+ replacementContext = undefined;
344
+ pendingDisposition = result.error.disposition;
345
+ continue;
346
+ }
347
+ if (result.kind === "post_spend_failure") {
348
+ let postSpendResult = result;
349
+ if (artifactLeaseStateV3(claim) !== "consumed") {
350
+ if (artifactLeaseStateV3(claim) === "claimed")
351
+ await retireArtifactLeaseV3(claim);
352
+ postSpendResult = {
353
+ kind: "post_spend_failure",
354
+ error: new ConnectErrorV3("artifact_invalid", { kind: "terminal" }),
355
+ };
356
+ }
357
+ this.#cycle.recordFailedAcquisitionOrLease();
358
+ this.#recordFailure("connect", postSpendResult.error.code, postSpendResult.error.disposition);
359
+ if (postSpendResult.error.disposition.kind === "terminal" || this.#attemptBudgetExhausted())
360
+ return;
361
+ next = "primary";
362
+ replacementContext = undefined;
363
+ pendingDisposition = postSpendResult.error.disposition;
364
+ continue;
365
+ }
366
+ if (artifactLeaseStateV3(claim) !== "claimed") {
367
+ this.#cycle.recordFailedAcquisitionOrLease();
368
+ this.#recordFailure("connect", "artifact_invalid", { kind: "terminal" });
369
+ return;
370
+ }
371
+ await retireArtifactLeaseV3(claim);
372
+ this.#cycle.recordFailedAcquisitionOrLease();
373
+ if (isExpired(artifact, this.#nowUnixSeconds)) {
374
+ const error = new ConnectErrorV3("expired_artifact", { kind: "retryable" });
375
+ this.#recordFailure("connect", error.code, error.disposition);
376
+ if (this.#attemptBudgetExhausted())
377
+ return;
378
+ next = "primary";
379
+ replacementContext = undefined;
380
+ pendingDisposition = error.disposition;
381
+ continue;
382
+ }
383
+ if (next === "replacement") {
384
+ const error = replacementTerminal(replacementContext);
385
+ this.#recordFailure("connect", error.code, error.disposition);
386
+ return;
387
+ }
388
+ const triggerKeys = blockPolicyRefreshTriggersV3(artifact.path.kind, result.failures, this.#cycle);
389
+ const refresh = aggregateCandidateFailuresV3(result.failures, !this.#cycle.snapshot().replacementUsed);
390
+ if (refresh === "policy_refresh") {
391
+ const failedKeys = new Set(result.failures.map(({ candidate }) => endpointKeyV3(artifact.path.kind, candidate)));
392
+ replacementContext = { artifact, candidates: allCandidates, failures: result.failures, triggerKeys, failedKeys };
393
+ this.#rememberFailure("connect", replacementTerminal(replacementContext).code);
394
+ next = "replacement";
395
+ continue;
396
+ }
397
+ if (triggerKeys.size > 0 && refresh.code === "connection_failed") {
398
+ const terminal = this.#cycle.blockedPolicyTerminal();
399
+ this.#recordFailure("connect", terminal.code, terminal.disposition);
400
+ return;
401
+ }
402
+ this.#recordFailure("connect", refresh.code, refresh.disposition);
403
+ if (refresh.disposition.kind === "terminal" || this.#attemptBudgetExhausted())
404
+ return;
405
+ next = "primary";
406
+ pendingDisposition = refresh.disposition;
407
+ }
408
+ }
409
+ async #acquire() {
410
+ const acquisition = new SourceAcquisitionRaceV3(this.#source, this.#lifetime.signal);
411
+ let deliveredLeaseSnapshot;
412
+ try {
413
+ const result = await acquisition.value();
414
+ await acquisition.settle();
415
+ if (result === undefined)
416
+ return invalidSourceResult();
417
+ if (result === null || typeof result !== "object")
418
+ return invalidSourceResult();
419
+ const deliveredLease = Reflect.get(result, "lease");
420
+ if (deliveredLease !== null && typeof deliveredLease === "object") {
421
+ deliveredLeaseSnapshot = deliveredLease;
422
+ }
423
+ const kind = Reflect.get(result, "kind");
424
+ const ownKeys = Reflect.ownKeys(result);
425
+ if (kind === "lease") {
426
+ if (deliveredLeaseSnapshot === undefined)
427
+ return invalidSourceResult();
428
+ if (!hasExactOwnKeyList(ownKeys, ["kind", "lease"])) {
429
+ await this.#claimAndRetire(deliveredLeaseSnapshot);
430
+ return invalidSourceResult();
431
+ }
432
+ return { kind: "lease", lease: deliveredLeaseSnapshot };
433
+ }
434
+ if (kind === "failure") {
435
+ if (deliveredLeaseSnapshot !== undefined)
436
+ await this.#claimAndRetire(deliveredLeaseSnapshot);
437
+ const code = Reflect.get(result, "code");
438
+ const disposition = Reflect.get(result, "disposition");
439
+ if (!hasExactOwnKeyList(ownKeys, ["kind", "code", "disposition"]) ||
440
+ typeof code !== "string" || !ARTIFACT_SOURCE_FAILURE_CODES_V3.has(code)) {
441
+ return invalidSourceResult();
442
+ }
443
+ return { kind: "failure", code, disposition: validateRetryDispositionV3(disposition) };
444
+ }
445
+ if (deliveredLeaseSnapshot !== undefined)
446
+ await this.#claimAndRetire(deliveredLeaseSnapshot);
447
+ return invalidSourceResult();
448
+ }
449
+ catch (error) {
450
+ await acquisition.settle();
451
+ if (deliveredLeaseSnapshot !== undefined)
452
+ await this.#claimAndRetire(deliveredLeaseSnapshot);
453
+ if (error instanceof ConnectErrorV3) {
454
+ try {
455
+ if (!ARTIFACT_SOURCE_FAILURE_CODES_V3.has(error.code))
456
+ return invalidSourceResult();
457
+ return {
458
+ kind: "failure",
459
+ code: error.code,
460
+ disposition: validateRetryDispositionV3(error.disposition),
461
+ };
462
+ }
463
+ catch {
464
+ return invalidSourceResult();
465
+ }
466
+ }
467
+ return invalidSourceResult();
468
+ }
469
+ }
470
+ async #claimAndRetire(lease) {
471
+ try {
472
+ await retireArtifactLeaseV3(claimArtifactLeaseV3(lease));
473
+ }
474
+ catch {
475
+ // A duplicate or already-owned late lease is a source contract violation,
476
+ // but close still remains terminal and does not start new work.
477
+ }
478
+ }
479
+ #assertArtifactFresh(artifact) {
480
+ if (isExpired(artifact, this.#nowUnixSeconds)) {
481
+ throw new ConnectErrorV3("expired_artifact", { kind: "retryable" });
482
+ }
483
+ }
484
+ #attemptBudgetExhausted() {
485
+ if (this.#maximumAttempts === 0 || this.#cycle.snapshot().attempts < this.#maximumAttempts)
486
+ return false;
487
+ this.#forceTerminalDisposition();
488
+ return true;
489
+ }
490
+ #forceTerminalDisposition() {
491
+ this.#disposition = { kind: "terminal" };
492
+ this.#transition("failed");
493
+ }
494
+ #recordFailure(phase, code, disposition) {
495
+ this.#rememberFailure(phase, code);
496
+ this.#disposition = disposition;
497
+ if (disposition.kind === "terminal")
498
+ this.#transition("failed");
499
+ }
500
+ #rememberFailure(phase, code) {
501
+ this.#failure = Object.freeze({ phase, code });
502
+ }
503
+ async #waitTermination(session) {
504
+ try {
505
+ return await raceAbort(session.waitTermination(), this.#lifetime.signal);
506
+ }
507
+ catch (error) {
508
+ if (this.#lifetime.signal.aborted)
509
+ return undefined;
510
+ return { error: error instanceof Error ? error : new Error(String(error)) };
511
+ }
512
+ }
513
+ #snapshot() {
514
+ return Object.freeze({
515
+ state: this.#state,
516
+ attempt: this.#state === "connected" ? this.#connectedAttempt : this.#cycle.snapshot().attempts,
517
+ ...(this.currentSession === undefined ? {} : { currentSession: this.currentSession }),
518
+ ...(this.#failure === undefined ? {} : { failure: this.#failure }),
519
+ ...(this.#disposition === undefined ? {} : { retryDisposition: this.#disposition }),
520
+ });
521
+ }
522
+ #transition(state) {
523
+ if (this.#state === "closed" && state !== "closed")
524
+ return;
525
+ this.#state = state;
526
+ const snapshot = this.#snapshot();
527
+ for (const listener of this.#listeners) {
528
+ try {
529
+ listener(snapshot);
530
+ }
531
+ catch { /* listener isolation */ }
532
+ }
533
+ }
534
+ }
535
+ export function createConnectionControllerV3(source, connector, options) {
536
+ return new ConnectionControllerV3(source, connector, options);
537
+ }
538
+ /** Serializes source delivery and controller cancellation at one ownership gate. */
539
+ class SourceAcquisitionRaceV3 {
540
+ #state = "pending";
541
+ #cleanup = Promise.resolve();
542
+ #sourcePromise;
543
+ #settled;
544
+ #resolveSettled;
545
+ #rejectSettled;
546
+ #signal;
547
+ #abort;
548
+ constructor(source, signal) {
549
+ this.#signal = signal;
550
+ this.#settled = new Promise((resolve, reject) => {
551
+ this.#resolveSettled = resolve;
552
+ this.#rejectSettled = reject;
553
+ });
554
+ try {
555
+ // Invoke the source before returning so a synchronous close cannot abort
556
+ // before the source has installed its own cancellation observer.
557
+ this.#sourcePromise = Promise.resolve(source.acquire({ signal }));
558
+ }
559
+ catch (error) {
560
+ this.#sourcePromise = Promise.reject(error);
561
+ }
562
+ this.#sourcePromise.then((result) => this.#deliver(result), (error) => this.#deliverFailure(error));
563
+ this.#abort = () => this.#cancel();
564
+ signal.addEventListener("abort", this.#abort, { once: true });
565
+ if (signal.aborted)
566
+ this.#cancel();
567
+ }
568
+ async value() {
569
+ return await this.#settled;
570
+ }
571
+ async settle() {
572
+ await this.#sourcePromise.catch(() => undefined);
573
+ await this.#cleanup;
574
+ this.#signal.removeEventListener("abort", this.#abort);
575
+ }
576
+ #cancel() {
577
+ if (this.#state !== "pending")
578
+ return;
579
+ this.#state = "canceled";
580
+ this.#resolveSettled(undefined);
581
+ }
582
+ #deliver(result) {
583
+ if (this.#state === "pending") {
584
+ this.#state = "delivered";
585
+ this.#resolveSettled(result);
586
+ return;
587
+ }
588
+ if (this.#state === "canceled") {
589
+ this.#cleanup = this.#cleanup.then(() => this.#retireLateLease(result));
590
+ }
591
+ }
592
+ #deliverFailure(error) {
593
+ if (this.#state === "pending") {
594
+ this.#state = "delivered";
595
+ this.#rejectSettled(error);
596
+ }
597
+ }
598
+ async #retireLateLease(result) {
599
+ if (result === null || typeof result !== "object")
600
+ return;
601
+ try {
602
+ const lease = deliveredLease(result);
603
+ if (lease === undefined)
604
+ return;
605
+ await retireArtifactLeaseV3(claimArtifactLeaseV3(lease));
606
+ }
607
+ catch {
608
+ // Late source cleanup is intentionally redacted at the public boundary.
609
+ }
610
+ }
611
+ }
612
+ function invalidSourceResult() {
613
+ return Object.freeze({
614
+ kind: "failure",
615
+ code: "artifact_invalid",
616
+ disposition: Object.freeze({ kind: "terminal" }),
617
+ });
618
+ }
619
+ const LEASE_ATTEMPT_RESULT_KINDS_V3 = new Set([
620
+ "established",
621
+ "candidate_failures",
622
+ "pre_spend_failure",
623
+ "post_spend_failure",
624
+ ]);
625
+ const CONNECT_ERROR_CODES_V3 = new Set([
626
+ "artifact_invalid",
627
+ "expired_artifact",
628
+ "transport_security_unsupported",
629
+ "transport_security_failed",
630
+ "connection_failed",
631
+ ]);
632
+ const TRANSPORT_FAILURE_CODES_V3 = new Set([
633
+ "invalid_artifact",
634
+ "expired_artifact",
635
+ "tls_unsupported",
636
+ "tls_policy_expired",
637
+ "tls_failed",
638
+ "connection_failed",
639
+ ]);
640
+ const TRANSPORT_FAILURE_DETAILS_V3 = new Set([
641
+ "ca_untrusted",
642
+ "pin_mismatch",
643
+ "unknown",
644
+ "browser_pin_opaque",
645
+ ]);
646
+ /** Enforces the runtime adapter boundary before the controller consumes a result. */
647
+ function normalizeLeaseAttemptResultV3(value, candidates, capability) {
648
+ try {
649
+ if (value === null || typeof value !== "object")
650
+ return undefined;
651
+ const kind = Reflect.get(value, "kind");
652
+ if (typeof kind !== "string" || !LEASE_ATTEMPT_RESULT_KINDS_V3.has(kind))
653
+ return undefined;
654
+ if (kind === "established") {
655
+ if (!hasExactOwnKeys(value, ["kind", "session"]))
656
+ return undefined;
657
+ const session = Reflect.get(value, "session");
658
+ if (!isManagedSessionV3(session))
659
+ return undefined;
660
+ return Object.freeze({ kind: "established", session: session });
661
+ }
662
+ if (kind === "candidate_failures") {
663
+ if (!hasExactOwnKeys(value, ["kind", "failures"]))
664
+ return undefined;
665
+ const failures = Reflect.get(value, "failures");
666
+ if (!Array.isArray(failures) || failures.length !== candidates.length)
667
+ return undefined;
668
+ const normalized = failures.map((failure) => normalizeCandidateFailureV3(failure, candidates, capability));
669
+ if (normalized.some((failure) => failure === undefined))
670
+ return undefined;
671
+ const seen = new Set(normalized.map((failure) => failure.candidate));
672
+ if (seen.size !== candidates.length)
673
+ return undefined;
674
+ return Object.freeze({
675
+ kind,
676
+ failures: Object.freeze(normalized),
677
+ });
678
+ }
679
+ if (!hasExactOwnKeys(value, ["kind", "error"]))
680
+ return undefined;
681
+ const error = Reflect.get(value, "error");
682
+ if (!(error instanceof ConnectErrorV3) || !CONNECT_ERROR_CODES_V3.has(error.code))
683
+ return undefined;
684
+ const normalizedError = new ConnectErrorV3(error.code, validateRetryDispositionV3(error.disposition));
685
+ return Object.freeze({ kind, error: normalizedError });
686
+ }
687
+ catch {
688
+ return undefined;
689
+ }
690
+ }
691
+ function isManagedSessionV3(value) {
692
+ return value !== null && typeof value === "object" &&
693
+ typeof Reflect.get(value, "waitTermination") === "function" &&
694
+ typeof Reflect.get(value, "close") === "function";
695
+ }
696
+ function normalizeCandidateFailureV3(value, candidates, capability) {
697
+ if (value === null || typeof value !== "object")
698
+ return undefined;
699
+ const keys = Reflect.ownKeys(value);
700
+ if (!hasExactOwnKeys(value, keys.includes("disposition")
701
+ ? ["candidate", "failure", "disposition"]
702
+ : ["candidate", "failure"]))
703
+ return undefined;
704
+ const candidate = Reflect.get(value, "candidate");
705
+ const failure = Reflect.get(value, "failure");
706
+ if (!candidates.includes(candidate))
707
+ return undefined;
708
+ if (!(failure instanceof TransportFailureV3) || !TRANSPORT_FAILURE_CODES_V3.has(failure.code) ||
709
+ !isValidTransportFailurePairV3(failure.code, failure.detail, candidate, capability))
710
+ return undefined;
711
+ const disposition = Reflect.get(value, "disposition");
712
+ const normalizedDisposition = disposition === undefined ? undefined : validateRetryDispositionV3(disposition);
713
+ return Object.freeze({
714
+ candidate: candidate,
715
+ failure,
716
+ ...(normalizedDisposition === undefined ? {} : { disposition: normalizedDisposition }),
717
+ });
718
+ }
719
+ function isValidTransportFailurePairV3(code, detail, candidate, capability) {
720
+ if (detail !== undefined && !TRANSPORT_FAILURE_DETAILS_V3.has(detail))
721
+ return false;
722
+ if (code === "tls_failed")
723
+ return detail === "ca_untrusted" || detail === "pin_mismatch" || detail === "unknown";
724
+ if (code === "connection_failed") {
725
+ if (detail === undefined)
726
+ return true;
727
+ return detail === "browser_pin_opaque" && candidate.carrier === "webtransport" &&
728
+ candidate.tls.mode === "pin" && capability.language === "typescript" && capability.runtime === "browser" &&
729
+ capability.tuples.some((tuple) => tuple.carrier === "webtransport" &&
730
+ tuple.networkMode === "dial" && tuple.securityModes.includes("pin"));
731
+ }
732
+ return detail === undefined;
733
+ }
734
+ function sessionFromMalformedConnectorResultV3(value) {
735
+ try {
736
+ if (value === null || typeof value !== "object" || Reflect.get(value, "kind") !== "established")
737
+ return undefined;
738
+ const session = Reflect.get(value, "session");
739
+ return isManagedSessionV3(session) ? session : undefined;
740
+ }
741
+ catch {
742
+ return undefined;
743
+ }
744
+ }
745
+ async function closeManagedSessionV3(session) {
746
+ try {
747
+ await Promise.resolve(session.close());
748
+ }
749
+ catch {
750
+ // Adapter cleanup is best-effort after ownership has already become terminal.
751
+ }
752
+ }
753
+ function hasExactOwnKeys(value, expected) {
754
+ const keys = Reflect.ownKeys(value);
755
+ return hasExactOwnKeyList(keys, expected);
756
+ }
757
+ function hasExactOwnKeyList(keys, expected) {
758
+ return keys.length === expected.length && expected.every((key) => keys.includes(key));
759
+ }
760
+ function deliveredLease(value) {
761
+ const lease = Reflect.get(value, "lease");
762
+ return lease !== null && typeof lease === "object" ? lease : undefined;
763
+ }
764
+ function sourceFailure(result) {
765
+ const code = result.code === "expired_artifact" ? "expired_artifact" :
766
+ result.code === "connection_failed" ? "connection_failed" : "artifact_invalid";
767
+ return new ConnectErrorV3(code, validateRetryDispositionV3(result.disposition));
768
+ }
769
+ function replacementTerminal(context) {
770
+ if (context === undefined)
771
+ return new ConnectErrorV3("artifact_invalid", { kind: "terminal" });
772
+ const value = aggregateCandidateFailuresV3(context.failures, false);
773
+ return value === "policy_refresh"
774
+ ? new ConnectErrorV3("transport_security_failed", { kind: "terminal" })
775
+ : value;
776
+ }
777
+ function isExpired(artifact, nowUnixSeconds) {
778
+ let expiry;
779
+ try {
780
+ if (artifact === null || typeof artifact !== "object")
781
+ return false;
782
+ const session = Reflect.get(artifact, "session");
783
+ if (session === null || typeof session !== "object")
784
+ return false;
785
+ expiry = Reflect.get(session, "init_expire_at_unix_s");
786
+ }
787
+ catch {
788
+ return false;
789
+ }
790
+ if (!Number.isSafeInteger(expiry) || expiry < 0)
791
+ return false;
792
+ const now = nowUnixSeconds();
793
+ if (!Number.isSafeInteger(now) || now < 0)
794
+ return true;
795
+ return now >= expiry;
796
+ }
797
+ async function raceAbort(promise, signal) {
798
+ if (signal.aborted)
799
+ throw signal.reason;
800
+ return await new Promise((resolve, reject) => {
801
+ const abort = () => reject(signal.reason);
802
+ signal.addEventListener("abort", abort, { once: true });
803
+ void promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
804
+ });
805
+ }