@getstrata/core 2.2.39 → 2.2.41
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 +12 -0
- package/dist/entries/audit/exportAuditLogs.js +133 -167
- package/dist/entries/auth/membershipMiddleware.js +1 -113
- package/dist/entries/auth/membershipScope.js +0 -36
- package/dist/entries/auth/membershipService.js +1 -43
- package/dist/entries/auth/oauth/oidcProvider.js +21 -260
- package/dist/entries/auth/oauth/providers.js +1 -238
- package/dist/entries/http/memoryThrottleMiddleware.js +3 -1
- package/dist/entries/http/response.js +4 -299
- package/dist/entries/http/scimThrottleMiddleware.js +3 -1
- package/dist/entries/http/throttleMiddleware.js +3 -1
- package/dist/entries/jobs/exportAuditLogsJob.js +133 -167
- package/dist/entries/security/safeFetch.js +1 -260
- package/dist/index.js +8 -6
- package/package.json +1 -1
|
@@ -1,265 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// ../../src/core/security/safeUrl.ts
|
|
3
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
4
|
-
import { BadRequestError } from "@getstrata/core/errors/http";
|
|
5
|
-
|
|
6
|
-
// ../../src/core/runtime/appEnv.ts
|
|
7
|
-
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
8
|
-
function normalizeEnvValue(value) {
|
|
9
|
-
return (value ?? "").trim().toLowerCase();
|
|
10
|
-
}
|
|
11
|
-
function isProductionEnv(env = process.env) {
|
|
12
|
-
const appEnv = normalizeEnvValue(env.APP_ENV);
|
|
13
|
-
const nodeEnv = normalizeEnvValue(env.NODE_ENV);
|
|
14
|
-
if (appEnv === "production" || nodeEnv === "production") {
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
if (appEnv === "") {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
return !NON_PRODUCTION_APP_ENVS.has(appEnv);
|
|
21
|
-
}
|
|
22
|
-
function envFlagEnabled(value) {
|
|
23
|
-
return value === "true";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ../../src/core/security/safeUrl.ts
|
|
27
|
-
var dnsLookup = dnsLookupImpl;
|
|
28
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
29
|
-
"localhost",
|
|
30
|
-
"127.0.0.1",
|
|
31
|
-
"0.0.0.0",
|
|
32
|
-
"::1",
|
|
33
|
-
"metadata.google.internal"
|
|
34
|
-
]);
|
|
35
|
-
function parseCanonicalDottedDecimal(hostname) {
|
|
36
|
-
const parts = hostname.split(".");
|
|
37
|
-
if (parts.length !== 4) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
const octets = [];
|
|
41
|
-
for (const part of parts) {
|
|
42
|
-
if (!/^(0|[1-9]\d{0,2})$/u.test(part)) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
const value = Number.parseInt(part, 10);
|
|
46
|
-
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
octets.push(value);
|
|
50
|
-
}
|
|
51
|
-
return octets;
|
|
52
|
-
}
|
|
53
|
-
function parseDottedIpv4(hostname) {
|
|
54
|
-
return parseCanonicalDottedDecimal(hostname);
|
|
55
|
-
}
|
|
56
|
-
function isBlockedIpv4Octets(octets) {
|
|
57
|
-
const [a = 0, b = 0] = octets;
|
|
58
|
-
if (a === 10 || a === 127 || a === 0) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
if (a === 169 && b === 254) {
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
if (a === 192 && b === 168) {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
if (a === 100 && b >= 64 && b <= 127) {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
if (a === 198 && (b === 18 || b === 19)) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
function expandIpv6(hostname) {
|
|
79
|
-
const trimmed = hostname.trim().toLowerCase();
|
|
80
|
-
const unbracketed = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
81
|
-
if (unbracketed.includes("%")) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
const mapped = unbracketed.match(/^::ffff:([0-9a-fx.]+)$/iu);
|
|
85
|
-
if (mapped?.[1]) {
|
|
86
|
-
const octets = parseDottedIpv4(mapped[1]);
|
|
87
|
-
if (!octets) {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
return [
|
|
91
|
-
0,
|
|
92
|
-
0,
|
|
93
|
-
0,
|
|
94
|
-
0,
|
|
95
|
-
0,
|
|
96
|
-
65535,
|
|
97
|
-
octets[0] << 8 | octets[1],
|
|
98
|
-
octets[2] << 8 | octets[3]
|
|
99
|
-
];
|
|
100
|
-
}
|
|
101
|
-
const halves = unbracketed.split("::");
|
|
102
|
-
if (halves.length > 2) {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
const parseGroup = (part) => {
|
|
106
|
-
if (!/^[0-9a-f]{1,4}$/u.test(part)) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
return Number.parseInt(part, 16);
|
|
110
|
-
};
|
|
111
|
-
if (halves.length === 1) {
|
|
112
|
-
const groups = unbracketed.split(":");
|
|
113
|
-
if (groups.length !== 8) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
const values = groups.map(parseGroup);
|
|
117
|
-
return values.every((value) => value !== null) ? values : null;
|
|
118
|
-
}
|
|
119
|
-
const head = halves[0] ? halves[0].split(":").filter(Boolean) : [];
|
|
120
|
-
const tail = halves[1] ? halves[1].split(":").filter(Boolean) : [];
|
|
121
|
-
if (head.length + tail.length > 8) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
const values = [];
|
|
125
|
-
for (const part of head) {
|
|
126
|
-
const value = parseGroup(part);
|
|
127
|
-
if (value === null) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
values.push(value);
|
|
131
|
-
}
|
|
132
|
-
while (values.length < 8 - tail.length) {
|
|
133
|
-
values.push(0);
|
|
134
|
-
}
|
|
135
|
-
for (const part of tail) {
|
|
136
|
-
const value = parseGroup(part);
|
|
137
|
-
if (value === null) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
values.push(value);
|
|
141
|
-
}
|
|
142
|
-
return values.length === 8 ? values : null;
|
|
143
|
-
}
|
|
144
|
-
function isBlockedIpv6(hostname) {
|
|
145
|
-
const groups = expandIpv6(hostname);
|
|
146
|
-
if (!groups) {
|
|
147
|
-
return hostname.includes(":");
|
|
148
|
-
}
|
|
149
|
-
const first = groups[0] ?? 0;
|
|
150
|
-
const second = groups[1] ?? 0;
|
|
151
|
-
if (groups.every((group) => group === 0)) {
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
if (groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1) {
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
if ((first & 65472) === 65152) {
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
if ((first & 65024) === 64512) {
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
if ((first & 65280) === 65280) {
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
if (first === 0 && second === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) {
|
|
167
|
-
const a = (groups[6] ?? 0) >> 8;
|
|
168
|
-
const b = (groups[6] ?? 0) & 255;
|
|
169
|
-
const c = (groups[7] ?? 0) >> 8;
|
|
170
|
-
const d = (groups[7] ?? 0) & 255;
|
|
171
|
-
return isBlockedIpv4Octets([a, b, c, d]);
|
|
172
|
-
}
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
function isBlockedHostname(hostname) {
|
|
176
|
-
const normalized = hostname.trim().toLowerCase();
|
|
177
|
-
if (normalized.length === 0) {
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
if (normalized.includes(":")) {
|
|
187
|
-
return isBlockedIpv6(normalized);
|
|
188
|
-
}
|
|
189
|
-
if (/^\d+$/u.test(normalized)) {
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
if (normalized.includes(".")) {
|
|
193
|
-
const canonical = parseCanonicalDottedDecimal(normalized);
|
|
194
|
-
if (canonical) {
|
|
195
|
-
return isBlockedIpv4Octets(canonical);
|
|
196
|
-
}
|
|
197
|
-
if (/^[0-9a-fx.]+$/iu.test(normalized)) {
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
function productionSafeOutboundOptions(options) {
|
|
204
|
-
if (!isProductionEnv()) {
|
|
205
|
-
return options;
|
|
206
|
-
}
|
|
207
|
-
return { ...options, allowPrivate: false, resolveDns: true };
|
|
208
|
-
}
|
|
209
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
210
|
-
const resolved = productionSafeOutboundOptions(options);
|
|
211
|
-
let parsed;
|
|
212
|
-
try {
|
|
213
|
-
parsed = new URL(rawUrl);
|
|
214
|
-
} catch {
|
|
215
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
216
|
-
}
|
|
217
|
-
if (parsed.protocol !== "https:" && !(resolved.allowHttp && parsed.protocol === "http:")) {
|
|
218
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
219
|
-
}
|
|
220
|
-
if (parsed.username || parsed.password) {
|
|
221
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
222
|
-
}
|
|
223
|
-
if (!resolved.allowPrivate && isBlockedHostname(parsed.hostname)) {
|
|
224
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
225
|
-
}
|
|
226
|
-
return parsed;
|
|
227
|
-
}
|
|
228
|
-
function isBlockedIpAddress(address) {
|
|
229
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
230
|
-
}
|
|
231
|
-
function isLiteralIpHostname(hostname) {
|
|
232
|
-
return parseCanonicalDottedDecimal(hostname) !== null || expandIpv6(hostname) !== null;
|
|
233
|
-
}
|
|
234
|
-
function pinUrlToAddress(url, address) {
|
|
235
|
-
const pinned = new URL(url.toString());
|
|
236
|
-
const ipv6 = expandIpv6(address.replace(/^\[|\]$/gu, ""));
|
|
237
|
-
pinned.hostname = ipv6 ? `[${address.replace(/^\[|\]$/gu, "")}]` : address;
|
|
238
|
-
return pinned;
|
|
239
|
-
}
|
|
240
|
-
async function resolveSafeOutboundTarget(rawUrl, options = {}) {
|
|
241
|
-
const resolvedOptions = productionSafeOutboundOptions(options);
|
|
242
|
-
const parsed = assertSafeOutboundUrl(rawUrl, resolvedOptions);
|
|
243
|
-
if (resolvedOptions.resolveDns === false || resolvedOptions.allowPrivate) {
|
|
244
|
-
return { url: parsed, addresses: [] };
|
|
245
|
-
}
|
|
246
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
247
|
-
if (isLiteralIpHostname(hostname)) {
|
|
248
|
-
return { url: parsed, addresses: [hostname] };
|
|
249
|
-
}
|
|
250
|
-
let results;
|
|
251
|
-
try {
|
|
252
|
-
results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
253
|
-
} catch {
|
|
254
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
255
|
-
}
|
|
256
|
-
if (results.length === 0 || results.some((result) => isBlockedIpAddress(result.address))) {
|
|
257
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
258
|
-
}
|
|
259
|
-
return { url: parsed, addresses: results.map((result) => result.address) };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
2
|
// ../../src/core/security/safeFetch.ts
|
|
3
|
+
import { pinUrlToAddress, resolveSafeOutboundTarget } from "@getstrata/core/security/safeUrl";
|
|
263
4
|
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
264
5
|
function untilAborted(work, signal) {
|
|
265
6
|
signal.throwIfAborted();
|
package/dist/index.js
CHANGED
|
@@ -12745,9 +12745,6 @@ function toPaginatedResourceCollection(items, meta, transformer) {
|
|
|
12745
12745
|
meta
|
|
12746
12746
|
};
|
|
12747
12747
|
}
|
|
12748
|
-
// ../../src/core/http/response.ts
|
|
12749
|
-
init_transactionControl();
|
|
12750
|
-
|
|
12751
12748
|
// ../../src/core/http/safeInternalPath.ts
|
|
12752
12749
|
var NESTED_REDIRECT_KEYS = new Set(["redirect", "next", "return", "returnTo", "return_to"]);
|
|
12753
12750
|
function looksLikeExternalTarget(value) {
|
|
@@ -12933,11 +12930,15 @@ async function renderWebErrorHtml(input) {
|
|
|
12933
12930
|
}
|
|
12934
12931
|
}
|
|
12935
12932
|
async function htmlErrorResponse(input) {
|
|
12936
|
-
|
|
12933
|
+
const response = htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
12934
|
+
response.headers.set("Cache-Control", "no-store");
|
|
12935
|
+
return response;
|
|
12937
12936
|
}
|
|
12938
12937
|
async function notFoundHtmlResponse(body) {
|
|
12939
12938
|
if (body !== undefined) {
|
|
12940
|
-
|
|
12939
|
+
const response = htmlResponse(body, { status: 404 });
|
|
12940
|
+
response.headers.set("Cache-Control", "no-store");
|
|
12941
|
+
return response;
|
|
12941
12942
|
}
|
|
12942
12943
|
return htmlErrorResponse({
|
|
12943
12944
|
status: 404,
|
|
@@ -13037,6 +13038,7 @@ async function webErrorResponse(error, request) {
|
|
|
13037
13038
|
}
|
|
13038
13039
|
|
|
13039
13040
|
// ../../src/core/http/response.ts
|
|
13041
|
+
init_transactionControl();
|
|
13040
13042
|
function jsonResponse2(data, init = {}) {
|
|
13041
13043
|
return Response.json(data, {
|
|
13042
13044
|
status: init.status ?? 200,
|
|
@@ -13055,7 +13057,7 @@ function errorResponse(error) {
|
|
|
13055
13057
|
return Response.json({
|
|
13056
13058
|
error: mappedError.message,
|
|
13057
13059
|
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
13058
|
-
}, { status: mappedError.status });
|
|
13060
|
+
}, { status: mappedError.status, headers: { "Cache-Control": "no-store" } });
|
|
13059
13061
|
}
|
|
13060
13062
|
function withErrorHandling(handler) {
|
|
13061
13063
|
return async (...args) => {
|