@voltro/runtime 0.86.0 → 0.87.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/CHANGELOG.md +5321 -0
- package/README.md +43 -0
- package/THIRD-PARTY-NOTICES.md +860 -221
- package/dist/analyticsSink.js +8 -14
- package/dist/apiKeys.js +82 -78
- package/dist/apiRoleBucketSampler.js +64 -0
- package/dist/apiRoleWorkloadHistory.js +56 -0
- package/dist/apiRoleWorkloadMetrics.js +78 -0
- package/dist/apiRoleWorkloadStorage.js +95 -0
- package/dist/approvals.js +67 -74
- package/dist/authMiddlewareLayer.js +7 -6
- package/dist/bindEvent.js +48 -28
- package/dist/bootListener.js +106 -39
- package/dist/bootStoreCodec.js +38 -37
- package/dist/circuitBreaker.js +1 -72
- package/dist/composeAnalytics.js +31 -10
- package/dist/computedQueryCache.js +231 -0
- package/dist/connectionsFacade.js +32 -30
- package/dist/coordinatedSchedule.js +84 -78
- package/dist/dataStore.js +154 -121
- package/dist/declaredTablesGuard.js +2 -1
- package/dist/dispatcher.js +403 -328
- package/dist/effectStore.js +2 -2
- package/dist/engineClients.js +24 -0
- package/dist/externalizedTasks.js +19 -6
- package/dist/fieldCipher.js +109 -40
- package/dist/fieldKeyRegistry.js +295 -0
- package/dist/fieldKeyRekey.js +286 -0
- package/dist/fieldKeyScan.js +136 -0
- package/dist/fieldKeyring.js +77 -0
- package/dist/hot.d.ts +56 -0
- package/dist/hot.js +3 -0
- package/dist/hotDataStore.js +148 -0
- package/dist/hotDataStoreSnapshot.js +98 -0
- package/dist/hotDataStoreTables.js +18 -0
- package/dist/http-range.d.ts +53 -0
- package/dist/http-range.js +52 -0
- package/dist/httpClientSsrf.js +12 -37
- package/dist/httpClientTracing.js +12 -9
- package/dist/httpCompression.js +17 -17
- package/dist/httpInputStream.js +82 -0
- package/dist/httpRouteMounts.js +1 -0
- package/dist/idempotencyStore.js +92 -52
- package/dist/index.d.ts +1164 -451
- package/dist/index.js +143 -132
- package/dist/kvFacade.js +23 -17
- package/dist/memoryQueryJoins.js +16 -16
- package/dist/memorySetOperations.js +43 -0
- package/dist/metrics.js +38 -24
- package/dist/outbox.js +1 -1
- package/dist/readRecorder.js +35 -34
- package/dist/readScopePredicate.js +12 -0
- package/dist/replicatedDataStore.js +22 -4
- package/dist/rowFilter.js +56 -39
- package/dist/rpcConnections.js +8 -8
- package/dist/rpcServer.js +522 -449
- package/dist/rpcWorkloadAdmission.js +50 -0
- package/dist/rywPositionStore.js +82 -47
- package/dist/scheduler.js +6 -1
- package/dist/servePipeline.js +215 -146
- package/dist/setReadScopes.js +28 -0
- package/dist/snapshotValueCodec.js +46 -0
- package/dist/sourceResolution.js +34 -24
- package/dist/storeMiddleware.js +211 -209
- package/dist/subscriber.js +1 -0
- package/dist/tenantScope.js +8 -20
- package/dist/traceparent.js +8 -19
- package/dist/webSocketGatewayRuntime.js +111 -71
- package/dist/workflowRuntime.js +9 -9
- package/package.json +19 -7
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { decodeHotSnapshot as e, encodeHotSnapshot as t } from "./hotDataStoreSnapshot.js";
|
|
2
|
+
import { hotKvTable as n, hotZTable as r } from "./hotDataStoreTables.js";
|
|
3
|
+
import { Effect as i, Stream as a } from "effect";
|
|
4
|
+
import { and as o, eq as s, generateId as c, gt as l, isNull as u, isSqliteFamily as d, or as f, queryFor as p, startsWith as m } from "@voltro/database";
|
|
5
|
+
import { createHash as h } from "node:crypto";
|
|
6
|
+
import { HotError as g, applyHotOperations as _, compareHotMembers as v, hotStoreFromRunner as y, validateHotOp as b } from "@voltro/kv/hot/contract";
|
|
7
|
+
//#region src/hotDataStore.ts
|
|
8
|
+
var x = n.tableName, S = r.tableName, C = "lock", w = (e) => h("sha256").update(e).digest("hex"), T = (h, T) => {
|
|
9
|
+
let { namespace: E } = T;
|
|
10
|
+
if (!E || !E.isWellFormed() || /[{}]/.test(E)) throw new g({
|
|
11
|
+
operation: "connect",
|
|
12
|
+
key: "",
|
|
13
|
+
reason: "invalid"
|
|
14
|
+
});
|
|
15
|
+
let D = T.now ?? Date.now, O = (e) => w(JSON.stringify([E, e])), k, A = () => k ??= h.transactional(async (e) => {
|
|
16
|
+
let t = {
|
|
17
|
+
id: c({
|
|
18
|
+
kind: "typeid",
|
|
19
|
+
prefix: "hot"
|
|
20
|
+
}, x),
|
|
21
|
+
keyHash: C,
|
|
22
|
+
namespace: null,
|
|
23
|
+
key: null,
|
|
24
|
+
payload: null,
|
|
25
|
+
expiresAtMs: null
|
|
26
|
+
};
|
|
27
|
+
T.dialect === "mssql" ? await e.upsert(x, t, {
|
|
28
|
+
conflictColumns: ["keyHash"],
|
|
29
|
+
update: []
|
|
30
|
+
}) : await e.insertIgnore(x, t, { conflictColumns: ["keyHash"] });
|
|
31
|
+
}).catch((e) => {
|
|
32
|
+
throw k = void 0, e;
|
|
33
|
+
}), j = async (t, i) => {
|
|
34
|
+
let a = O(i), o = (await t.query(p(n).where(s("keyHash", a)).descriptor))[0];
|
|
35
|
+
if (o === void 0) return;
|
|
36
|
+
if (o.namespace !== E || o.key !== i || typeof o.payload != "string") throw new g({
|
|
37
|
+
operation: "read",
|
|
38
|
+
key: i,
|
|
39
|
+
reason: "unavailable"
|
|
40
|
+
});
|
|
41
|
+
let c = o.expiresAtMs;
|
|
42
|
+
if (c !== null && (typeof c != "number" || !Number.isFinite(c))) throw new g({
|
|
43
|
+
operation: "read",
|
|
44
|
+
key: i,
|
|
45
|
+
reason: "unavailable"
|
|
46
|
+
});
|
|
47
|
+
let l = await t.query(p(r).where(s("keyHash", a)).descriptor), u = [];
|
|
48
|
+
for (let e of l) {
|
|
49
|
+
let t = e.member, n = e.score;
|
|
50
|
+
if (typeof t != "string" || typeof n != "number" || !Number.isFinite(n)) throw new g({
|
|
51
|
+
operation: "read",
|
|
52
|
+
key: i,
|
|
53
|
+
reason: "unavailable"
|
|
54
|
+
});
|
|
55
|
+
u.push([t, n]);
|
|
56
|
+
}
|
|
57
|
+
return e(o.payload, c === null ? Infinity : c, u);
|
|
58
|
+
}, M = async (e, n, r) => {
|
|
59
|
+
let i = O(n);
|
|
60
|
+
if (await e.deleteMany(S, { where: s("keyHash", i) }), r === void 0) await e.deleteMany(x, { where: s("keyHash", i) });
|
|
61
|
+
else if (await e.upsert(x, {
|
|
62
|
+
id: c({
|
|
63
|
+
kind: "typeid",
|
|
64
|
+
prefix: "hot"
|
|
65
|
+
}, x),
|
|
66
|
+
keyHash: i,
|
|
67
|
+
namespace: E,
|
|
68
|
+
key: n,
|
|
69
|
+
payload: t(r),
|
|
70
|
+
expiresAtMs: r.expiresAt === Infinity ? null : r.expiresAt
|
|
71
|
+
}, {
|
|
72
|
+
conflictColumns: ["keyHash"],
|
|
73
|
+
update: [
|
|
74
|
+
"namespace",
|
|
75
|
+
"key",
|
|
76
|
+
"payload",
|
|
77
|
+
"expiresAtMs"
|
|
78
|
+
]
|
|
79
|
+
}), r.kind === "z") for (let [t, n] of r.values) await e.insert(S, {
|
|
80
|
+
id: c({
|
|
81
|
+
kind: "typeid",
|
|
82
|
+
prefix: "hotz"
|
|
83
|
+
}, S),
|
|
84
|
+
keyHash: i,
|
|
85
|
+
memberHash: w(t),
|
|
86
|
+
member: t,
|
|
87
|
+
score: n
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
return y((e) => i.try({
|
|
91
|
+
try: () => {
|
|
92
|
+
e.forEach(b);
|
|
93
|
+
},
|
|
94
|
+
catch: (e) => e instanceof g ? e : new g({
|
|
95
|
+
operation: "multi",
|
|
96
|
+
key: "",
|
|
97
|
+
reason: "invalid"
|
|
98
|
+
})
|
|
99
|
+
}).pipe(i.flatMap(() => i.tryPromise({
|
|
100
|
+
try: async () => e.length === 0 ? [] : (await A(), h.transactional(async (t) => {
|
|
101
|
+
if (d(T.dialect) && await t.updateMany(x, { payload: null }, { where: s("keyHash", C) }), (await t.query(p(n).where(s("keyHash", C)).forUpdate().descriptor)).length !== 1) throw new g({
|
|
102
|
+
operation: "multi",
|
|
103
|
+
key: "",
|
|
104
|
+
reason: "unavailable"
|
|
105
|
+
});
|
|
106
|
+
let r = D();
|
|
107
|
+
if (!Number.isFinite(r)) throw new g({
|
|
108
|
+
operation: "multi",
|
|
109
|
+
key: "",
|
|
110
|
+
reason: "invalid"
|
|
111
|
+
});
|
|
112
|
+
let i = /* @__PURE__ */ new Map(), a = [...new Set(e.map((e) => e.key))];
|
|
113
|
+
for (let e of a) {
|
|
114
|
+
let n = await j(t, e);
|
|
115
|
+
n !== void 0 && i.set(e, n);
|
|
116
|
+
}
|
|
117
|
+
let o = new Map(i), c = _(i, e, r);
|
|
118
|
+
for (let e of a) i.get(e) !== o.get(e) && await M(t, e, i.get(e));
|
|
119
|
+
return c;
|
|
120
|
+
})),
|
|
121
|
+
catch: (e) => e instanceof g ? e : new g({
|
|
122
|
+
operation: "multi",
|
|
123
|
+
key: "",
|
|
124
|
+
reason: "unavailable"
|
|
125
|
+
})
|
|
126
|
+
}))), (e) => a.unwrap(i.tryPromise({
|
|
127
|
+
try: async () => {
|
|
128
|
+
if (!e.isWellFormed()) throw new g({
|
|
129
|
+
operation: "scanPrefix",
|
|
130
|
+
key: e,
|
|
131
|
+
reason: "invalid"
|
|
132
|
+
});
|
|
133
|
+
let t = D(), r = (await h.query(p(n).where(o(s("namespace", E), m("key", e), f(u("expiresAtMs"), l("expiresAtMs", t)))).descriptor)).flatMap((t) => t.namespace === E && typeof t.key == "string" && t.key.startsWith(e) && typeof t.payload == "string" ? [t.key] : []);
|
|
134
|
+
return a.fromIterable(r.sort(v));
|
|
135
|
+
},
|
|
136
|
+
catch: (t) => t instanceof g ? t : new g({
|
|
137
|
+
operation: "scanPrefix",
|
|
138
|
+
key: e,
|
|
139
|
+
reason: "unavailable"
|
|
140
|
+
})
|
|
141
|
+
})), {
|
|
142
|
+
atomicMulti: !0,
|
|
143
|
+
crossNode: !0,
|
|
144
|
+
persistent: !0
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
//#endregion
|
|
148
|
+
export { T as dataStoreHotStore };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Schema as e } from "effect";
|
|
2
|
+
//#region src/hotDataStoreSnapshot.ts
|
|
3
|
+
var t = e.Number.pipe(e.check(e.makeFilter(Number.isFinite))), n = e.Union([
|
|
4
|
+
e.Struct({
|
|
5
|
+
kind: e.Literal("fixed"),
|
|
6
|
+
windowStart: t,
|
|
7
|
+
count: t
|
|
8
|
+
}),
|
|
9
|
+
e.Struct({
|
|
10
|
+
kind: e.Literal("sliding"),
|
|
11
|
+
currStart: t,
|
|
12
|
+
currCount: t,
|
|
13
|
+
prevCount: t
|
|
14
|
+
}),
|
|
15
|
+
e.Struct({
|
|
16
|
+
kind: e.Literal("token"),
|
|
17
|
+
tokens: t,
|
|
18
|
+
lastRefill: t
|
|
19
|
+
})
|
|
20
|
+
]), r = e.Union([
|
|
21
|
+
e.Struct({ kind: e.Literal("z") }),
|
|
22
|
+
e.Struct({
|
|
23
|
+
kind: e.Literal("h"),
|
|
24
|
+
values: e.Array(e.Tuple([e.String, e.String]))
|
|
25
|
+
}),
|
|
26
|
+
e.Struct({
|
|
27
|
+
kind: e.Literal("s"),
|
|
28
|
+
values: e.Array(e.String)
|
|
29
|
+
}),
|
|
30
|
+
e.Struct({
|
|
31
|
+
kind: e.Literal("counter"),
|
|
32
|
+
value: t
|
|
33
|
+
}),
|
|
34
|
+
e.Struct({
|
|
35
|
+
kind: e.Literal("lease"),
|
|
36
|
+
value: e.String
|
|
37
|
+
}),
|
|
38
|
+
e.Struct({
|
|
39
|
+
kind: e.Literal("bucket"),
|
|
40
|
+
tokens: t,
|
|
41
|
+
updatedAt: t
|
|
42
|
+
}),
|
|
43
|
+
e.Struct({
|
|
44
|
+
kind: e.Literal("rateLimit"),
|
|
45
|
+
state: n
|
|
46
|
+
})
|
|
47
|
+
]), i = e.decodeUnknownSync(r), a = (e) => {
|
|
48
|
+
switch (e.kind) {
|
|
49
|
+
case "z": return JSON.stringify({ kind: "z" });
|
|
50
|
+
case "h":
|
|
51
|
+
case "s": return JSON.stringify({
|
|
52
|
+
kind: e.kind,
|
|
53
|
+
values: [...e.values]
|
|
54
|
+
});
|
|
55
|
+
case "counter": return JSON.stringify({
|
|
56
|
+
kind: e.kind,
|
|
57
|
+
value: e.value
|
|
58
|
+
});
|
|
59
|
+
case "lease": return JSON.stringify({
|
|
60
|
+
kind: e.kind,
|
|
61
|
+
value: e.value
|
|
62
|
+
});
|
|
63
|
+
case "bucket": return JSON.stringify({
|
|
64
|
+
kind: e.kind,
|
|
65
|
+
tokens: e.tokens,
|
|
66
|
+
updatedAt: e.updatedAt
|
|
67
|
+
});
|
|
68
|
+
case "rateLimit": return JSON.stringify({
|
|
69
|
+
kind: e.kind,
|
|
70
|
+
state: e.state
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}, o = (e, t, n) => {
|
|
74
|
+
let r = i(JSON.parse(e));
|
|
75
|
+
switch (r.kind) {
|
|
76
|
+
case "z": return {
|
|
77
|
+
kind: "z",
|
|
78
|
+
values: new Map(n),
|
|
79
|
+
expiresAt: t
|
|
80
|
+
};
|
|
81
|
+
case "h": return {
|
|
82
|
+
kind: "h",
|
|
83
|
+
values: new Map(r.values),
|
|
84
|
+
expiresAt: t
|
|
85
|
+
};
|
|
86
|
+
case "s": return {
|
|
87
|
+
kind: "s",
|
|
88
|
+
values: new Set(r.values),
|
|
89
|
+
expiresAt: t
|
|
90
|
+
};
|
|
91
|
+
default: return {
|
|
92
|
+
...r,
|
|
93
|
+
expiresAt: t
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
//#endregion
|
|
98
|
+
export { o as decodeHotSnapshot, a as encodeHotSnapshot };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { id as e, real as t, table as n, text as r } from "@voltro/database";
|
|
2
|
+
//#region src/hotDataStoreTables.ts
|
|
3
|
+
var i = n("_voltro_hot_kv", {
|
|
4
|
+
id: e({ prefix: "hot" }),
|
|
5
|
+
keyHash: r().maxLength(64),
|
|
6
|
+
namespace: r().nullable(),
|
|
7
|
+
key: r().nullable(),
|
|
8
|
+
payload: r().nullable(),
|
|
9
|
+
expiresAtMs: t().nullable()
|
|
10
|
+
}).unique("hotKeyHash", ["keyHash"]).index(["expiresAtMs"]).nonReactive(), a = n("_voltro_hot_z", {
|
|
11
|
+
id: e({ prefix: "hotz" }),
|
|
12
|
+
keyHash: r().maxLength(64),
|
|
13
|
+
memberHash: r().maxLength(64),
|
|
14
|
+
member: r(),
|
|
15
|
+
score: t()
|
|
16
|
+
}).unique("hotMemberHash", ["keyHash", "memberHash"]).index(["keyHash", "score"]).nonReactive(), o = [i, a];
|
|
17
|
+
//#endregion
|
|
18
|
+
export { o as hotDataStoreTables, i as hotKvTable, a as hotZTable };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The outcome of a `Range` header against a representation of `size` bytes. */
|
|
2
|
+
export declare type ByteRangeRequest =
|
|
3
|
+
/** Absent, malformed, another unit, or several ranges: serve the whole thing. */
|
|
4
|
+
{
|
|
5
|
+
readonly _tag: 'ignored';
|
|
6
|
+
}
|
|
7
|
+
/** Syntactically valid, but no byte of it exists: 416 `bytes *\/size`. */
|
|
8
|
+
| {
|
|
9
|
+
readonly _tag: 'unsatisfiable';
|
|
10
|
+
}
|
|
11
|
+
/** Serve bytes `start..end`, both inclusive: 206 `bytes start-end/size`. */
|
|
12
|
+
| {
|
|
13
|
+
readonly _tag: 'satisfiable';
|
|
14
|
+
readonly start: number;
|
|
15
|
+
readonly end: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RFC 9110 §13.1.5: may a `Range` be honoured given this `If-Range`?
|
|
20
|
+
*
|
|
21
|
+
* `true` when there is no `If-Range`, or when it names the current
|
|
22
|
+
* representation by a STRONG validator: an identical strong entity tag, or
|
|
23
|
+
* the exact `Last-Modified` second — and only once that second is over
|
|
24
|
+
* (`nowMs`), because a file written twice within one second would otherwise
|
|
25
|
+
* keep a date that no longer names its bytes. A weak tag, an unparsable
|
|
26
|
+
* value, or a validator the representation does not have never matches: the
|
|
27
|
+
* client then gets the whole representation, never bytes of a different one.
|
|
28
|
+
*/
|
|
29
|
+
export declare const ifRangeAllowsRange: (ifRange: string | undefined, current: RangeValidators, nowMs?: number) => boolean;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a `Range` header against a representation of `size` bytes
|
|
33
|
+
* (a non-negative safe integer, normally from `stat`).
|
|
34
|
+
*/
|
|
35
|
+
export declare const parseByteRange: (header: string | undefined, size: number) => ByteRangeRequest;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* An HTTP-date (RFC 9110 §5.6.7) in epoch milliseconds, `undefined` when the
|
|
39
|
+
* value is none. All three historical forms are accepted. asctime carries no
|
|
40
|
+
* zone and HTTP dates are always GMT, so it is read as GMT — `Date.parse`
|
|
41
|
+
* alone would read it in the server's local time zone.
|
|
42
|
+
*/
|
|
43
|
+
export declare const parseHttpDate: (value: string) => number | undefined;
|
|
44
|
+
|
|
45
|
+
/** The current validators of the representation a range would be cut from. */
|
|
46
|
+
export declare interface RangeValidators {
|
|
47
|
+
/** The representation's entity tag as sent in `ETag`, quotes included. */
|
|
48
|
+
readonly etag?: string | undefined;
|
|
49
|
+
/** Last modification time in epoch milliseconds (as sent in `Last-Modified`). */
|
|
50
|
+
readonly lastModifiedMs?: number | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/httpRange.ts
|
|
2
|
+
var e = { _tag: "ignored" }, t = { _tag: "unsatisfiable" }, n = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+)=(.*)$/s, r = /^(\d*)-(\d*)$/, i = (e) => {
|
|
3
|
+
let t = e.replace(/^0+(?=\d)/, "");
|
|
4
|
+
if (t.length > 16) return;
|
|
5
|
+
let n = Number(t);
|
|
6
|
+
return Number.isSafeInteger(n) ? n : void 0;
|
|
7
|
+
}, a = (e, t) => {
|
|
8
|
+
let n = e.replace(/^0+(?=\d)/, ""), r = t.replace(/^0+(?=\d)/, "");
|
|
9
|
+
return n.length === r.length ? n < r : n.length < r.length;
|
|
10
|
+
}, o = (o, s) => {
|
|
11
|
+
if (o === void 0) return e;
|
|
12
|
+
let c = n.exec(o.trim());
|
|
13
|
+
if (c === null || c[1].toLowerCase() !== "bytes") return e;
|
|
14
|
+
let l = c[2].split(",").map((e) => e.trim()).filter((e) => e !== "");
|
|
15
|
+
if (l.length !== 1) return e;
|
|
16
|
+
let u = r.exec(l[0]);
|
|
17
|
+
if (u === null) return e;
|
|
18
|
+
let [, d = "", f = ""] = u;
|
|
19
|
+
if (d === "") {
|
|
20
|
+
if (f === "") return e;
|
|
21
|
+
let n = i(f);
|
|
22
|
+
return n === 0 || s === 0 ? t : {
|
|
23
|
+
_tag: "satisfiable",
|
|
24
|
+
start: n === void 0 ? 0 : Math.max(0, s - n),
|
|
25
|
+
end: s - 1
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (f !== "" && a(f, d)) return e;
|
|
29
|
+
let p = i(d);
|
|
30
|
+
if (p === void 0 || p >= s) return t;
|
|
31
|
+
let m = f === "" ? void 0 : i(f);
|
|
32
|
+
return {
|
|
33
|
+
_tag: "satisfiable",
|
|
34
|
+
start: p,
|
|
35
|
+
end: m === void 0 ? s - 1 : Math.min(m, s - 1)
|
|
36
|
+
};
|
|
37
|
+
}, s = /^(W\/)?"[\x21\x23-\x7e\x80-\xff]*"$/, c = /^[A-Za-z]{3}/, l = (e) => {
|
|
38
|
+
let t = e.trim();
|
|
39
|
+
if (!c.test(t)) return;
|
|
40
|
+
let n = Date.parse(/(?:GMT|UTC)$/i.test(t) ? t : `${t} GMT`);
|
|
41
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
42
|
+
}, u = (e, t, n = Date.now()) => {
|
|
43
|
+
if (e === void 0) return !0;
|
|
44
|
+
let r = e.trim();
|
|
45
|
+
if (r.startsWith("\"") || r.startsWith("W/")) return !s.test(r) || r.startsWith("W/") ? !1 : t.etag !== void 0 && !t.etag.startsWith("W/") && t.etag === r;
|
|
46
|
+
let i = l(r);
|
|
47
|
+
if (i === void 0 || t.lastModifiedMs === void 0) return !1;
|
|
48
|
+
let a = Math.floor(t.lastModifiedMs / 1e3);
|
|
49
|
+
return a * 1e3 === i && Math.floor(n / 1e3) > a;
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { u as ifRangeAllowsRange, o as parseByteRange, l as parseHttpDate };
|
package/dist/httpClientSsrf.js
CHANGED
|
@@ -1,49 +1,24 @@
|
|
|
1
1
|
import { Effect as e, Layer as t } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { ssrfReason as n, ssrfReason as r } from "@voltro/protocol";
|
|
3
|
+
import { HttpClient as i, HttpClientError as a } from "effect/http";
|
|
3
4
|
//#region src/httpClientSsrf.ts
|
|
4
|
-
var
|
|
5
|
+
var o = class extends Error {
|
|
5
6
|
url;
|
|
6
7
|
reason;
|
|
7
8
|
_tag = "SsrfBlockedError";
|
|
8
9
|
constructor(e, t) {
|
|
9
10
|
super(`SSRF: blocked outbound request to ${e} — ${t}. If this target is intentional, add its host to \`http.allowHosts\` in app.config.ts.`), this.url = e, this.reason = t, this.name = "SsrfBlockedError";
|
|
10
11
|
}
|
|
11
|
-
},
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
n = new URL(e);
|
|
20
|
-
} catch {
|
|
21
|
-
return "malformed url";
|
|
22
|
-
}
|
|
23
|
-
if (n.protocol !== "http:" && n.protocol !== "https:") return `blocked scheme "${n.protocol}"`;
|
|
24
|
-
let r = n.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
25
|
-
if (s(r, n.port, t.allowHosts ?? [])) return null;
|
|
26
|
-
if (r === "localhost" || r.endsWith(".localhost") || r.endsWith(".internal") || r.endsWith(".local")) return `blocked host "${r}"`;
|
|
27
|
-
let i = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(r);
|
|
28
|
-
if (i) {
|
|
29
|
-
let e = Number(i[1]), t = Number(i[2]);
|
|
30
|
-
return e > 255 || t > 255 || Number(i[3]) > 255 || Number(i[4]) > 255 ? "malformed ipv4 literal" : a(e, t) ? `blocked private/reserved address ${r}` : null;
|
|
31
|
-
}
|
|
32
|
-
if (r.includes(":")) {
|
|
33
|
-
let e = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(r);
|
|
34
|
-
return e ? c(`${n.protocol}//${e[1]}`, t) : o(r) ? `blocked private/reserved address ${r}` : null;
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}, l = (a = {}) => t.effect(n.HttpClient, e.map(n.HttpClient, (t) => {
|
|
38
|
-
let o = n.transform(t, (t, n) => e.suspend(() => {
|
|
39
|
-
let o = c(n.url, a);
|
|
40
|
-
return o === null ? t : e.fail(new r.HttpClientError({ reason: new r.TransportError({
|
|
41
|
-
request: n,
|
|
42
|
-
cause: new i(n.url, o),
|
|
43
|
-
description: `SSRF policy: ${o}`
|
|
12
|
+
}, s = (r = {}) => t.effect(i.HttpClient, e.map(i.HttpClient, (t) => {
|
|
13
|
+
let s = i.transform(t, (t, i) => e.suspend(() => {
|
|
14
|
+
let s = n(i.url, r);
|
|
15
|
+
return s === null ? t : e.fail(new a.HttpClientError({ reason: new a.TransportError({
|
|
16
|
+
request: i,
|
|
17
|
+
cause: new o(i.url, s),
|
|
18
|
+
description: `SSRF policy: ${s}`
|
|
44
19
|
}) }));
|
|
45
20
|
}));
|
|
46
|
-
return
|
|
21
|
+
return i.followRedirects(s, r.maxRedirects ?? 5);
|
|
47
22
|
}));
|
|
48
23
|
//#endregion
|
|
49
|
-
export {
|
|
24
|
+
export { o as SsrfBlockedError, s as ssrfGuardHttpClientLayer, r as ssrfReason };
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { noteCacheVolatile as e } from "./computedQueryCache.js";
|
|
2
|
+
import { formatTraceparent as t } from "./traceparent.js";
|
|
3
|
+
import { Effect as n, Layer as r } from "effect";
|
|
4
|
+
import { HttpClient as i, HttpClientRequest as a } from "effect/http";
|
|
4
5
|
//#region src/httpClientTracing.ts
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
var o = r.effect(i.HttpClient, n.map(i.HttpClient, (r) => r.pipe(i.mapRequestEffect((r) => n.sync(() => {
|
|
7
|
+
e("HttpClient");
|
|
8
|
+
}).pipe(n.andThen(n.currentSpan), n.map((e) => r.headers.traceparent === void 0 ? a.setHeader(r, "traceparent", t({
|
|
9
|
+
traceId: e.traceId,
|
|
10
|
+
spanId: e.spanId,
|
|
11
|
+
sampled: e.sampled
|
|
12
|
+
})) : r), n.catch(() => n.succeed(r)))))));
|
|
10
13
|
//#endregion
|
|
11
|
-
export {
|
|
14
|
+
export { o as tracingHttpClientLayer };
|
package/dist/httpCompression.js
CHANGED
|
@@ -36,32 +36,32 @@ var r = 1024, i = [
|
|
|
36
36
|
[t.BROTLI_PARAM_QUALITY]: 4,
|
|
37
37
|
[t.BROTLI_PARAM_SIZE_HINT]: r.length
|
|
38
38
|
} })) : new Uint8Array(n(r, { level: 6 })), c = (e) => {
|
|
39
|
-
let t = e.options?.enabled ?? !0, n = e.options?.minBytes ?? 1024
|
|
40
|
-
if (!t || !
|
|
39
|
+
let t = e.options?.enabled ?? !0, n = e.options?.minBytes ?? 1024;
|
|
40
|
+
if (!(!t || !a(e.contentType) || e.size < n)) return o(e.acceptEncoding);
|
|
41
|
+
}, l = (e) => {
|
|
42
|
+
let t = a(e.contentType) ? { vary: "Accept-Encoding" } : {}, n = c({
|
|
43
|
+
...e,
|
|
44
|
+
size: e.body.length
|
|
45
|
+
});
|
|
46
|
+
if (n === void 0) return {
|
|
41
47
|
bytes: e.body,
|
|
42
|
-
headers:
|
|
48
|
+
headers: t,
|
|
43
49
|
compressed: !1
|
|
44
50
|
};
|
|
45
|
-
let
|
|
46
|
-
|
|
51
|
+
let r = s(e.body, n);
|
|
52
|
+
return r.length >= e.body.length ? {
|
|
47
53
|
bytes: e.body,
|
|
48
|
-
headers:
|
|
49
|
-
compressed: !1
|
|
50
|
-
};
|
|
51
|
-
let l = s(e.body, c);
|
|
52
|
-
return l.length >= e.body.length ? {
|
|
53
|
-
bytes: e.body,
|
|
54
|
-
headers: i,
|
|
54
|
+
headers: t,
|
|
55
55
|
compressed: !1
|
|
56
56
|
} : {
|
|
57
|
-
bytes:
|
|
57
|
+
bytes: r,
|
|
58
58
|
headers: {
|
|
59
|
-
...
|
|
60
|
-
"content-encoding":
|
|
59
|
+
...t,
|
|
60
|
+
"content-encoding": n
|
|
61
61
|
},
|
|
62
62
|
compressed: !0
|
|
63
63
|
};
|
|
64
|
-
},
|
|
64
|
+
}, u = (e, t) => {
|
|
65
65
|
let n = {}, r = [], i = (e) => {
|
|
66
66
|
for (let t of String(e).split(",")) {
|
|
67
67
|
let e = t.trim();
|
|
@@ -73,4 +73,4 @@ var r = 1024, i = [
|
|
|
73
73
|
return r.length > 0 && (n.vary = r.join(", ")), n;
|
|
74
74
|
};
|
|
75
75
|
//#endregion
|
|
76
|
-
export { r as DEFAULT_COMPRESSION_MIN_BYTES, a as isCompressibleContentType,
|
|
76
|
+
export { r as DEFAULT_COMPRESSION_MIN_BYTES, a as isCompressibleContentType, l as maybeCompress, u as mergeCompressionHeaders, o as negotiateEncoding, c as negotiatedCompression };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//#region src/httpInputStream.ts
|
|
2
|
+
var e = async (e, t, n, r, i) => {
|
|
3
|
+
let a = new AbortController(), o, s = !1, c = e.readableEnded, l = e.destroyed && !c, u = !1, d = 0, f = !1, p = !1, m = () => {
|
|
4
|
+
o?.();
|
|
5
|
+
}, h = () => {
|
|
6
|
+
l = !0, a.abort(), m();
|
|
7
|
+
}, g = () => {
|
|
8
|
+
c = !0, m();
|
|
9
|
+
}, _ = () => {
|
|
10
|
+
e.complete || h();
|
|
11
|
+
};
|
|
12
|
+
e.once("end", g), e.once("error", h), e.once("aborted", h), e.once("close", _), i.addEventListener("abort", h, { once: !0 }), (i.aborted || l) && h();
|
|
13
|
+
let v = { [Symbol.asyncIterator]() {
|
|
14
|
+
if (f) throw Error("HTTP input can only be consumed once");
|
|
15
|
+
return f = !0, {
|
|
16
|
+
async next() {
|
|
17
|
+
if (p) throw Error("Concurrent HTTP input reads are not supported");
|
|
18
|
+
p = !0;
|
|
19
|
+
try {
|
|
20
|
+
for (;;) {
|
|
21
|
+
if (l || a.signal.aborted) throw Error("HTTP input aborted");
|
|
22
|
+
if (u) throw Error("HTTP input exceeds its byte limit");
|
|
23
|
+
if (s) return {
|
|
24
|
+
done: !0,
|
|
25
|
+
value: void 0
|
|
26
|
+
};
|
|
27
|
+
let n = e.read();
|
|
28
|
+
if (n !== null) {
|
|
29
|
+
if (d += n.byteLength, d > t) throw u = !0, Error("HTTP input exceeds its byte limit");
|
|
30
|
+
return {
|
|
31
|
+
done: !1,
|
|
32
|
+
value: n
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (c) return {
|
|
36
|
+
done: !0,
|
|
37
|
+
value: void 0
|
|
38
|
+
};
|
|
39
|
+
await new Promise((t) => {
|
|
40
|
+
let n = () => {
|
|
41
|
+
e.off("readable", n), o = void 0, t();
|
|
42
|
+
};
|
|
43
|
+
o = n, e.once("readable", n);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
p = !1;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async return() {
|
|
51
|
+
return s = !0, m(), {
|
|
52
|
+
done: !0,
|
|
53
|
+
value: void 0
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
} }, y = () => ({
|
|
58
|
+
status: 413,
|
|
59
|
+
body: "payload too large",
|
|
60
|
+
contentType: "text/plain"
|
|
61
|
+
}), b;
|
|
62
|
+
try {
|
|
63
|
+
let e = new Promise((e, t) => {
|
|
64
|
+
b = () => t(/* @__PURE__ */ Error("HTTP input aborted")), a.signal.addEventListener("abort", b, { once: !0 }), a.signal.aborted && b();
|
|
65
|
+
}), t = await Promise.race([e, Promise.resolve().then(() => {
|
|
66
|
+
if (a.signal.aborted) throw Error("HTTP input aborted");
|
|
67
|
+
return r({
|
|
68
|
+
...n,
|
|
69
|
+
body: v,
|
|
70
|
+
signal: a.signal
|
|
71
|
+
});
|
|
72
|
+
})]);
|
|
73
|
+
return u ? y() : t;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (u) return y();
|
|
76
|
+
throw e;
|
|
77
|
+
} finally {
|
|
78
|
+
s = !0, b !== void 0 && a.signal.removeEventListener("abort", b), a.abort(), m(), e.off("end", g), e.off("error", h), e.off("aborted", h), e.off("close", _), i.removeEventListener("abort", h), !e.destroyed && !e.readableEnded && e.resume();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { e as dispatchHttpInputStream };
|
package/dist/httpRouteMounts.js
CHANGED
|
@@ -14,6 +14,7 @@ var n = (e) => e.split("/").map((e) => e.startsWith(":") ? ":param" : e).join("/
|
|
|
14
14
|
}
|
|
15
15
|
let i = [];
|
|
16
16
|
t.forEach((e, t) => {
|
|
17
|
+
if (e.some((e) => e.handleStream !== void 0) && (e.length !== 1 || e[0].match !== "exact" || e[0].path.endsWith("/*"))) throw Error("A streamed HTTP input requires an exclusive exact route path");
|
|
17
18
|
i.push({
|
|
18
19
|
mountPath: t,
|
|
19
20
|
routes: e,
|