@floegence/flowersec-core 0.19.0 → 0.19.1
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.
|
@@ -13,6 +13,13 @@ export class ControlplaneRequestError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
export const DEFAULT_CONNECT_ARTIFACT_PATH = "/v1/connect/artifact";
|
|
15
15
|
export const DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH = "/v1/connect/artifact/entry";
|
|
16
|
+
const DEFAULT_MAX_CONTROLPLANE_RESPONSE_BYTES = 1 << 20;
|
|
17
|
+
class ControlplaneResponseTooLargeError extends Error {
|
|
18
|
+
constructor(maxBytes) {
|
|
19
|
+
super(`controlplane response exceeded ${maxBytes} bytes`);
|
|
20
|
+
this.name = "ControlplaneResponseTooLargeError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
16
23
|
function resolveFetch(fetchImpl) {
|
|
17
24
|
if (fetchImpl)
|
|
18
25
|
return fetchImpl;
|
|
@@ -37,6 +44,46 @@ function parseMaybeJSON(bodyText) {
|
|
|
37
44
|
return trimmed;
|
|
38
45
|
}
|
|
39
46
|
}
|
|
47
|
+
function parseContentLength(headerValue) {
|
|
48
|
+
const raw = String(headerValue ?? "").trim();
|
|
49
|
+
if (raw === "")
|
|
50
|
+
return null;
|
|
51
|
+
if (!/^[0-9]+$/.test(raw))
|
|
52
|
+
return null;
|
|
53
|
+
const parsed = Number(raw);
|
|
54
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0)
|
|
55
|
+
return null;
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
async function readControlplaneText(response, maxBytes) {
|
|
59
|
+
const contentLength = parseContentLength(response.headers.get("Content-Length"));
|
|
60
|
+
if (contentLength !== null && contentLength > maxBytes) {
|
|
61
|
+
throw new ControlplaneResponseTooLargeError(maxBytes);
|
|
62
|
+
}
|
|
63
|
+
if (!response.body) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
const reader = response.body.getReader();
|
|
67
|
+
const decoder = new TextDecoder();
|
|
68
|
+
let totalBytes = 0;
|
|
69
|
+
let text = "";
|
|
70
|
+
while (true) {
|
|
71
|
+
const chunk = await reader.read();
|
|
72
|
+
if (chunk.done)
|
|
73
|
+
break;
|
|
74
|
+
totalBytes += chunk.value.byteLength;
|
|
75
|
+
if (totalBytes > maxBytes) {
|
|
76
|
+
try {
|
|
77
|
+
await reader.cancel();
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
throw new ControlplaneResponseTooLargeError(maxBytes);
|
|
81
|
+
}
|
|
82
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
83
|
+
}
|
|
84
|
+
text += decoder.decode();
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
40
87
|
function decodeErrorMessage(status, responseBody) {
|
|
41
88
|
let message = `controlplane request failed: ${status}`;
|
|
42
89
|
let code = "";
|
|
@@ -61,7 +108,19 @@ export async function requestControlplaneJSON(url, init) {
|
|
|
61
108
|
...init,
|
|
62
109
|
cache: "no-store",
|
|
63
110
|
});
|
|
64
|
-
|
|
111
|
+
let rawBody = "";
|
|
112
|
+
try {
|
|
113
|
+
rawBody = await readControlplaneText(response, DEFAULT_MAX_CONTROLPLANE_RESPONSE_BYTES);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (err instanceof ControlplaneResponseTooLargeError && !response.ok) {
|
|
117
|
+
throw new ControlplaneRequestError({
|
|
118
|
+
status: response.status,
|
|
119
|
+
message: err.message,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
65
124
|
const responseBody = parseMaybeJSON(rawBody);
|
|
66
125
|
if (!response.ok) {
|
|
67
126
|
const error = decodeErrorMessage(response.status, responseBody);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare class CookieJar {
|
|
2
2
|
private readonly cookies;
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
private nextCreatedAtSeq;
|
|
4
|
+
setCookie(setCookieHeader: string, requestPath?: string): void;
|
|
5
|
+
updateFromSetCookieHeaders(headers: readonly string[], requestPath?: string): void;
|
|
5
6
|
getCookieHeader(path: string): string;
|
|
6
7
|
}
|
package/dist/proxy/cookieJar.js
CHANGED
|
@@ -11,9 +11,54 @@ function parseCookieNameValue(s) {
|
|
|
11
11
|
return null;
|
|
12
12
|
return { name, value };
|
|
13
13
|
}
|
|
14
|
+
function cookieStorageKey(name, path) {
|
|
15
|
+
return `${name}\u0000${path}`;
|
|
16
|
+
}
|
|
17
|
+
function requestPathOnly(path) {
|
|
18
|
+
const raw = path.trim();
|
|
19
|
+
if (!raw.startsWith("/"))
|
|
20
|
+
return "/";
|
|
21
|
+
const q = raw.indexOf("?");
|
|
22
|
+
const out = q >= 0 ? raw.slice(0, q) : raw;
|
|
23
|
+
return out === "" ? "/" : out;
|
|
24
|
+
}
|
|
25
|
+
function defaultCookiePathFromRequestPath(requestPath) {
|
|
26
|
+
const path = requestPathOnly(requestPath);
|
|
27
|
+
if (path === "/")
|
|
28
|
+
return "/";
|
|
29
|
+
const lastSlash = path.lastIndexOf("/");
|
|
30
|
+
if (lastSlash <= 0)
|
|
31
|
+
return "/";
|
|
32
|
+
return path.slice(0, lastSlash);
|
|
33
|
+
}
|
|
34
|
+
function normalizeCookiePath(pathAttr, requestPath) {
|
|
35
|
+
const path = pathAttr?.trim() ?? "";
|
|
36
|
+
if (path === "" || !path.startsWith("/"))
|
|
37
|
+
return defaultCookiePathFromRequestPath(requestPath);
|
|
38
|
+
return path;
|
|
39
|
+
}
|
|
40
|
+
function pathMatchesCookiePath(requestPath, cookiePath) {
|
|
41
|
+
const path = requestPathOnly(requestPath);
|
|
42
|
+
if (cookiePath === "/")
|
|
43
|
+
return true;
|
|
44
|
+
if (path === cookiePath)
|
|
45
|
+
return true;
|
|
46
|
+
if (!path.startsWith(cookiePath))
|
|
47
|
+
return false;
|
|
48
|
+
if (cookiePath.endsWith("/"))
|
|
49
|
+
return true;
|
|
50
|
+
return path.charAt(cookiePath.length) === "/";
|
|
51
|
+
}
|
|
52
|
+
function compareCookiesForHeader(a, b) {
|
|
53
|
+
const pathLenDiff = b.path.length - a.path.length;
|
|
54
|
+
if (pathLenDiff !== 0)
|
|
55
|
+
return pathLenDiff;
|
|
56
|
+
return a.createdAtSeq - b.createdAtSeq;
|
|
57
|
+
}
|
|
14
58
|
export class CookieJar {
|
|
15
59
|
cookies = new Map();
|
|
16
|
-
|
|
60
|
+
nextCreatedAtSeq = 0;
|
|
61
|
+
setCookie(setCookieHeader, requestPath = "/") {
|
|
17
62
|
const raw = setCookieHeader.trim();
|
|
18
63
|
if (raw === "")
|
|
19
64
|
return;
|
|
@@ -23,15 +68,16 @@ export class CookieJar {
|
|
|
23
68
|
const nv = parseCookieNameValue(parts[0] ?? "");
|
|
24
69
|
if (nv == null)
|
|
25
70
|
return;
|
|
26
|
-
let
|
|
71
|
+
let pathAttr;
|
|
27
72
|
let expiresAtMs;
|
|
73
|
+
let maxAgeSeen = false;
|
|
28
74
|
for (let i = 1; i < parts.length; i++) {
|
|
29
75
|
const p = parts[i];
|
|
30
76
|
const lower = p.toLowerCase();
|
|
31
77
|
if (lower.startsWith("path=")) {
|
|
32
78
|
const v = p.slice("path=".length).trim();
|
|
33
79
|
if (v !== "")
|
|
34
|
-
|
|
80
|
+
pathAttr = v;
|
|
35
81
|
continue;
|
|
36
82
|
}
|
|
37
83
|
if (lower.startsWith("max-age=")) {
|
|
@@ -39,14 +85,13 @@ export class CookieJar {
|
|
|
39
85
|
const n = Number.parseInt(v, 10);
|
|
40
86
|
if (!Number.isFinite(n))
|
|
41
87
|
continue;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
expiresAtMs = nowMs() + n * 1000;
|
|
88
|
+
maxAgeSeen = true;
|
|
89
|
+
expiresAtMs = n <= 0 ? 0 : nowMs() + n * 1000;
|
|
47
90
|
continue;
|
|
48
91
|
}
|
|
49
92
|
if (lower.startsWith("expires=")) {
|
|
93
|
+
if (maxAgeSeen)
|
|
94
|
+
continue;
|
|
50
95
|
const v = p.slice("expires=".length).trim();
|
|
51
96
|
const t = Date.parse(v);
|
|
52
97
|
if (!Number.isFinite(t))
|
|
@@ -55,34 +100,37 @@ export class CookieJar {
|
|
|
55
100
|
continue;
|
|
56
101
|
}
|
|
57
102
|
}
|
|
58
|
-
|
|
103
|
+
const path = normalizeCookiePath(pathAttr, requestPath);
|
|
104
|
+
const key = cookieStorageKey(nv.name, path);
|
|
59
105
|
if (expiresAtMs != null && expiresAtMs <= nowMs()) {
|
|
60
|
-
this.cookies.delete(
|
|
106
|
+
this.cookies.delete(key);
|
|
61
107
|
return;
|
|
62
108
|
}
|
|
109
|
+
const createdAtSeq = this.cookies.get(key)?.createdAtSeq ?? this.nextCreatedAtSeq++;
|
|
63
110
|
if (expiresAtMs == null) {
|
|
64
|
-
this.cookies.set(
|
|
111
|
+
this.cookies.set(key, { name: nv.name, value: nv.value, path, createdAtSeq });
|
|
65
112
|
}
|
|
66
113
|
else {
|
|
67
|
-
this.cookies.set(
|
|
114
|
+
this.cookies.set(key, { name: nv.name, value: nv.value, path, expiresAtMs, createdAtSeq });
|
|
68
115
|
}
|
|
69
116
|
}
|
|
70
|
-
updateFromSetCookieHeaders(headers) {
|
|
117
|
+
updateFromSetCookieHeaders(headers, requestPath = "/") {
|
|
71
118
|
for (const h of headers)
|
|
72
|
-
this.setCookie(h);
|
|
119
|
+
this.setCookie(h, requestPath);
|
|
73
120
|
}
|
|
74
121
|
getCookieHeader(path) {
|
|
75
122
|
const now = nowMs();
|
|
76
|
-
const
|
|
77
|
-
for (const c of this.cookies.
|
|
123
|
+
const matched = [];
|
|
124
|
+
for (const [key, c] of this.cookies.entries()) {
|
|
78
125
|
if (c.expiresAtMs != null && c.expiresAtMs <= now) {
|
|
79
|
-
this.cookies.delete(
|
|
126
|
+
this.cookies.delete(key);
|
|
80
127
|
continue;
|
|
81
128
|
}
|
|
82
|
-
if (
|
|
129
|
+
if (!pathMatchesCookiePath(path, c.path))
|
|
83
130
|
continue;
|
|
84
|
-
|
|
131
|
+
matched.push(c);
|
|
85
132
|
}
|
|
86
|
-
|
|
133
|
+
matched.sort(compareCookiesForHeader);
|
|
134
|
+
return matched.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
87
135
|
}
|
|
88
136
|
}
|
package/dist/proxy/runtime.js
CHANGED
|
@@ -102,7 +102,7 @@ export function createProxyRuntime(opts) {
|
|
|
102
102
|
ctl?.postMessage({ type: "flowersec-proxy:register-runtime" });
|
|
103
103
|
}
|
|
104
104
|
catch {
|
|
105
|
-
// Best-effort:
|
|
105
|
+
// Best-effort: controllerchange will retry once the active Service Worker is ready.
|
|
106
106
|
}
|
|
107
107
|
};
|
|
108
108
|
const onMessage = (ev) => {
|
|
@@ -168,7 +168,7 @@ export function createProxyRuntime(opts) {
|
|
|
168
168
|
const status = Math.max(0, Math.floor(respMeta.status ?? 502));
|
|
169
169
|
const rawHeaders = Array.isArray(respMeta.headers) ? respMeta.headers : [];
|
|
170
170
|
const { passthrough, setCookie } = filterResponseHeaders(rawHeaders, { extraAllowed: extraResponseHeaders });
|
|
171
|
-
cookieJar.updateFromSetCookieHeaders(setCookie);
|
|
171
|
+
cookieJar.updateFromSetCookieHeaders(setCookie, path);
|
|
172
172
|
port.postMessage({ type: "flowersec-proxy:response_meta", status, headers: passthrough });
|
|
173
173
|
const chunks = await readChunkFrames(reader, maxChunkBytes, maxBodyBytes);
|
|
174
174
|
for await (const chunk of chunks) {
|
|
@@ -159,6 +159,7 @@ self.addEventListener("install", (event) => {
|
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
self.addEventListener("activate", (event) => {
|
|
162
|
+
runtimeClientId = null;
|
|
162
163
|
event.waitUntil(self.clients.claim());
|
|
163
164
|
});
|
|
164
165
|
|
|
@@ -207,16 +208,10 @@ async function getWindowClient(preferredClientId) {
|
|
|
207
208
|
return cs.length > 0 ? cs[0] : null;
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
if (runtimeClientId)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
const cs = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
|
|
216
|
-
if (cs.length > 0) {
|
|
217
|
-
runtimeClientId = cs[0].id;
|
|
218
|
-
return cs[0];
|
|
219
|
-
}
|
|
211
|
+
if (!runtimeClientId) return null;
|
|
212
|
+
const c = await self.clients.get(runtimeClientId);
|
|
213
|
+
if (c) return c;
|
|
214
|
+
runtimeClientId = null;
|
|
220
215
|
return null;
|
|
221
216
|
}
|
|
222
217
|
|