@floegence/flowersec-core 0.24.0 → 0.26.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.
@@ -1,4 +1,5 @@
1
1
  import type { YamuxStream } from "../yamux/stream.js";
2
2
  export declare function createMessagePortBackedStream(port: MessagePort, opts?: Readonly<{
3
- writeAcknowledgements?: boolean;
3
+ maxBufferedBytes?: number;
4
+ onTerminal?: () => void;
4
5
  }>): YamuxStream;
@@ -4,79 +4,151 @@ function cloneChunk(chunk) {
4
4
  out.set(chunk);
5
5
  return out.buffer;
6
6
  }
7
+ function validWriteId(value) {
8
+ return Number.isSafeInteger(value) && Number(value) > 0;
9
+ }
7
10
  export function createMessagePortBackedStream(port, opts = {}) {
8
11
  let closed = false;
12
+ let acceptingWrites = true;
9
13
  let error = null;
10
14
  const queue = [];
11
15
  const waiters = [];
12
- const pendingWrites = new Map();
16
+ let pendingInboundWriteId = null;
17
+ let pendingOutboundAcknowledgement = null;
13
18
  let nextWriteId = 1;
14
- const resolveWaiter = (value) => {
15
- const waiter = waiters.shift();
16
- if (waiter) {
17
- waiter(value);
18
- return true;
19
+ let writeTail = Promise.resolve();
20
+ let queuedWriteBytes = 0;
21
+ const maxBufferedBytes = opts.maxBufferedBytes ?? 4 * (1 << 20);
22
+ let terminalNotified = false;
23
+ const notifyTerminal = () => {
24
+ if (terminalNotified)
25
+ return;
26
+ terminalNotified = true;
27
+ opts.onTerminal?.();
28
+ };
29
+ const closePort = () => {
30
+ try {
31
+ port.close();
32
+ }
33
+ catch {
34
+ // Best-effort.
19
35
  }
20
- return false;
21
36
  };
22
- const pushValue = (value) => {
23
- if (resolveWaiter(value))
24
- return;
25
- queue.push(value);
37
+ const drainWaiters = (value) => {
38
+ for (const waiter of waiters.splice(0))
39
+ waiter(value);
26
40
  };
27
- const fail = (err) => {
41
+ const terminateWithError = (err, notifyPeer) => {
28
42
  if (error != null)
29
43
  return;
30
44
  error = err;
31
- while (resolveWaiter(err)) {
32
- // Drain waiters.
45
+ closed = true;
46
+ acceptingWrites = false;
47
+ queue.length = 0;
48
+ pendingInboundWriteId = null;
49
+ drainWaiters(err);
50
+ pendingOutboundAcknowledgement?.reject(err);
51
+ pendingOutboundAcknowledgement = null;
52
+ if (notifyPeer) {
53
+ try {
54
+ port.postMessage({
55
+ type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE,
56
+ message: err.message,
57
+ });
58
+ }
59
+ catch {
60
+ // The local error remains authoritative.
61
+ }
62
+ }
63
+ closePort();
64
+ notifyTerminal();
65
+ };
66
+ const failProtocol = (message) => {
67
+ terminateWithError(new Error(`proxy Window stream protocol error: ${message}`), true);
68
+ };
69
+ const finishClosed = () => {
70
+ if (closed)
71
+ return;
72
+ if (pendingInboundWriteId != null) {
73
+ failProtocol("stream ended before the pending chunk was acknowledged");
74
+ return;
75
+ }
76
+ closed = true;
77
+ acceptingWrites = false;
78
+ const closeError = new Error("stream is closed");
79
+ pendingOutboundAcknowledgement?.reject(closeError);
80
+ pendingOutboundAcknowledgement = null;
81
+ drainWaiters(null);
82
+ closePort();
83
+ notifyTerminal();
84
+ };
85
+ const acknowledgeRead = (item) => {
86
+ if (closed)
87
+ return;
88
+ if (pendingInboundWriteId !== item.writeId) {
89
+ failProtocol("read acknowledgement does not match the pending chunk");
90
+ throw error;
91
+ }
92
+ pendingInboundWriteId = null;
93
+ try {
94
+ port.postMessage({
95
+ type: PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE,
96
+ writeId: item.writeId,
97
+ });
98
+ }
99
+ catch (cause) {
100
+ const err = cause instanceof Error ? cause : new Error(String(cause));
101
+ terminateWithError(err, false);
102
+ throw err;
33
103
  }
34
- for (const pending of pendingWrites.values())
35
- pending.reject(err);
36
- pendingWrites.clear();
37
104
  };
38
105
  port.onmessage = (ev) => {
106
+ if (closed)
107
+ return;
39
108
  const data = ev.data;
40
109
  if (data == null || typeof data !== "object")
41
110
  return;
42
111
  const type = typeof data.type === "string" ? data.type : "";
43
112
  switch (type) {
44
113
  case PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE: {
45
- const raw = data.data;
46
- if (!(raw instanceof ArrayBuffer))
114
+ const msg = data;
115
+ if (!(msg.data instanceof ArrayBuffer) || !validWriteId(msg.writeId)) {
116
+ failProtocol("invalid stream chunk");
117
+ return;
118
+ }
119
+ if (pendingInboundWriteId != null || queue.length > 0) {
120
+ failProtocol("received more than one unacknowledged chunk");
47
121
  return;
48
- pushValue(new Uint8Array(raw));
122
+ }
123
+ pendingInboundWriteId = msg.writeId;
124
+ const item = { chunk: new Uint8Array(msg.data), writeId: msg.writeId };
125
+ const waiter = waiters.shift();
126
+ if (waiter == null) {
127
+ queue.push(item);
128
+ }
129
+ else {
130
+ waiter(item);
131
+ }
49
132
  return;
50
133
  }
51
134
  case PROXY_WINDOW_STREAM_END_MSG_TYPE:
52
135
  case PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE:
53
- closed = true;
54
- for (const pending of pendingWrites.values())
55
- pending.reject(new Error("stream is closed"));
56
- pendingWrites.clear();
57
- pushValue(null);
136
+ finishClosed();
58
137
  return;
59
138
  case PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE: {
60
139
  const writeId = data.writeId;
61
- if (!Number.isSafeInteger(writeId) || writeId <= 0)
62
- return;
63
- const pending = pendingWrites.get(writeId);
64
- if (pending == null)
140
+ const pending = pendingOutboundAcknowledgement;
141
+ if (!validWriteId(writeId) || pending == null || pending.writeId !== writeId) {
142
+ failProtocol("unexpected stream write acknowledgement");
65
143
  return;
66
- pendingWrites.delete(writeId);
144
+ }
145
+ pendingOutboundAcknowledgement = null;
67
146
  pending.resolve();
68
147
  return;
69
148
  }
70
149
  case PROXY_WINDOW_STREAM_RESET_MSG_TYPE: {
71
- closed = true;
72
150
  const message = String(data.message ?? "stream reset");
73
- fail(new Error(message));
74
- try {
75
- port.close();
76
- }
77
- catch {
78
- // Best-effort.
79
- }
151
+ terminateWithError(new Error(message), false);
80
152
  return;
81
153
  }
82
154
  default:
@@ -88,9 +160,11 @@ export function createMessagePortBackedStream(port, opts = {}) {
88
160
  async read() {
89
161
  if (error != null)
90
162
  throw error;
91
- const next = queue.shift();
92
- if (next !== undefined)
93
- return next;
163
+ const queued = queue.shift();
164
+ if (queued != null) {
165
+ acknowledgeRead(queued);
166
+ return queued.chunk;
167
+ }
94
168
  if (closed)
95
169
  return null;
96
170
  const value = await new Promise((resolve) => {
@@ -98,60 +172,85 @@ export function createMessagePortBackedStream(port, opts = {}) {
98
172
  });
99
173
  if (value instanceof Error)
100
174
  throw value;
101
- return value;
175
+ if (value == null)
176
+ return null;
177
+ acknowledgeRead(value);
178
+ return value.chunk;
102
179
  },
103
180
  async write(chunk) {
104
181
  if (error != null)
105
182
  throw error;
106
- if (closed)
183
+ if (!acceptingWrites || closed)
107
184
  throw new Error("stream is closed");
108
- const ab = cloneChunk(chunk);
109
- if (opts.writeAcknowledgements !== true) {
110
- port.postMessage({ type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, data: ab }, [ab]);
111
- return;
185
+ const nextQueuedWriteBytes = queuedWriteBytes + chunk.byteLength;
186
+ if (!Number.isSafeInteger(nextQueuedWriteBytes) || nextQueuedWriteBytes > maxBufferedBytes) {
187
+ throw new Error("proxy WebSocket outbound buffer exceeded");
112
188
  }
113
- const writeId = nextWriteId++;
114
- await new Promise((resolve, reject) => {
115
- pendingWrites.set(writeId, { resolve, reject });
116
- try {
117
- port.postMessage({ type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, data: ab, writeId }, [ab]);
189
+ queuedWriteBytes = nextQueuedWriteBytes;
190
+ const ab = cloneChunk(chunk);
191
+ const operation = writeTail.then(async () => {
192
+ if (error != null)
193
+ throw error;
194
+ if (closed)
195
+ throw new Error("stream is closed");
196
+ if (!Number.isSafeInteger(nextWriteId)) {
197
+ failProtocol("stream write identifier space exhausted");
198
+ throw error;
118
199
  }
119
- catch (error) {
120
- pendingWrites.delete(writeId);
121
- reject(error instanceof Error ? error : new Error(String(error)));
200
+ if (pendingOutboundAcknowledgement != null) {
201
+ failProtocol("multiple outbound chunks are awaiting acknowledgement");
202
+ throw error;
122
203
  }
204
+ const writeId = nextWriteId++;
205
+ await new Promise((resolve, reject) => {
206
+ pendingOutboundAcknowledgement = { writeId, resolve, reject };
207
+ try {
208
+ port.postMessage({
209
+ type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE,
210
+ data: ab,
211
+ writeId,
212
+ }, [ab]);
213
+ }
214
+ catch (cause) {
215
+ pendingOutboundAcknowledgement = null;
216
+ const postError = cause instanceof Error ? cause : new Error(String(cause));
217
+ terminateWithError(postError, false);
218
+ reject(postError);
219
+ }
220
+ });
123
221
  });
222
+ writeTail = operation.catch(() => { });
223
+ try {
224
+ await operation;
225
+ }
226
+ finally {
227
+ queuedWriteBytes = Math.max(0, queuedWriteBytes - chunk.byteLength);
228
+ }
124
229
  },
125
230
  async close() {
231
+ if (closed)
232
+ return;
233
+ acceptingWrites = false;
234
+ await writeTail;
235
+ if (error != null)
236
+ throw error;
126
237
  if (closed)
127
238
  return;
128
239
  closed = true;
129
- const closeError = new Error("stream is closed");
130
- for (const pending of pendingWrites.values())
131
- pending.reject(closeError);
132
- pendingWrites.clear();
240
+ queue.length = 0;
241
+ pendingInboundWriteId = null;
242
+ drainWaiters(null);
133
243
  try {
134
244
  port.postMessage({ type: PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE });
135
245
  }
136
246
  finally {
137
- pushValue(null);
247
+ closePort();
248
+ notifyTerminal();
138
249
  }
139
250
  },
140
251
  reset(err) {
141
- const message = err instanceof Error ? err.message : String(err);
142
- closed = true;
143
- fail(err instanceof Error ? err : new Error(message));
144
- try {
145
- port.postMessage({ type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE, message });
146
- }
147
- finally {
148
- try {
149
- port.close();
150
- }
151
- catch {
152
- // Best-effort.
153
- }
154
- }
252
+ const resetError = err instanceof Error ? err : new Error(String(err));
253
+ terminateWithError(resetError, true);
155
254
  },
156
255
  };
157
256
  }
@@ -26,5 +26,4 @@ export type ResolvedProxyPreset = Readonly<{
26
26
  export declare const DEFAULT_PROXY_PRESET_MANIFEST: ProxyPresetManifest;
27
27
  export type ProxyPresetInput = ProxyPresetManifest | Partial<ProxyPresetLimits>;
28
28
  export declare function assertProxyPresetManifest(value: unknown): ProxyPresetManifest;
29
- export declare function resolveNamedProxyPreset(name: string): ProxyPresetManifest;
30
29
  export declare function resolveProxyPreset(input?: ProxyPresetInput): ResolvedProxyPreset;
@@ -75,15 +75,6 @@ export function assertProxyPresetManifest(value) {
75
75
  limits: assertLimits(value.limits),
76
76
  });
77
77
  }
78
- export function resolveNamedProxyPreset(name) {
79
- switch (String(name ?? "").trim()) {
80
- case "":
81
- case "default":
82
- return DEFAULT_PROXY_PRESET_MANIFEST;
83
- default:
84
- throw new Error(`unknown proxy preset: ${name}`);
85
- }
86
- }
87
78
  function toResolved(manifest) {
88
79
  return Object.freeze({
89
80
  v: 1,
@@ -1,4 +1,4 @@
1
- import { assertProxyPresetManifest, resolveNamedProxyPreset, } from "./preset.js";
1
+ import { assertProxyPresetManifest, } from "./preset.js";
2
2
  const RUNTIME_SCOPE_NAME = "proxy.runtime";
3
3
  const PRESET_ID_RE = /^[a-z][a-z0-9._-]{0,63}$/;
4
4
  const MAX_RUNTIME_PAYLOAD_BYTES = 8 * 1024;
@@ -203,12 +203,7 @@ export function resolvePresetInputFromScope(scope, presetOverride) {
203
203
  return presetOverride;
204
204
  if (scope.preset.snapshot)
205
205
  return scope.preset.snapshot;
206
- try {
207
- return resolveNamedProxyPreset(scope.preset.presetId);
208
- }
209
- catch {
210
- return undefined;
211
- }
206
+ throw new Error("proxy.runtime preset snapshot is required");
212
207
  }
213
208
  export function resolveRuntimePresetLimits(scope) {
214
209
  if (scope.limits === undefined)
@@ -10,6 +10,7 @@ export type ProxyServerOptions = Readonly<{
10
10
  maxChunkBytes?: number;
11
11
  maxBodyBytes?: number;
12
12
  maxWsFrameBytes?: number;
13
+ maxWsQueuedBytes?: number;
13
14
  defaultTimeoutMs?: number;
14
15
  maxTimeoutMs?: number;
15
16
  maxConcurrentStreams?: number;