@floegence/flowersec-core 2.5.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -18
- package/dist/browser/connectSessionV3.d.ts +13 -0
- package/dist/browser/connectSessionV3.js +75 -0
- package/dist/browser/index.d.ts +3 -2
- package/dist/browser/index.js +2 -1
- package/dist/browser/v2.d.ts +3 -0
- package/dist/browser/v2.js +2 -0
- package/dist/facade.d.ts +15 -8
- package/dist/facade.js +5 -4
- package/dist/interop/proxyServerPeer.js +1 -1
- package/dist/interop/serverParityPeer.js +8 -1
- package/dist/node/acceptor.d.ts +10 -0
- package/dist/node/acceptor.js +45 -2
- package/dist/node/acceptorV3.d.ts +57 -0
- package/dist/node/acceptorV3.js +227 -0
- package/dist/node/connectSessionV3.d.ts +21 -0
- package/dist/node/connectSessionV3.js +73 -0
- package/dist/node/index.d.ts +15 -10
- package/dist/node/index.js +6 -5
- package/dist/node/nativeTransportAddon.d.ts +39 -1
- package/dist/node/nativeTransportAddon.js +29 -5
- package/dist/node/rawQuicAdapterV3.d.ts +10 -0
- package/dist/node/rawQuicAdapterV3.js +76 -0
- package/dist/node/rawQuicServerV3.d.ts +24 -0
- package/dist/node/rawQuicServerV3.js +34 -0
- package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
- package/dist/node/runtimeAuthorizationV3.js +126 -0
- package/dist/node/tunnelRuntimeV3.d.ts +57 -0
- package/dist/node/tunnelRuntimeV3.js +857 -0
- package/dist/node/v2.d.ts +11 -0
- package/dist/node/v2.js +6 -0
- package/dist/node/webSocketServerV3.d.ts +27 -0
- package/dist/node/webSocketServerV3.js +219 -0
- package/dist/public/streamHandlers.d.ts +3 -0
- package/dist/public/streamHandlers.js +6 -1
- package/dist/v2/index.d.ts +14 -0
- package/dist/v2/index.js +7 -0
- package/dist/v2/protocol.js +23 -32
- package/dist/v2/session.d.ts +5 -0
- package/dist/v2/session.js +90 -35
- package/dist/v3/admissionError.d.ts +4 -0
- package/dist/v3/admissionError.js +8 -0
- package/dist/v3/artifact.d.ts +145 -0
- package/dist/v3/artifact.js +1162 -0
- package/dist/v3/artifactLease.d.ts +17 -0
- package/dist/v3/artifactLease.js +109 -0
- package/dist/v3/browserRuntime.d.ts +37 -0
- package/dist/v3/browserRuntime.js +197 -0
- package/dist/v3/browserSessionRuntime.d.ts +2 -0
- package/dist/v3/browserSessionRuntime.js +21 -0
- package/dist/v3/capability.d.ts +32 -0
- package/dist/v3/capability.js +345 -0
- package/dist/v3/carrier.d.ts +84 -0
- package/dist/v3/carrier.js +411 -0
- package/dist/v3/connectionController.d.ts +87 -0
- package/dist/v3/connectionController.js +805 -0
- package/dist/v3/contract.d.ts +65 -0
- package/dist/v3/contract.js +3 -0
- package/dist/v3/controller.d.ts +43 -0
- package/dist/v3/controller.js +303 -0
- package/dist/v3/handshake.d.ts +81 -0
- package/dist/v3/handshake.js +471 -0
- package/dist/v3/jcs.d.ts +6 -0
- package/dist/v3/jcs.js +69 -0
- package/dist/v3/jsonPreflight.d.ts +1 -0
- package/dist/v3/jsonPreflight.js +251 -0
- package/dist/v3/nodeRuntime.d.ts +40 -0
- package/dist/v3/nodeRuntime.js +315 -0
- package/dist/v3/nodeSessionRuntime.d.ts +2 -0
- package/dist/v3/nodeSessionRuntime.js +13 -0
- package/dist/v3/protocol.d.ts +99 -0
- package/dist/v3/protocol.js +773 -0
- package/dist/v3/publicApi.d.ts +12 -0
- package/dist/v3/publicApi.js +46 -0
- package/dist/v3/publicSession.d.ts +1 -0
- package/dist/v3/publicSession.js +281 -0
- package/dist/v3/retryDisposition.d.ts +16 -0
- package/dist/v3/retryDisposition.js +30 -0
- package/dist/v3/runtimeAdapters.d.ts +9 -0
- package/dist/v3/runtimeAdapters.js +109 -0
- package/dist/v3/security.d.ts +34 -0
- package/dist/v3/security.js +100 -0
- package/dist/v3/serverAdmission.d.ts +38 -0
- package/dist/v3/serverAdmission.js +223 -0
- package/dist/v3/session.d.ts +318 -0
- package/dist/v3/session.js +2289 -0
- package/dist/v3/sessionConnector.d.ts +37 -0
- package/dist/v3/sessionConnector.js +440 -0
- package/dist/v3/streamLifetimeLedger.d.ts +31 -0
- package/dist/v3/streamLifetimeLedger.js +131 -0
- package/dist/v3/streamMetadata.d.ts +1 -0
- package/dist/v3/streamMetadata.js +7 -0
- package/dist/v3/transportConstants.d.ts +56 -0
- package/dist/v3/transportConstants.js +59 -0
- package/dist/v3/unreliableMessage.d.ts +44 -0
- package/dist/v3/unreliableMessage.js +284 -0
- package/dist/v3/webSocketCarrier.d.ts +31 -0
- package/dist/v3/webSocketCarrier.js +370 -0
- package/dist/v3/webTransportCarrier.d.ts +27 -0
- package/dist/v3/webTransportCarrier.js +534 -0
- package/package.json +4 -4
- package/sbom/cyclonedx.json +6 -6
- package/sbom/spdx.json +11 -11
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Session } from "../public/contract.js";
|
|
2
|
+
import type { RpcRouter } from "../rpc/server.js";
|
|
3
|
+
import { type ArtifactV3, type CanonicalArtifactCandidateV3 } from "./artifact.js";
|
|
4
|
+
import { type ArtifactLeaseV3 } from "./artifactLease.js";
|
|
5
|
+
import type { CarrierSessionV3, NativeCarrierStreamV3 } from "./carrier.js";
|
|
6
|
+
import type { LeaseAttemptContextV3, LeaseAttemptResultV3 } from "./connectionController.js";
|
|
7
|
+
import { type SessionDeadlineFactoryV3, type SessionProtocolRuntimeV3 } from "./session.js";
|
|
8
|
+
import { type RuntimeCapabilityDescriptorV3 } from "./capability.js";
|
|
9
|
+
export type ClientAdmissionChannelV3 = Readonly<{
|
|
10
|
+
framing: "message";
|
|
11
|
+
write(data: Uint8Array, signal?: AbortSignal): Promise<void>;
|
|
12
|
+
read(signal?: AbortSignal): Promise<Uint8Array>;
|
|
13
|
+
abort(error: Error): void;
|
|
14
|
+
}> | Readonly<{
|
|
15
|
+
framing: "stream";
|
|
16
|
+
stream: NativeCarrierStreamV3;
|
|
17
|
+
abort(error: Error): void;
|
|
18
|
+
}>;
|
|
19
|
+
export type ReadyAdmissionTransportV3 = Readonly<{
|
|
20
|
+
candidate: CanonicalArtifactCandidateV3;
|
|
21
|
+
openAdmissionChannel(signal?: AbortSignal): Promise<ClientAdmissionChannelV3>;
|
|
22
|
+
finalize(): CarrierSessionV3;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
abort(): void;
|
|
25
|
+
}>;
|
|
26
|
+
export type CandidateDialerV3 = (candidate: CanonicalArtifactCandidateV3, artifact: ArtifactV3, attemptNowUnixSeconds: number, capability: RuntimeCapabilityDescriptorV3, signal: AbortSignal) => Promise<ReadyAdmissionTransportV3>;
|
|
27
|
+
export type SessionConnectorRuntimeV3 = Readonly<{
|
|
28
|
+
capabilitySnapshot(): RuntimeCapabilityDescriptorV3;
|
|
29
|
+
dial: CandidateDialerV3;
|
|
30
|
+
protocolRuntime: SessionProtocolRuntimeV3;
|
|
31
|
+
connectTimeoutMilliseconds?: number;
|
|
32
|
+
createRPCRouter?: () => RpcRouter;
|
|
33
|
+
nowUnixSeconds?: () => number;
|
|
34
|
+
deadlineFactory?: SessionDeadlineFactoryV3;
|
|
35
|
+
}>;
|
|
36
|
+
export declare function connectArtifactLeaseV3(lease: ArtifactLeaseV3, runtime: SessionConnectorRuntimeV3, signal?: AbortSignal): Promise<Session>;
|
|
37
|
+
export declare function attemptClaimedArtifactLeaseV3(context: LeaseAttemptContextV3, runtime: SessionConnectorRuntimeV3): Promise<LeaseAttemptResultV3<Session>>;
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { base64urlDecode } from "../utils/base64url.js";
|
|
2
|
+
import { SDK_DEFAULTS } from "../defaults.js";
|
|
3
|
+
import { AdmissionStatusV3, admissionBindingV3, buildFSB3RequestV3, canonicalizeCandidatesV3, decodeFSA3ResponseV3, encodeFSB3RequestV3, validateArtifactV3, } from "./artifact.js";
|
|
4
|
+
import { artifactLeaseStateV3, claimArtifactLeaseV3, claimedArtifactV3, commitArtifactLeaseSpendV3, retireArtifactLeaseV3, } from "./artifactLease.js";
|
|
5
|
+
import { aggregateCandidateFailuresV3, } from "./controller.js";
|
|
6
|
+
import { projectSessionV3 } from "./publicSession.js";
|
|
7
|
+
import { ConnectErrorV3, TransportFailureV3, snapshotTransportSecurityPolicyV3, } from "./security.js";
|
|
8
|
+
import { establishSessionV3, } from "./session.js";
|
|
9
|
+
import { supportsCandidateSecurityV3, validateRuntimeCapabilityDescriptorV3, } from "./capability.js";
|
|
10
|
+
export async function connectArtifactLeaseV3(lease, runtime, signal) {
|
|
11
|
+
let claim;
|
|
12
|
+
try {
|
|
13
|
+
claim = claimArtifactLeaseV3(lease);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new ConnectErrorV3("artifact_invalid", { kind: "terminal" });
|
|
17
|
+
}
|
|
18
|
+
const artifact = claimedArtifactV3(claim);
|
|
19
|
+
const now = runtime.nowUnixSeconds ?? (() => Math.floor(Date.now() / 1_000));
|
|
20
|
+
let capability;
|
|
21
|
+
let candidates;
|
|
22
|
+
try {
|
|
23
|
+
capability = runtime.capabilitySnapshot();
|
|
24
|
+
validateRuntimeCapabilityDescriptorV3(capability);
|
|
25
|
+
validateArtifactV3(artifact);
|
|
26
|
+
assertArtifactFresh(artifact, now);
|
|
27
|
+
candidates = canonicalizeCandidatesV3(artifact.path.kind, artifact.path.candidates).candidates;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
await retireArtifactLeaseV3(claim);
|
|
31
|
+
throw publicPreSpendError(error);
|
|
32
|
+
}
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const unlink = linkAbort(signal, controller);
|
|
35
|
+
try {
|
|
36
|
+
const result = await attemptClaimedArtifactLeaseV3({
|
|
37
|
+
kind: "primary",
|
|
38
|
+
artifact,
|
|
39
|
+
candidates,
|
|
40
|
+
claim,
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
capability,
|
|
43
|
+
assertArtifactFresh: () => assertArtifactFresh(artifact, now),
|
|
44
|
+
}, runtime);
|
|
45
|
+
if (result.kind === "established")
|
|
46
|
+
return result.session;
|
|
47
|
+
if (artifactLeaseStateV3(claim) === "claimed")
|
|
48
|
+
await retireArtifactLeaseV3(claim);
|
|
49
|
+
if (result.kind === "candidate_failures") {
|
|
50
|
+
const failure = aggregateCandidateFailuresV3(result.failures, false);
|
|
51
|
+
throw failure === "policy_refresh"
|
|
52
|
+
? new ConnectErrorV3("transport_security_failed", { kind: "terminal" })
|
|
53
|
+
: failure;
|
|
54
|
+
}
|
|
55
|
+
throw result.error;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
unlink();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function attemptClaimedArtifactLeaseV3(context, runtime) {
|
|
62
|
+
let timeoutMilliseconds;
|
|
63
|
+
try {
|
|
64
|
+
timeoutMilliseconds = normalizeConnectTimeoutMilliseconds(runtime.connectTimeoutMilliseconds);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return { kind: "pre_spend_failure", error: publicPreSpendError(error) };
|
|
68
|
+
}
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const unlink = linkConnectAttemptAbort(context.signal, controller);
|
|
71
|
+
const timer = setTimeout(() => controller.abort(new ConnectErrorV3("connection_failed", { kind: "retryable" })), timeoutMilliseconds);
|
|
72
|
+
try {
|
|
73
|
+
throwIfAborted(controller.signal);
|
|
74
|
+
return await attemptClaimedArtifactLeaseWithinDeadlineV3({ ...context, signal: controller.signal }, runtime);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const projected = error instanceof ConnectErrorV3
|
|
78
|
+
? error
|
|
79
|
+
: new ConnectErrorV3("connection_failed", { kind: "terminal" });
|
|
80
|
+
return artifactLeaseStateV3(context.claim) === "claimed"
|
|
81
|
+
? { kind: "pre_spend_failure", error: projected }
|
|
82
|
+
: { kind: "post_spend_failure", error: projected };
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
unlink();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function attemptClaimedArtifactLeaseWithinDeadlineV3(context, runtime) {
|
|
90
|
+
const role = context.artifact.path.kind === "tunnel" && context.artifact.path.role === 2
|
|
91
|
+
? "server"
|
|
92
|
+
: "client";
|
|
93
|
+
const failures = [];
|
|
94
|
+
const pending = new Map();
|
|
95
|
+
const controllers = new Map();
|
|
96
|
+
const preparedTransportCleanups = new Map();
|
|
97
|
+
const cleanupPreparedTransport = (transport) => {
|
|
98
|
+
const existing = preparedTransportCleanups.get(transport);
|
|
99
|
+
if (existing !== undefined)
|
|
100
|
+
return existing;
|
|
101
|
+
const cleanup = closePreparedTransportV3(transport, context.signal);
|
|
102
|
+
preparedTransportCleanups.set(transport, cleanup);
|
|
103
|
+
return cleanup;
|
|
104
|
+
};
|
|
105
|
+
let attemptNow;
|
|
106
|
+
try {
|
|
107
|
+
// A candidate race has one fixed wall-clock snapshot for every pin policy.
|
|
108
|
+
attemptNow = readAttemptNow(runtime.nowUnixSeconds);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
kind: "pre_spend_failure",
|
|
113
|
+
error: publicPreSpendError(error),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
for (const [index, candidate] of context.candidates.entries()) {
|
|
117
|
+
throwIfAborted(context.signal);
|
|
118
|
+
if (!supportsCandidateSecurityV3(context.capability, candidate.carrier, context.artifact.path.kind, role, candidate.tls.mode)) {
|
|
119
|
+
failures.push({ candidate, failure: new TransportFailureV3("tls_unsupported") });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const tuple = context.capability.tuples.find((entry) => entry.carrier === candidate.carrier && entry.path === context.artifact.path.kind &&
|
|
124
|
+
entry.networkMode === "dial" && entry.sessionRole === role);
|
|
125
|
+
snapshotTransportSecurityPolicyV3(candidate.tls, attemptNow, tuple?.securityModes ?? []);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
failures.push({ candidate, failure: asTransportFailure(error) });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
controllers.set(index, controller);
|
|
133
|
+
const unlink = linkAbort(context.signal, controller);
|
|
134
|
+
const task = Promise.resolve().then(async () => {
|
|
135
|
+
throwIfAborted(controller.signal);
|
|
136
|
+
return await runtime.dial(candidate, context.artifact, attemptNow, context.capability, controller.signal);
|
|
137
|
+
})
|
|
138
|
+
.then((ready) => ({ index, ready }))
|
|
139
|
+
.catch((error) => ({ index, failure: asTransportFailure(error) }))
|
|
140
|
+
.finally(unlink);
|
|
141
|
+
pending.set(index, task);
|
|
142
|
+
}
|
|
143
|
+
let winner;
|
|
144
|
+
try {
|
|
145
|
+
while (pending.size > 0 && winner === undefined) {
|
|
146
|
+
throwIfAborted(context.signal);
|
|
147
|
+
const result = await raceAbort(Promise.race(pending.values()), context.signal);
|
|
148
|
+
throwIfAborted(context.signal);
|
|
149
|
+
pending.delete(result.index);
|
|
150
|
+
if (result.ready !== undefined)
|
|
151
|
+
winner = result.ready;
|
|
152
|
+
else {
|
|
153
|
+
const candidate = context.candidates[result.index];
|
|
154
|
+
failures.push({ candidate, failure: result.failure ?? new TransportFailureV3("connection_failed") });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
for (const [index, controller] of controllers) {
|
|
160
|
+
if (context.candidates[index] !== winner?.candidate)
|
|
161
|
+
controller.abort(new Error("candidate lost race"));
|
|
162
|
+
}
|
|
163
|
+
for (const task of pending.values()) {
|
|
164
|
+
void task.then(async (result) => {
|
|
165
|
+
if (result.ready !== undefined && result.ready !== winner) {
|
|
166
|
+
await cleanupPreparedTransport(result.ready);
|
|
167
|
+
}
|
|
168
|
+
}).catch(() => undefined);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (winner === undefined) {
|
|
172
|
+
try {
|
|
173
|
+
context.assertArtifactFresh();
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return { kind: "pre_spend_failure", error: publicPreSpendError(error) };
|
|
177
|
+
}
|
|
178
|
+
return { kind: "candidate_failures", failures: Object.freeze(failures) };
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
context.assertArtifactFresh();
|
|
182
|
+
const rawFSB3 = encodeFSB3RequestV3(buildFSB3RequestV3(context.artifact, winner.candidate.id));
|
|
183
|
+
const config = sessionConfigFromArtifactV3(context.artifact, rawFSB3, runtime);
|
|
184
|
+
const channel = await raceAbort(winner.openAdmissionChannel(context.signal), context.signal);
|
|
185
|
+
try {
|
|
186
|
+
context.assertArtifactFresh();
|
|
187
|
+
await commitArtifactLeaseSpendV3(context.claim, context.signal);
|
|
188
|
+
context.assertArtifactFresh();
|
|
189
|
+
const response = channel.framing === "message"
|
|
190
|
+
? await exchangeMessage(channel, rawFSB3, context.signal)
|
|
191
|
+
: await exchangeStream(channel.stream, rawFSB3, context.signal);
|
|
192
|
+
const fsa3 = decodeFSA3ResponseV3(response);
|
|
193
|
+
if (fsa3.status !== AdmissionStatusV3.Success) {
|
|
194
|
+
throw new ConnectErrorV3("connection_failed", {
|
|
195
|
+
kind: fsa3.status === AdmissionStatusV3.Retryable ? "retryable" : "terminal",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const carrier = winner.finalize();
|
|
199
|
+
try {
|
|
200
|
+
const session = await raceAbort(establishSessionV3(carrier, config, { signal: context.signal }), context.signal);
|
|
201
|
+
return { kind: "established", session: projectSessionV3(session) };
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
carrier.abort({ code: 6, reason: "session establishment failed" });
|
|
205
|
+
throw error instanceof ConnectErrorV3
|
|
206
|
+
? error
|
|
207
|
+
: new ConnectErrorV3("connection_failed", { kind: "retryable" });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
channel.abort(asError(error));
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
await cleanupPreparedTransport(winner);
|
|
217
|
+
throwIfAborted(context.signal);
|
|
218
|
+
if (artifactLeaseStateV3(context.claim) === "claimed") {
|
|
219
|
+
return { kind: "pre_spend_failure", error: publicPreSpendError(error) };
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
kind: "post_spend_failure",
|
|
223
|
+
error: error instanceof ConnectErrorV3
|
|
224
|
+
? error
|
|
225
|
+
: new ConnectErrorV3("connection_failed", { kind: "terminal" }),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function sessionConfigFromArtifactV3(artifact, rawFSB3, runtime) {
|
|
230
|
+
const localBinding = admissionBindingV3(rawFSB3);
|
|
231
|
+
const tunnel = artifact.path.kind === "tunnel" ? artifact.path : undefined;
|
|
232
|
+
return {
|
|
233
|
+
role: tunnel?.role === 2 ? "server" : "client",
|
|
234
|
+
path: artifact.path.kind,
|
|
235
|
+
channelID: artifact.session.channel_id,
|
|
236
|
+
sessionContractHash: base64urlDecode(artifact.session.contract_hash_b64u),
|
|
237
|
+
suite: artifact.session.default_suite,
|
|
238
|
+
psk: base64urlDecode(artifact.session.e2ee_psk_b64u),
|
|
239
|
+
maxInboundStreams: artifact.session.max_inbound_streams,
|
|
240
|
+
sessionContract: artifact.session,
|
|
241
|
+
...(runtime.createRPCRouter === undefined ? {} : { rpcRouter: runtime.createRPCRouter() }),
|
|
242
|
+
idleTimeoutMs: artifact.session.idle_timeout_seconds * 1_000,
|
|
243
|
+
closeTimeoutMs: 5_000,
|
|
244
|
+
runtime: runtime.protocolRuntime,
|
|
245
|
+
localAdmissionBinding: localBinding,
|
|
246
|
+
peerAdmissionBinding: tunnel === undefined ? localBinding : new Uint8Array(32),
|
|
247
|
+
localEndpointInstanceID: tunnel?.local_endpoint_instance_id ?? "",
|
|
248
|
+
expectedPeerEndpointInstanceID: tunnel?.expected_peer_endpoint_instance_id ?? "",
|
|
249
|
+
deadlines: {
|
|
250
|
+
establishTimeoutMs: artifact.session.establish_timeout_seconds * 1_000,
|
|
251
|
+
rekeyPrepareTimeoutMs: artifact.session.rekey_prepare_timeout_seconds * 1_000,
|
|
252
|
+
rekeyCompletionTimeoutMs: artifact.session.rekey_completion_timeout_seconds * 1_000,
|
|
253
|
+
...(runtime.deadlineFactory === undefined ? {} : { factory: runtime.deadlineFactory }),
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function exchangeMessage(channel, rawFSB3, signal) {
|
|
258
|
+
await raceAbort(channel.write(rawFSB3, signal), signal);
|
|
259
|
+
return await raceAbort(channel.read(signal), signal);
|
|
260
|
+
}
|
|
261
|
+
async function exchangeStream(stream, rawFSB3, signal) {
|
|
262
|
+
await writeAll(stream, rawFSB3, signal);
|
|
263
|
+
await raceAbort(stream.closeWrite(), signal);
|
|
264
|
+
const reader = new ExactReader(stream);
|
|
265
|
+
const header = await reader.readExactly(8, signal);
|
|
266
|
+
const reasonLength = new DataView(header.buffer, header.byteOffset, header.byteLength).getUint16(6, false);
|
|
267
|
+
if (reasonLength > 64)
|
|
268
|
+
throw new ConnectErrorV3("connection_failed", { kind: "terminal" });
|
|
269
|
+
const reason = await reader.readExactly(reasonLength, signal);
|
|
270
|
+
await reader.expectEOF(signal);
|
|
271
|
+
return concat(header, reason);
|
|
272
|
+
}
|
|
273
|
+
class ExactReader {
|
|
274
|
+
stream;
|
|
275
|
+
#chunks = [];
|
|
276
|
+
#offset = 0;
|
|
277
|
+
#bytes = 0;
|
|
278
|
+
constructor(stream) {
|
|
279
|
+
this.stream = stream;
|
|
280
|
+
}
|
|
281
|
+
async readExactly(length, signal) {
|
|
282
|
+
while (this.#bytes < length) {
|
|
283
|
+
const chunk = await raceAbort(this.stream.read(), signal);
|
|
284
|
+
if (chunk === null)
|
|
285
|
+
throw new ConnectErrorV3("connection_failed", { kind: "terminal" });
|
|
286
|
+
if (chunk.length > 0) {
|
|
287
|
+
this.#chunks.push(chunk);
|
|
288
|
+
this.#bytes += chunk.length;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const output = new Uint8Array(length);
|
|
292
|
+
let written = 0;
|
|
293
|
+
while (written < length) {
|
|
294
|
+
const chunk = this.#chunks[0];
|
|
295
|
+
const take = Math.min(length - written, chunk.length - this.#offset);
|
|
296
|
+
output.set(chunk.subarray(this.#offset, this.#offset + take), written);
|
|
297
|
+
written += take;
|
|
298
|
+
this.#offset += take;
|
|
299
|
+
this.#bytes -= take;
|
|
300
|
+
if (this.#offset === chunk.length) {
|
|
301
|
+
this.#chunks.shift();
|
|
302
|
+
this.#offset = 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return output;
|
|
306
|
+
}
|
|
307
|
+
async expectEOF(signal) {
|
|
308
|
+
if (this.#bytes !== 0)
|
|
309
|
+
throw new ConnectErrorV3("connection_failed", { kind: "terminal" });
|
|
310
|
+
while (true) {
|
|
311
|
+
const chunk = await raceAbort(this.stream.read(), signal);
|
|
312
|
+
if (chunk === null)
|
|
313
|
+
return;
|
|
314
|
+
if (chunk.length !== 0)
|
|
315
|
+
throw new ConnectErrorV3("connection_failed", { kind: "terminal" });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function writeAll(stream, data, signal) {
|
|
320
|
+
let offset = 0;
|
|
321
|
+
while (offset < data.length) {
|
|
322
|
+
const written = await raceAbort(stream.write(data.subarray(offset)), signal);
|
|
323
|
+
if (written < 1 || written > data.length - offset) {
|
|
324
|
+
throw new ConnectErrorV3("connection_failed", { kind: "terminal" });
|
|
325
|
+
}
|
|
326
|
+
offset += written;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function readAttemptNow(now) {
|
|
330
|
+
const value = (now ?? (() => Math.floor(Date.now() / 1_000)))();
|
|
331
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
332
|
+
throw new TransportFailureV3("invalid_artifact");
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
function assertArtifactFresh(artifact, now) {
|
|
336
|
+
const value = now();
|
|
337
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
338
|
+
throw new ConnectErrorV3("artifact_invalid", { kind: "terminal" });
|
|
339
|
+
if (value >= artifact.session.init_expire_at_unix_s) {
|
|
340
|
+
throw new ConnectErrorV3("expired_artifact", { kind: "retryable" });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function publicPreSpendError(error) {
|
|
344
|
+
if (error instanceof ConnectErrorV3)
|
|
345
|
+
return error;
|
|
346
|
+
return new ConnectErrorV3("artifact_invalid", { kind: "terminal" });
|
|
347
|
+
}
|
|
348
|
+
function normalizeConnectTimeoutMilliseconds(value) {
|
|
349
|
+
const timeout = value ?? SDK_DEFAULTS.transport.connectTimeoutMs;
|
|
350
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1) {
|
|
351
|
+
throw new ConnectErrorV3("artifact_invalid", { kind: "terminal" });
|
|
352
|
+
}
|
|
353
|
+
return timeout;
|
|
354
|
+
}
|
|
355
|
+
function asTransportFailure(error) {
|
|
356
|
+
return error instanceof TransportFailureV3
|
|
357
|
+
? error
|
|
358
|
+
: new TransportFailureV3("connection_failed", undefined, error);
|
|
359
|
+
}
|
|
360
|
+
function asError(error) {
|
|
361
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
362
|
+
}
|
|
363
|
+
function linkAbort(parent, child) {
|
|
364
|
+
if (parent === undefined)
|
|
365
|
+
return () => undefined;
|
|
366
|
+
const abort = () => child.abort(parent.reason);
|
|
367
|
+
if (parent.aborted)
|
|
368
|
+
abort();
|
|
369
|
+
else
|
|
370
|
+
parent.addEventListener("abort", abort, { once: true });
|
|
371
|
+
return () => parent.removeEventListener("abort", abort);
|
|
372
|
+
}
|
|
373
|
+
function linkConnectAttemptAbort(parent, child) {
|
|
374
|
+
const abort = () => child.abort(new ConnectErrorV3("connection_failed", { kind: "terminal" }));
|
|
375
|
+
if (parent.aborted)
|
|
376
|
+
abort();
|
|
377
|
+
else
|
|
378
|
+
parent.addEventListener("abort", abort, { once: true });
|
|
379
|
+
return () => parent.removeEventListener("abort", abort);
|
|
380
|
+
}
|
|
381
|
+
function throwIfAborted(signal) {
|
|
382
|
+
if (signal.aborted)
|
|
383
|
+
throw signal.reason;
|
|
384
|
+
}
|
|
385
|
+
async function closePreparedTransportV3(transport, signal) {
|
|
386
|
+
const closing = Promise.resolve()
|
|
387
|
+
.then(async () => await transport.close())
|
|
388
|
+
.catch(() => undefined);
|
|
389
|
+
if (signal.aborted) {
|
|
390
|
+
abortPreparedTransportV3(transport);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
await raceAbort(closing, signal);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
abortPreparedTransportV3(transport);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function abortPreparedTransportV3(transport) {
|
|
401
|
+
try {
|
|
402
|
+
transport.abort();
|
|
403
|
+
}
|
|
404
|
+
catch { /* best effort */ }
|
|
405
|
+
}
|
|
406
|
+
async function raceAbort(promise, signal) {
|
|
407
|
+
if (signal.aborted)
|
|
408
|
+
throw signal.reason;
|
|
409
|
+
return await new Promise((resolve, reject) => {
|
|
410
|
+
let settled = false;
|
|
411
|
+
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
412
|
+
const abort = () => {
|
|
413
|
+
if (settled)
|
|
414
|
+
return;
|
|
415
|
+
settled = true;
|
|
416
|
+
cleanup();
|
|
417
|
+
reject(signal.reason);
|
|
418
|
+
};
|
|
419
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
420
|
+
void promise.then((value) => {
|
|
421
|
+
if (settled)
|
|
422
|
+
return;
|
|
423
|
+
settled = true;
|
|
424
|
+
cleanup();
|
|
425
|
+
resolve(value);
|
|
426
|
+
}, (error) => {
|
|
427
|
+
if (settled)
|
|
428
|
+
return;
|
|
429
|
+
settled = true;
|
|
430
|
+
cleanup();
|
|
431
|
+
reject(error);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function concat(left, right) {
|
|
436
|
+
const output = new Uint8Array(left.length + right.length);
|
|
437
|
+
output.set(left);
|
|
438
|
+
output.set(right, left.length);
|
|
439
|
+
return output;
|
|
440
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const MAX_STREAM_LIFETIME_SLOTS_V3 = 1048576;
|
|
2
|
+
export declare function maxLogicalStreamIDV3(openerRole: 1 | 2): bigint;
|
|
3
|
+
export declare enum StreamLifetimeStateV3 {
|
|
4
|
+
Empty = 0,
|
|
5
|
+
AbandonedNoFSS3 = 1,
|
|
6
|
+
PrefaceSeen = 2,
|
|
7
|
+
ActiveOrResolved = 3
|
|
8
|
+
}
|
|
9
|
+
export type LateFSS3ActionV3 = "accepted" | "reset";
|
|
10
|
+
export declare class StreamLifetimeLedgerV3Error extends Error {
|
|
11
|
+
readonly code: "capacity" | "duplicate" | "invalid_state";
|
|
12
|
+
constructor(code: "capacity" | "duplicate" | "invalid_state", message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class StreamLifetimeLedgerV3 {
|
|
15
|
+
private readonly openerRole;
|
|
16
|
+
private readonly states;
|
|
17
|
+
private resolvedFrontier;
|
|
18
|
+
constructor(openerRole: 1 | 2);
|
|
19
|
+
get frontier(): bigint;
|
|
20
|
+
get backingBytes(): number;
|
|
21
|
+
state(id: bigint): StreamLifetimeStateV3;
|
|
22
|
+
validFSS3(id: bigint): LateFSS3ActionV3;
|
|
23
|
+
validOpen(id: bigint): void;
|
|
24
|
+
peerReset(id: bigint): void;
|
|
25
|
+
localResetCommitted(id: bigint): void;
|
|
26
|
+
private requireSlot;
|
|
27
|
+
private slotIndex;
|
|
28
|
+
private stateAt;
|
|
29
|
+
private setStateAt;
|
|
30
|
+
private advanceFrontier;
|
|
31
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export const MAX_STREAM_LIFETIME_SLOTS_V3 = 1_048_576;
|
|
2
|
+
export function maxLogicalStreamIDV3(openerRole) {
|
|
3
|
+
return BigInt(MAX_STREAM_LIFETIME_SLOTS_V3) * 2n - (openerRole === 1 ? 1n : 0n);
|
|
4
|
+
}
|
|
5
|
+
export var StreamLifetimeStateV3;
|
|
6
|
+
(function (StreamLifetimeStateV3) {
|
|
7
|
+
StreamLifetimeStateV3[StreamLifetimeStateV3["Empty"] = 0] = "Empty";
|
|
8
|
+
StreamLifetimeStateV3[StreamLifetimeStateV3["AbandonedNoFSS3"] = 1] = "AbandonedNoFSS3";
|
|
9
|
+
StreamLifetimeStateV3[StreamLifetimeStateV3["PrefaceSeen"] = 2] = "PrefaceSeen";
|
|
10
|
+
StreamLifetimeStateV3[StreamLifetimeStateV3["ActiveOrResolved"] = 3] = "ActiveOrResolved";
|
|
11
|
+
})(StreamLifetimeStateV3 || (StreamLifetimeStateV3 = {}));
|
|
12
|
+
export class StreamLifetimeLedgerV3Error extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = "StreamLifetimeLedgerV3Error";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class StreamLifetimeLedgerV3 {
|
|
21
|
+
openerRole;
|
|
22
|
+
states = new Uint8Array(MAX_STREAM_LIFETIME_SLOTS_V3 / 4);
|
|
23
|
+
resolvedFrontier = 0n;
|
|
24
|
+
constructor(openerRole) {
|
|
25
|
+
this.openerRole = openerRole;
|
|
26
|
+
}
|
|
27
|
+
get frontier() {
|
|
28
|
+
return this.resolvedFrontier;
|
|
29
|
+
}
|
|
30
|
+
get backingBytes() {
|
|
31
|
+
return this.states.byteLength;
|
|
32
|
+
}
|
|
33
|
+
state(id) {
|
|
34
|
+
const index = this.slotIndex(id, false);
|
|
35
|
+
return index === undefined ? StreamLifetimeStateV3.Empty : this.stateAt(index);
|
|
36
|
+
}
|
|
37
|
+
validFSS3(id) {
|
|
38
|
+
const index = this.requireSlot(id);
|
|
39
|
+
switch (this.stateAt(index)) {
|
|
40
|
+
case StreamLifetimeStateV3.Empty:
|
|
41
|
+
this.setStateAt(index, StreamLifetimeStateV3.PrefaceSeen);
|
|
42
|
+
return "accepted";
|
|
43
|
+
case StreamLifetimeStateV3.AbandonedNoFSS3:
|
|
44
|
+
this.setStateAt(index, StreamLifetimeStateV3.ActiveOrResolved);
|
|
45
|
+
this.advanceFrontier();
|
|
46
|
+
return "reset";
|
|
47
|
+
case StreamLifetimeStateV3.PrefaceSeen:
|
|
48
|
+
case StreamLifetimeStateV3.ActiveOrResolved:
|
|
49
|
+
throw new StreamLifetimeLedgerV3Error("duplicate", "duplicate logical stream identity");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
validOpen(id) {
|
|
53
|
+
const index = this.requireSlot(id);
|
|
54
|
+
if (this.stateAt(index) !== StreamLifetimeStateV3.PrefaceSeen) {
|
|
55
|
+
throw new StreamLifetimeLedgerV3Error("invalid_state", "OPEN without a pending FSS3 identity");
|
|
56
|
+
}
|
|
57
|
+
this.setStateAt(index, StreamLifetimeStateV3.ActiveOrResolved);
|
|
58
|
+
this.advanceFrontier();
|
|
59
|
+
}
|
|
60
|
+
peerReset(id) {
|
|
61
|
+
const index = this.requireSlot(id);
|
|
62
|
+
switch (this.stateAt(index)) {
|
|
63
|
+
case StreamLifetimeStateV3.Empty:
|
|
64
|
+
this.setStateAt(index, StreamLifetimeStateV3.AbandonedNoFSS3);
|
|
65
|
+
break;
|
|
66
|
+
case StreamLifetimeStateV3.PrefaceSeen:
|
|
67
|
+
this.setStateAt(index, StreamLifetimeStateV3.ActiveOrResolved);
|
|
68
|
+
break;
|
|
69
|
+
case StreamLifetimeStateV3.AbandonedNoFSS3:
|
|
70
|
+
case StreamLifetimeStateV3.ActiveOrResolved:
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
this.advanceFrontier();
|
|
74
|
+
}
|
|
75
|
+
localResetCommitted(id) {
|
|
76
|
+
const index = this.requireSlot(id);
|
|
77
|
+
const state = this.stateAt(index);
|
|
78
|
+
if (state === StreamLifetimeStateV3.ActiveOrResolved)
|
|
79
|
+
return;
|
|
80
|
+
if (state !== StreamLifetimeStateV3.PrefaceSeen) {
|
|
81
|
+
throw new StreamLifetimeLedgerV3Error("invalid_state", "ordered reset without a pending FSS3 identity");
|
|
82
|
+
}
|
|
83
|
+
this.setStateAt(index, StreamLifetimeStateV3.ActiveOrResolved);
|
|
84
|
+
this.advanceFrontier();
|
|
85
|
+
}
|
|
86
|
+
requireSlot(id) {
|
|
87
|
+
const index = this.slotIndex(id, true);
|
|
88
|
+
if (index === undefined) {
|
|
89
|
+
throw new StreamLifetimeLedgerV3Error("capacity", "logical stream lifetime ledger capacity exceeded");
|
|
90
|
+
}
|
|
91
|
+
return index;
|
|
92
|
+
}
|
|
93
|
+
slotIndex(id, validate) {
|
|
94
|
+
const validParity = id > 0n && (this.openerRole === 1 ? (id & 1n) === 1n : (id & 1n) === 0n);
|
|
95
|
+
if (!validParity) {
|
|
96
|
+
if (validate)
|
|
97
|
+
throw new StreamLifetimeLedgerV3Error("capacity", "logical stream identity is outside the opener role");
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const ordinal = this.openerRole === 1 ? (id + 1n) / 2n : id / 2n;
|
|
101
|
+
if (ordinal < 1n || ordinal > BigInt(MAX_STREAM_LIFETIME_SLOTS_V3))
|
|
102
|
+
return undefined;
|
|
103
|
+
return Number(ordinal - 1n);
|
|
104
|
+
}
|
|
105
|
+
stateAt(index) {
|
|
106
|
+
const shift = (index % 4) * 2;
|
|
107
|
+
return ((this.states[Math.floor(index / 4)] >>> shift) & 0x03);
|
|
108
|
+
}
|
|
109
|
+
setStateAt(index, state) {
|
|
110
|
+
const byteIndex = Math.floor(index / 4);
|
|
111
|
+
const shift = (index % 4) * 2;
|
|
112
|
+
const mask = 0x03 << shift;
|
|
113
|
+
this.states[byteIndex] = (this.states[byteIndex] & ~mask) | (state << shift);
|
|
114
|
+
}
|
|
115
|
+
advanceFrontier() {
|
|
116
|
+
let next = this.resolvedFrontier === 0n
|
|
117
|
+
? (this.openerRole === 1 ? 1n : 2n)
|
|
118
|
+
: this.resolvedFrontier + 2n;
|
|
119
|
+
while (true) {
|
|
120
|
+
const index = this.slotIndex(next, false);
|
|
121
|
+
if (index === undefined)
|
|
122
|
+
return;
|
|
123
|
+
const state = this.stateAt(index);
|
|
124
|
+
if (state !== StreamLifetimeStateV3.AbandonedNoFSS3 && state !== StreamLifetimeStateV3.ActiveOrResolved) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.resolvedFrontier = next;
|
|
128
|
+
next += 2n;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createStreamMetadata, streamMetadataValues, } from "../public/streamMetadata.js";
|
|
2
|
+
/** @internal */
|
|
3
|
+
export { createStreamMetadata, StreamMetadataError } from "../public/streamMetadata.js";
|
|
4
|
+
/** @internal */
|
|
5
|
+
export const createStreamMetadataV3 = createStreamMetadata;
|
|
6
|
+
/** @internal */
|
|
7
|
+
export const streamMetadataValuesV3 = streamMetadataValues;
|