@floegence/flowersec-core 0.22.1 → 0.24.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 +17 -65
- package/YAMUX_ALIGNMENT.md +4 -4
- package/dist/client-connect/connectCore.d.ts +3 -3
- package/dist/client-connect/connectCore.js +120 -106
- package/dist/client.d.ts +2 -0
- package/dist/controlplane/issuer.d.ts +5 -2
- package/dist/controlplane/issuer.js +76 -16
- package/dist/controlplane/request.js +3 -1
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +2 -0
- package/dist/e2ee/handshake.d.ts +2 -2
- package/dist/e2ee/secureChannel.d.ts +2 -2
- package/dist/e2ee/secureChannel.js +22 -10
- package/dist/endpoint/index.d.ts +1 -0
- package/dist/endpoint/index.js +26 -5
- package/dist/proxy/controllerWindow.js +4 -5
- package/dist/proxy/runtime.js +10 -19
- package/dist/reconnect/index.js +141 -88
- package/dist/rpc/server.d.ts +1 -0
- package/dist/rpc/server.js +49 -20
- package/dist/streamio/index.js +3 -6
- package/dist/utils/errors.d.ts +1 -1
- package/dist/yamux/byteReader.d.ts +2 -0
- package/dist/yamux/byteReader.js +28 -24
- package/dist/yamux/errors.d.ts +8 -0
- package/dist/yamux/errors.js +18 -0
- package/dist/yamux/session.js +8 -5
- package/dist/yamux/stream.d.ts +1 -1
- package/dist/yamux/stream.js +3 -3
- package/package.json +4 -1
|
@@ -2,13 +2,13 @@ import { ed25519 } from "@noble/curves/ed25519";
|
|
|
2
2
|
import { base64urlEncode } from "../utils/base64url.js";
|
|
3
3
|
import { signToken } from "./token.js";
|
|
4
4
|
export class IssuerKeyset {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
active;
|
|
6
|
+
verificationKeys = new Map();
|
|
7
7
|
constructor(kid, signingSeed) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this.
|
|
8
|
+
const normalizedKID = normalizeKid(kid);
|
|
9
|
+
const retainedSeed = copySigningSeed(signingSeed);
|
|
10
|
+
this.active = { kid: normalizedKID, signingSeed: retainedSeed };
|
|
11
|
+
this.verificationKeys.set(normalizedKID, ed25519.getPublicKey(retainedSeed));
|
|
12
12
|
}
|
|
13
13
|
static random(kid) {
|
|
14
14
|
const seed = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -20,30 +20,90 @@ export class IssuerKeyset {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
currentKID() {
|
|
23
|
-
return this.kid;
|
|
23
|
+
return this.requireActive().kid;
|
|
24
24
|
}
|
|
25
25
|
publicKeys() {
|
|
26
|
-
|
|
26
|
+
this.requireActive();
|
|
27
|
+
return new Map([...this.verificationKeys.entries()]
|
|
28
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
29
|
+
.map(([kid, publicKey]) => [kid, publicKey.slice()]));
|
|
27
30
|
}
|
|
28
31
|
sign(payload) {
|
|
29
|
-
|
|
32
|
+
const active = this.requireActive();
|
|
33
|
+
return signToken(active.signingSeed, { ...payload, kid: active.kid });
|
|
34
|
+
}
|
|
35
|
+
addVerificationKey(kid, publicKey) {
|
|
36
|
+
this.requireActive();
|
|
37
|
+
const normalizedKID = normalizeKid(kid);
|
|
38
|
+
const retainedKey = copyPublicKey(publicKey);
|
|
39
|
+
const existing = this.verificationKeys.get(normalizedKID);
|
|
40
|
+
if (existing != null) {
|
|
41
|
+
if (!equalBytes(existing, retainedKey))
|
|
42
|
+
throw new Error(`key ID ${normalizedKID} is already bound to a different public key`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.verificationKeys.set(normalizedKID, retainedKey);
|
|
30
46
|
}
|
|
31
47
|
rotate(kid, signingSeed) {
|
|
48
|
+
const active = this.requireActive();
|
|
32
49
|
const nextKID = normalizeKid(kid);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
const nextSeed = copySigningSeed(signingSeed);
|
|
51
|
+
const nextPublicKey = ed25519.getPublicKey(nextSeed);
|
|
52
|
+
const prepublishedKey = this.verificationKeys.get(nextKID);
|
|
53
|
+
if (prepublishedKey == null) {
|
|
54
|
+
nextSeed.fill(0);
|
|
55
|
+
throw new Error(`key ID ${nextKID} must be prepublished before rotation`);
|
|
56
|
+
}
|
|
57
|
+
if (!equalBytes(prepublishedKey, nextPublicKey)) {
|
|
58
|
+
nextSeed.fill(0);
|
|
59
|
+
throw new Error(`key ID ${nextKID} is bound to a different public key`);
|
|
60
|
+
}
|
|
61
|
+
this.active = { kid: nextKID, signingSeed: nextSeed };
|
|
62
|
+
active.signingSeed.fill(0);
|
|
63
|
+
}
|
|
64
|
+
retireVerificationKey(kid) {
|
|
65
|
+
const active = this.requireActive();
|
|
66
|
+
const normalizedKID = normalizeKid(kid);
|
|
67
|
+
if (normalizedKID === active.kid)
|
|
68
|
+
throw new Error("the active signing key cannot be retired");
|
|
69
|
+
if (!this.verificationKeys.delete(normalizedKID))
|
|
70
|
+
throw new Error(`key ID ${normalizedKID} is not published`);
|
|
39
71
|
}
|
|
40
72
|
exportTunnelKeyset() {
|
|
41
73
|
const keys = [...this.publicKeys()].map(([kid, publicKey]) => ({ kid, pubkey_b64u: base64urlEncode(publicKey) }));
|
|
42
74
|
return new TextEncoder().encode(JSON.stringify({ keys }, null, 2));
|
|
43
75
|
}
|
|
44
76
|
dispose() {
|
|
45
|
-
this.
|
|
77
|
+
const active = this.active;
|
|
78
|
+
if (active == null)
|
|
79
|
+
return;
|
|
80
|
+
active.signingSeed.fill(0);
|
|
81
|
+
this.active = undefined;
|
|
82
|
+
this.verificationKeys.clear();
|
|
46
83
|
}
|
|
84
|
+
requireActive() {
|
|
85
|
+
if (this.active == null)
|
|
86
|
+
throw new Error("issuer keyset is disposed");
|
|
87
|
+
return this.active;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function copySigningSeed(signingSeed) {
|
|
91
|
+
if (signingSeed.length !== 32)
|
|
92
|
+
throw new TypeError("Ed25519 signing seed must be 32 bytes");
|
|
93
|
+
return signingSeed.slice();
|
|
94
|
+
}
|
|
95
|
+
function copyPublicKey(publicKey) {
|
|
96
|
+
if (publicKey.length !== 32)
|
|
97
|
+
throw new TypeError("Ed25519 public key must be 32 bytes");
|
|
98
|
+
return publicKey.slice();
|
|
99
|
+
}
|
|
100
|
+
function equalBytes(left, right) {
|
|
101
|
+
if (left.length !== right.length)
|
|
102
|
+
return false;
|
|
103
|
+
let diff = 0;
|
|
104
|
+
for (let index = 0; index < left.length; index++)
|
|
105
|
+
diff |= left[index] ^ right[index];
|
|
106
|
+
return diff === 0;
|
|
47
107
|
}
|
|
48
108
|
function normalizeKid(kid) {
|
|
49
109
|
const value = kid.trim();
|
|
@@ -77,7 +77,9 @@ async function readControlplaneText(response, maxBytes) {
|
|
|
77
77
|
try {
|
|
78
78
|
await reader.cancel();
|
|
79
79
|
}
|
|
80
|
-
catch {
|
|
80
|
+
catch {
|
|
81
|
+
// The size violation is authoritative; cancellation is secondary cleanup.
|
|
82
|
+
}
|
|
81
83
|
throw new ControlplaneResponseTooLargeError(maxBytes);
|
|
82
84
|
}
|
|
83
85
|
text += decoder.decode(chunk.value, { stream: true });
|
package/dist/defaults.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const SDK_DEFAULTS: Readonly<{
|
|
|
8
8
|
maxHandshakePayloadBytes: number;
|
|
9
9
|
maxRecordBytes: number;
|
|
10
10
|
outboundRecordChunkBytes: number;
|
|
11
|
+
maxInboundBufferedBytes: number;
|
|
11
12
|
maxOutboundBufferedBytes: number;
|
|
12
13
|
}>;
|
|
13
14
|
yamux: Readonly<{
|
|
@@ -15,6 +16,7 @@ export declare const SDK_DEFAULTS: Readonly<{
|
|
|
15
16
|
maxInboundStreams: 32;
|
|
16
17
|
maxFrameBytes: number;
|
|
17
18
|
preferredOutboundFrameBytes: number;
|
|
19
|
+
maxStreamWriteQueueBytes: number;
|
|
18
20
|
maxStreamReceiveBytes: number;
|
|
19
21
|
maxSessionReceiveBytes: number;
|
|
20
22
|
}>;
|
package/dist/defaults.js
CHANGED
|
@@ -8,6 +8,7 @@ export const SDK_DEFAULTS = Object.freeze({
|
|
|
8
8
|
maxHandshakePayloadBytes: 8 * 1024,
|
|
9
9
|
maxRecordBytes: 1024 * 1024,
|
|
10
10
|
outboundRecordChunkBytes: 64 * 1024,
|
|
11
|
+
maxInboundBufferedBytes: 4 * 1024 * 1024,
|
|
11
12
|
maxOutboundBufferedBytes: 4 * 1024 * 1024,
|
|
12
13
|
}),
|
|
13
14
|
yamux: Object.freeze({
|
|
@@ -15,6 +16,7 @@ export const SDK_DEFAULTS = Object.freeze({
|
|
|
15
16
|
maxInboundStreams: 32,
|
|
16
17
|
maxFrameBytes: 256 * 1024,
|
|
17
18
|
preferredOutboundFrameBytes: 64 * 1024,
|
|
19
|
+
maxStreamWriteQueueBytes: 4 * 1024 * 1024,
|
|
18
20
|
maxStreamReceiveBytes: 256 * 1024,
|
|
19
21
|
maxSessionReceiveBytes: 16 * 1024 * 1024,
|
|
20
22
|
}),
|
package/dist/e2ee/handshake.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type HandshakeClientOptions = Readonly<{
|
|
|
16
16
|
maxRecordBytes: number;
|
|
17
17
|
/** Preferred plaintext bytes per outbound record. */
|
|
18
18
|
outboundRecordChunkBytes?: number;
|
|
19
|
-
/** Maximum
|
|
19
|
+
/** Maximum queued inbound plaintext bytes for the secure channel. */
|
|
20
20
|
maxBufferedBytes?: number;
|
|
21
21
|
/** Maximum queued outbound plaintext bytes for the secure channel. */
|
|
22
22
|
maxOutboundBufferedBytes?: number;
|
|
@@ -44,7 +44,7 @@ export type HandshakeServerOptions = Readonly<{
|
|
|
44
44
|
maxRecordBytes: number;
|
|
45
45
|
/** Preferred plaintext bytes per outbound record. */
|
|
46
46
|
outboundRecordChunkBytes?: number;
|
|
47
|
-
/** Maximum
|
|
47
|
+
/** Maximum queued inbound plaintext bytes for the secure channel. */
|
|
48
48
|
maxBufferedBytes?: number;
|
|
49
49
|
/** Maximum queued outbound plaintext bytes for the secure channel. */
|
|
50
50
|
maxOutboundBufferedBytes?: number;
|
|
@@ -31,7 +31,7 @@ export declare class SecureChannel {
|
|
|
31
31
|
private readonly transport;
|
|
32
32
|
private readonly maxRecordBytes;
|
|
33
33
|
private readonly outboundRecordChunkBytes;
|
|
34
|
-
private readonly
|
|
34
|
+
private readonly maxInboundBufferedBytes;
|
|
35
35
|
private readonly maxOutboundBufferedBytes;
|
|
36
36
|
private sendKey;
|
|
37
37
|
private recvKey;
|
|
@@ -50,7 +50,7 @@ export declare class SecureChannel {
|
|
|
50
50
|
private sendQueueBytes;
|
|
51
51
|
private sendClosed;
|
|
52
52
|
private sendErr;
|
|
53
|
-
private
|
|
53
|
+
private recvQueue;
|
|
54
54
|
private recvQueueHead;
|
|
55
55
|
private recvQueueBytes;
|
|
56
56
|
private recvWaiters;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RECORD_FLAG_APP, RECORD_FLAG_PING, RECORD_FLAG_REKEY } from "./constants.js";
|
|
2
2
|
import { decryptRecord, encryptRecord, maxPlaintextBytes } from "./record.js";
|
|
3
3
|
import { deriveRekeyKey } from "./kdf.js";
|
|
4
|
+
import { SDK_DEFAULTS } from "../defaults.js";
|
|
4
5
|
const maxRecordSeq = (1n << 64n) - 1n;
|
|
5
6
|
class RecordSeqExhaustedError extends Error {
|
|
6
7
|
constructor() {
|
|
@@ -15,8 +16,7 @@ export class SecureChannel {
|
|
|
15
16
|
// Maximum allowed bytes per record frame.
|
|
16
17
|
maxRecordBytes;
|
|
17
18
|
outboundRecordChunkBytes;
|
|
18
|
-
|
|
19
|
-
maxBufferedBytes;
|
|
19
|
+
maxInboundBufferedBytes;
|
|
20
20
|
maxOutboundBufferedBytes;
|
|
21
21
|
// Active encryption keys and nonce prefixes for the current epoch.
|
|
22
22
|
sendKey;
|
|
@@ -52,16 +52,16 @@ export class SecureChannel {
|
|
|
52
52
|
this.transport = args.transport;
|
|
53
53
|
this.maxRecordBytes = args.maxRecordBytes;
|
|
54
54
|
const maxPlain = Math.max(1, maxPlaintextBytes(this.maxRecordBytes));
|
|
55
|
-
this.outboundRecordChunkBytes = args.outboundRecordChunkBytes ?? Math.min(
|
|
55
|
+
this.outboundRecordChunkBytes = args.outboundRecordChunkBytes ?? Math.min(SDK_DEFAULTS.e2ee.outboundRecordChunkBytes, maxPlain);
|
|
56
56
|
if (!Number.isSafeInteger(this.outboundRecordChunkBytes) || this.outboundRecordChunkBytes <= 0 || this.outboundRecordChunkBytes > maxPlain) {
|
|
57
57
|
throw new RangeError("outboundRecordChunkBytes must be a positive integer within the record plaintext limit");
|
|
58
58
|
}
|
|
59
|
-
this.
|
|
60
|
-
const maxOutboundBufferedBytes = args.maxOutboundBufferedBytes ??
|
|
59
|
+
this.maxInboundBufferedBytes = Math.max(0, args.maxBufferedBytes ?? SDK_DEFAULTS.e2ee.maxInboundBufferedBytes);
|
|
60
|
+
const maxOutboundBufferedBytes = args.maxOutboundBufferedBytes ?? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes;
|
|
61
61
|
if (!Number.isSafeInteger(maxOutboundBufferedBytes) || maxOutboundBufferedBytes < 0) {
|
|
62
62
|
throw new RangeError("maxOutboundBufferedBytes must be a non-negative safe integer");
|
|
63
63
|
}
|
|
64
|
-
this.maxOutboundBufferedBytes = maxOutboundBufferedBytes === 0 ?
|
|
64
|
+
this.maxOutboundBufferedBytes = maxOutboundBufferedBytes === 0 ? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes : maxOutboundBufferedBytes;
|
|
65
65
|
this.sendKey = args.sendKey;
|
|
66
66
|
this.recvKey = args.recvKey;
|
|
67
67
|
this.sendNoncePrefix = args.sendNoncePrefix;
|
|
@@ -87,7 +87,11 @@ export class SecureChannel {
|
|
|
87
87
|
if (this.recvQueueHead < this.recvQueue.length) {
|
|
88
88
|
const b = this.recvQueue[this.recvQueueHead];
|
|
89
89
|
this.recvQueueHead++;
|
|
90
|
-
if (this.recvQueueHead
|
|
90
|
+
if (this.recvQueueHead === this.recvQueue.length) {
|
|
91
|
+
this.recvQueue = [];
|
|
92
|
+
this.recvQueueHead = 0;
|
|
93
|
+
}
|
|
94
|
+
else if (this.recvQueueHead > 1024 && this.recvQueueHead * 2 > this.recvQueue.length) {
|
|
91
95
|
this.recvQueue.splice(0, this.recvQueueHead);
|
|
92
96
|
this.recvQueueHead = 0;
|
|
93
97
|
}
|
|
@@ -171,7 +175,11 @@ export class SecureChannel {
|
|
|
171
175
|
return null;
|
|
172
176
|
const req = this.sendQueue[this.sendQueueHead];
|
|
173
177
|
this.sendQueueHead++;
|
|
174
|
-
if (this.sendQueueHead
|
|
178
|
+
if (this.sendQueueHead === this.sendQueue.length) {
|
|
179
|
+
this.sendQueue = [];
|
|
180
|
+
this.sendQueueHead = 0;
|
|
181
|
+
}
|
|
182
|
+
else if (this.sendQueueHead > 1024 && this.sendQueueHead * 2 > this.sendQueue.length) {
|
|
175
183
|
this.sendQueue.splice(0, this.sendQueueHead);
|
|
176
184
|
this.sendQueueHead = 0;
|
|
177
185
|
}
|
|
@@ -182,7 +190,11 @@ export class SecureChannel {
|
|
|
182
190
|
return undefined;
|
|
183
191
|
const w = this.sendWaiters[this.sendWaitersHead];
|
|
184
192
|
this.sendWaitersHead++;
|
|
185
|
-
if (this.sendWaitersHead
|
|
193
|
+
if (this.sendWaitersHead === this.sendWaiters.length) {
|
|
194
|
+
this.sendWaiters = [];
|
|
195
|
+
this.sendWaitersHead = 0;
|
|
196
|
+
}
|
|
197
|
+
else if (this.sendWaitersHead > 1024 && this.sendWaitersHead * 2 > this.sendWaiters.length) {
|
|
186
198
|
this.sendWaiters.splice(0, this.sendWaitersHead);
|
|
187
199
|
this.sendWaitersHead = 0;
|
|
188
200
|
}
|
|
@@ -284,7 +296,7 @@ export class SecureChannel {
|
|
|
284
296
|
}
|
|
285
297
|
this.recvSeq = seq + 1n;
|
|
286
298
|
if (flags === RECORD_FLAG_APP) {
|
|
287
|
-
if (this.
|
|
299
|
+
if (this.maxInboundBufferedBytes > 0 && this.recvQueueBytes + plaintext.length > this.maxInboundBufferedBytes) {
|
|
288
300
|
throw new Error("recv buffer exceeded");
|
|
289
301
|
}
|
|
290
302
|
this.recvQueue.push(plaintext);
|
package/dist/endpoint/index.d.ts
CHANGED
package/dist/endpoint/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { decodeHandshakeFrame } from "../e2ee/framing.js";
|
|
|
8
8
|
import { HANDSHAKE_TYPE_INIT, PROTOCOL_VERSION } from "../e2ee/constants.js";
|
|
9
9
|
import { readStreamHello, writeStreamHello } from "../streamhello/streamHello.js";
|
|
10
10
|
import { ByteReader } from "../yamux/byteReader.js";
|
|
11
|
+
import { isYamuxPingTimeoutError } from "../yamux/errors.js";
|
|
11
12
|
import { YamuxSession } from "../yamux/session.js";
|
|
12
13
|
import { RpcServer } from "../rpc/server.js";
|
|
13
14
|
import { base64urlDecode, base64urlEncode } from "../utils/base64url.js";
|
|
@@ -43,9 +44,10 @@ export class Session {
|
|
|
43
44
|
return session;
|
|
44
45
|
}
|
|
45
46
|
async openStream(kind, options = {}) {
|
|
47
|
+
const streamKind = normalizeStreamKind(kind, this.path);
|
|
46
48
|
const stream = await this.mux.openStream(options);
|
|
47
49
|
try {
|
|
48
|
-
await writeStreamHello((bytes) => stream.write(bytes),
|
|
50
|
+
await writeStreamHello((bytes) => stream.write(bytes), streamKind);
|
|
49
51
|
return stream;
|
|
50
52
|
}
|
|
51
53
|
catch (error) {
|
|
@@ -82,8 +84,27 @@ export class Session {
|
|
|
82
84
|
return;
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
|
-
probeLiveness(timeoutMs = SDK_DEFAULTS.transport.handshakeTimeoutMs) {
|
|
86
|
-
|
|
87
|
+
async probeLiveness(timeoutMs = SDK_DEFAULTS.transport.handshakeTimeoutMs) {
|
|
88
|
+
try {
|
|
89
|
+
return await this.mux.probeLiveness(timeoutMs);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new FlowersecError({
|
|
93
|
+
path: this.path,
|
|
94
|
+
stage: "yamux",
|
|
95
|
+
code: isYamuxPingTimeoutError(error) ? "timeout" : "ping_failed",
|
|
96
|
+
message: "endpoint liveness probe failed",
|
|
97
|
+
cause: error,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async rekey() {
|
|
102
|
+
try {
|
|
103
|
+
await this.secure.rekeyNow();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new FlowersecError({ path: this.path, stage: "secure", code: "rekey_failed", message: "endpoint rekey failed", cause: error });
|
|
107
|
+
}
|
|
87
108
|
}
|
|
88
109
|
close() {
|
|
89
110
|
this.fail(new Error("endpoint session closed"));
|
|
@@ -239,7 +260,7 @@ async function establishSession(path, transport, handshake, options, endpointIns
|
|
|
239
260
|
maxHandshakePayload: options.maxHandshakePayload ?? SDK_DEFAULTS.e2ee.maxHandshakePayloadBytes,
|
|
240
261
|
maxRecordBytes: options.maxRecordBytes ?? SDK_DEFAULTS.e2ee.maxRecordBytes,
|
|
241
262
|
outboundRecordChunkBytes: options.outboundRecordChunkBytes ?? SDK_DEFAULTS.e2ee.outboundRecordChunkBytes,
|
|
242
|
-
maxBufferedBytes: options.maxBufferedBytes ?? SDK_DEFAULTS.e2ee.
|
|
263
|
+
maxBufferedBytes: options.maxBufferedBytes ?? SDK_DEFAULTS.e2ee.maxInboundBufferedBytes,
|
|
243
264
|
maxOutboundBufferedBytes: options.maxOutboundBufferedBytes ?? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes,
|
|
244
265
|
timeoutMs: options.handshakeTimeoutMs ?? SDK_DEFAULTS.transport.handshakeTimeoutMs,
|
|
245
266
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
@@ -319,7 +340,7 @@ function randomEndpointInstanceId() {
|
|
|
319
340
|
function normalizeStreamKind(kind, path) {
|
|
320
341
|
const value = kind.trim();
|
|
321
342
|
if (value === "")
|
|
322
|
-
throw new FlowersecError({ path, stage: "
|
|
343
|
+
throw new FlowersecError({ path, stage: "rpc", code: "missing_stream_kind", message: "missing stream kind" });
|
|
323
344
|
return value;
|
|
324
345
|
}
|
|
325
346
|
function unwrapServerGrant(input) {
|
|
@@ -78,11 +78,10 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
78
78
|
terminal = true;
|
|
79
79
|
acceptingWrites = false;
|
|
80
80
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
81
|
-
|
|
82
|
-
stream
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Best-effort.
|
|
81
|
+
if (stream != null) {
|
|
82
|
+
void Promise.resolve(stream.reset(err)).catch(() => {
|
|
83
|
+
// The bridge error is already delivered through the terminal response.
|
|
84
|
+
});
|
|
86
85
|
}
|
|
87
86
|
try {
|
|
88
87
|
ac.abort(err.message);
|
package/dist/proxy/runtime.js
CHANGED
|
@@ -416,13 +416,13 @@ export function createProxyRuntime(opts) {
|
|
|
416
416
|
}
|
|
417
417
|
if (!respMeta.ok) {
|
|
418
418
|
const msg = respMeta.error?.message ?? "upstream error";
|
|
419
|
-
port.postMessage({
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
419
|
+
port.postMessage({
|
|
420
|
+
type: "flowersec-proxy:response_error",
|
|
421
|
+
status: 502,
|
|
422
|
+
...(respMeta.error?.code === undefined ? {} : { code: respMeta.error.code }),
|
|
423
|
+
message: msg,
|
|
424
|
+
});
|
|
425
|
+
await stream.reset(new Error(msg));
|
|
426
426
|
stream = null;
|
|
427
427
|
return;
|
|
428
428
|
}
|
|
@@ -455,14 +455,10 @@ export function createProxyRuntime(opts) {
|
|
|
455
455
|
port.postMessage({
|
|
456
456
|
type: "flowersec-proxy:response_error",
|
|
457
457
|
status,
|
|
458
|
+
...(code === undefined ? {} : { code }),
|
|
458
459
|
message: msg,
|
|
459
460
|
});
|
|
460
|
-
|
|
461
|
-
stream?.reset(new Error(msg));
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
// Best-effort.
|
|
465
|
-
}
|
|
461
|
+
await stream?.reset(new Error(msg));
|
|
466
462
|
}
|
|
467
463
|
finally {
|
|
468
464
|
wakeResponseCreditWaiter();
|
|
@@ -494,12 +490,7 @@ export function createProxyRuntime(opts) {
|
|
|
494
490
|
const resp = (await readJsonFrame(reader, maxJsonFrameBytes));
|
|
495
491
|
if (resp.v !== PROXY_PROTOCOL_VERSION || resp.ok !== true) {
|
|
496
492
|
const msg = resp.error?.message ?? "upstream ws open failed";
|
|
497
|
-
|
|
498
|
-
stream.reset(new Error(msg));
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
// Best-effort.
|
|
502
|
-
}
|
|
493
|
+
await stream.reset(new Error(msg));
|
|
503
494
|
throw new Error(msg);
|
|
504
495
|
}
|
|
505
496
|
return { stream, protocol: resp.protocol ?? "" };
|