@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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.2.41
4
+
5
+ Make framework HTML/JSON error responses non-storeable, including configured/fallback renderers and custom-body 404 responses; retain success-response cache policy and add packed header regressions.
6
+
7
+ Fix packaged error wrappers ignoring the configured application error renderer; preserve API JSON precedence and add packed configured/default rendering regressions.
8
+
9
+ Fix packaged membership middleware and default service ignoring the configured application lookup. Use the canonical membership context while preserving explicit service injection and request isolation. Add mandatory packed external-app coverage for concurrent users, replacement configuration and propagated lookup failures.
10
+
11
+ ## 2.2.40
12
+
13
+ Fix shared DNS fixture state in packaged outbound consumers. safeFetch and GitHub OAuth now use the canonical safeUrl resolver while retaining DNS pinning, private-address validation and request deadlines. Add mandatory packed external-app regressions for resolver replacement and private-target refusal; document controlled-provider qualification limits.
14
+
3
15
  ## 2.2.39
4
16
 
5
17
  Fix published HTTP metrics registry identity: share root/subpath Prometheus state and preserve registry, class and bucket exports. Keep generated middleware/exporter observations consistent and extend installed Postgres/SQLite/MySQL HTTP regressions; no app bridge, schema change or collector-policy change.
@@ -995,10 +995,141 @@ function sdkClientClassName() {
995
995
  return fromName ? `${fromName}Client` : "AppClient";
996
996
  }
997
997
 
998
+ // ../../src/core/security/safeFetch.ts
999
+ import { pinUrlToAddress, resolveSafeOutboundTarget } from "@getstrata/core/security/safeUrl";
1000
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1001
+ function untilAborted(work, signal) {
1002
+ signal.throwIfAborted();
1003
+ return new Promise((resolve, reject) => {
1004
+ const onAbort = () => reject(signal.reason);
1005
+ signal.addEventListener("abort", onAbort, { once: true });
1006
+ work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
1007
+ });
1008
+ }
1009
+ function preserveResponseMetadata(response, original) {
1010
+ Object.defineProperties(response, {
1011
+ url: { value: original.url },
1012
+ redirected: { value: original.redirected },
1013
+ type: { value: original.type }
1014
+ });
1015
+ const clone = response.clone.bind(response);
1016
+ response.clone = () => preserveResponseMetadata(clone(), original);
1017
+ return response;
1018
+ }
1019
+ function withBodyDeadline(response, signal, release) {
1020
+ if (!response.body) {
1021
+ release();
1022
+ return response;
1023
+ }
1024
+ const reader = response.body.getReader();
1025
+ let stopped = false;
1026
+ let bodyController;
1027
+ const stop = (reason, errorBody) => {
1028
+ if (stopped)
1029
+ return;
1030
+ stopped = true;
1031
+ release();
1032
+ signal.removeEventListener("abort", onAbort);
1033
+ if (errorBody)
1034
+ bodyController.error(reason);
1035
+ reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
1036
+ };
1037
+ const onAbort = () => stop(signal.reason, true);
1038
+ const body = new ReadableStream({
1039
+ start(controller) {
1040
+ bodyController = controller;
1041
+ signal.addEventListener("abort", onAbort, { once: true });
1042
+ if (signal.aborted)
1043
+ onAbort();
1044
+ },
1045
+ async pull(controller) {
1046
+ try {
1047
+ const result = await untilAborted(reader.read(), signal);
1048
+ if (stopped)
1049
+ return;
1050
+ if (result.done) {
1051
+ stopped = true;
1052
+ release();
1053
+ signal.removeEventListener("abort", onAbort);
1054
+ reader.releaseLock();
1055
+ controller.close();
1056
+ } else {
1057
+ controller.enqueue(result.value);
1058
+ }
1059
+ } catch (error) {
1060
+ stop(error, true);
1061
+ }
1062
+ },
1063
+ cancel(reason) {
1064
+ stop(reason, false);
1065
+ }
1066
+ }, { highWaterMark: 0 });
1067
+ return preserveResponseMetadata(new Response(body, {
1068
+ status: response.status,
1069
+ statusText: response.statusText,
1070
+ headers: response.headers
1071
+ }), response);
1072
+ }
1073
+ async function safeFetch(input, init = {}, options = {}) {
1074
+ init.signal?.throwIfAborted();
1075
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
1076
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1077
+ throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
1078
+ }
1079
+ const maxRedirects = options.maxRedirects ?? 0;
1080
+ const urlOptions = {
1081
+ allowHttp: options.allowHttp,
1082
+ resolveDns: options.resolveDns ?? true,
1083
+ allowPrivate: options.allowPrivate
1084
+ };
1085
+ const controller = new AbortController;
1086
+ const forwardAbort = () => controller.abort(init.signal?.reason);
1087
+ init.signal?.addEventListener("abort", forwardAbort, { once: true });
1088
+ const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
1089
+ const release = () => {
1090
+ clearTimeout(timeout);
1091
+ init.signal?.removeEventListener("abort", forwardAbort);
1092
+ };
1093
+ try {
1094
+ let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
1095
+ let redirectCount = 0;
1096
+ while (true) {
1097
+ controller.signal.throwIfAborted();
1098
+ const address = current.addresses[0];
1099
+ const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
1100
+ const headers = new Headers(init.headers);
1101
+ if (address && !headers.has("host"))
1102
+ headers.set("Host", current.url.host);
1103
+ const fetchInit = {
1104
+ ...init,
1105
+ headers,
1106
+ signal: controller.signal,
1107
+ redirect: "manual"
1108
+ };
1109
+ if (address)
1110
+ fetchInit.tls = { serverName: current.url.hostname };
1111
+ const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
1112
+ if (response.status >= 300 && response.status < 400) {
1113
+ const location = response.headers.get("location");
1114
+ if (location && redirectCount < maxRedirects) {
1115
+ if (response.body)
1116
+ await untilAborted(response.body.cancel(), controller.signal);
1117
+ current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
1118
+ redirectCount++;
1119
+ continue;
1120
+ }
1121
+ }
1122
+ return withBodyDeadline(response, controller.signal, release);
1123
+ }
1124
+ } catch (error) {
1125
+ controller.abort(error);
1126
+ release();
1127
+ throw error;
1128
+ }
1129
+ }
1130
+
998
1131
  // ../../src/core/security/safeUrl.ts
999
- import { lookup as dnsLookupImpl } from "dns/promises";
1000
1132
  import { BadRequestError } from "@getstrata/core/errors/http";
1001
- var dnsLookup = dnsLookupImpl;
1002
1133
  var BLOCKED_HOSTNAMES = new Set([
1003
1134
  "localhost",
1004
1135
  "127.0.0.1",
@@ -1199,171 +1330,6 @@ function assertSafeOutboundUrl(rawUrl, options = {}) {
1199
1330
  }
1200
1331
  return parsed;
1201
1332
  }
1202
- function isBlockedIpAddress(address) {
1203
- return isBlockedHostname(address.trim().toLowerCase());
1204
- }
1205
- function isLiteralIpHostname(hostname) {
1206
- return parseCanonicalDottedDecimal(hostname) !== null || expandIpv6(hostname) !== null;
1207
- }
1208
- function pinUrlToAddress(url, address) {
1209
- const pinned = new URL(url.toString());
1210
- const ipv6 = expandIpv6(address.replace(/^\[|\]$/gu, ""));
1211
- pinned.hostname = ipv6 ? `[${address.replace(/^\[|\]$/gu, "")}]` : address;
1212
- return pinned;
1213
- }
1214
- async function resolveSafeOutboundTarget(rawUrl, options = {}) {
1215
- const resolvedOptions = productionSafeOutboundOptions(options);
1216
- const parsed = assertSafeOutboundUrl(rawUrl, resolvedOptions);
1217
- if (resolvedOptions.resolveDns === false || resolvedOptions.allowPrivate) {
1218
- return { url: parsed, addresses: [] };
1219
- }
1220
- const hostname = parsed.hostname.trim().toLowerCase();
1221
- if (isLiteralIpHostname(hostname)) {
1222
- return { url: parsed, addresses: [hostname] };
1223
- }
1224
- let results;
1225
- try {
1226
- results = await dnsLookup(hostname, { all: true, verbatim: true });
1227
- } catch {
1228
- throw new BadRequestError("Webhook URL targets a blocked host.");
1229
- }
1230
- if (results.length === 0 || results.some((result) => isBlockedIpAddress(result.address))) {
1231
- throw new BadRequestError("Webhook URL targets a blocked host.");
1232
- }
1233
- return { url: parsed, addresses: results.map((result) => result.address) };
1234
- }
1235
-
1236
- // ../../src/core/security/safeFetch.ts
1237
- var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1238
- function untilAborted(work, signal) {
1239
- signal.throwIfAborted();
1240
- return new Promise((resolve, reject) => {
1241
- const onAbort = () => reject(signal.reason);
1242
- signal.addEventListener("abort", onAbort, { once: true });
1243
- work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
1244
- });
1245
- }
1246
- function preserveResponseMetadata(response, original) {
1247
- Object.defineProperties(response, {
1248
- url: { value: original.url },
1249
- redirected: { value: original.redirected },
1250
- type: { value: original.type }
1251
- });
1252
- const clone = response.clone.bind(response);
1253
- response.clone = () => preserveResponseMetadata(clone(), original);
1254
- return response;
1255
- }
1256
- function withBodyDeadline(response, signal, release) {
1257
- if (!response.body) {
1258
- release();
1259
- return response;
1260
- }
1261
- const reader = response.body.getReader();
1262
- let stopped = false;
1263
- let bodyController;
1264
- const stop = (reason, errorBody) => {
1265
- if (stopped)
1266
- return;
1267
- stopped = true;
1268
- release();
1269
- signal.removeEventListener("abort", onAbort);
1270
- if (errorBody)
1271
- bodyController.error(reason);
1272
- reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
1273
- };
1274
- const onAbort = () => stop(signal.reason, true);
1275
- const body = new ReadableStream({
1276
- start(controller) {
1277
- bodyController = controller;
1278
- signal.addEventListener("abort", onAbort, { once: true });
1279
- if (signal.aborted)
1280
- onAbort();
1281
- },
1282
- async pull(controller) {
1283
- try {
1284
- const result = await untilAborted(reader.read(), signal);
1285
- if (stopped)
1286
- return;
1287
- if (result.done) {
1288
- stopped = true;
1289
- release();
1290
- signal.removeEventListener("abort", onAbort);
1291
- reader.releaseLock();
1292
- controller.close();
1293
- } else {
1294
- controller.enqueue(result.value);
1295
- }
1296
- } catch (error) {
1297
- stop(error, true);
1298
- }
1299
- },
1300
- cancel(reason) {
1301
- stop(reason, false);
1302
- }
1303
- }, { highWaterMark: 0 });
1304
- return preserveResponseMetadata(new Response(body, {
1305
- status: response.status,
1306
- statusText: response.statusText,
1307
- headers: response.headers
1308
- }), response);
1309
- }
1310
- async function safeFetch(input, init = {}, options = {}) {
1311
- init.signal?.throwIfAborted();
1312
- const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
1313
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1314
- throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
1315
- }
1316
- const maxRedirects = options.maxRedirects ?? 0;
1317
- const urlOptions = {
1318
- allowHttp: options.allowHttp,
1319
- resolveDns: options.resolveDns ?? true,
1320
- allowPrivate: options.allowPrivate
1321
- };
1322
- const controller = new AbortController;
1323
- const forwardAbort = () => controller.abort(init.signal?.reason);
1324
- init.signal?.addEventListener("abort", forwardAbort, { once: true });
1325
- const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
1326
- const release = () => {
1327
- clearTimeout(timeout);
1328
- init.signal?.removeEventListener("abort", forwardAbort);
1329
- };
1330
- try {
1331
- let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
1332
- let redirectCount = 0;
1333
- while (true) {
1334
- controller.signal.throwIfAborted();
1335
- const address = current.addresses[0];
1336
- const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
1337
- const headers = new Headers(init.headers);
1338
- if (address && !headers.has("host"))
1339
- headers.set("Host", current.url.host);
1340
- const fetchInit = {
1341
- ...init,
1342
- headers,
1343
- signal: controller.signal,
1344
- redirect: "manual"
1345
- };
1346
- if (address)
1347
- fetchInit.tls = { serverName: current.url.hostname };
1348
- const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
1349
- if (response.status >= 300 && response.status < 400) {
1350
- const location = response.headers.get("location");
1351
- if (location && redirectCount < maxRedirects) {
1352
- if (response.body)
1353
- await untilAborted(response.body.cancel(), controller.signal);
1354
- current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
1355
- redirectCount++;
1356
- continue;
1357
- }
1358
- }
1359
- return withBodyDeadline(response, controller.signal, release);
1360
- }
1361
- } catch (error) {
1362
- controller.abort(error);
1363
- release();
1364
- throw error;
1365
- }
1366
- }
1367
1333
 
1368
1334
  // ../../src/core/audit/exportAuditLogs.ts
1369
1335
  init_databaseTenantContext();
@@ -1,118 +1,6 @@
1
1
  // @bun
2
- var __esm = (fn, res, err) => () => {
3
- if (fn)
4
- try {
5
- res = fn(fn = 0);
6
- } catch (e) {
7
- err = [e];
8
- }
9
- if (err)
10
- throw err[0];
11
- return res;
12
- };
13
-
14
- // ../../src/core/runtime/asyncContextStore.ts
15
- import { AsyncLocalStorage } from "async_hooks";
16
- function createAsyncContextStore(key) {
17
- const symbol = Symbol.for(key);
18
- const globalRecord = globalThis;
19
- const existing = globalRecord[symbol];
20
- if (existing) {
21
- return existing;
22
- }
23
- const store = new AsyncLocalStorage;
24
- globalRecord[symbol] = store;
25
- return store;
26
- }
27
- var init_asyncContextStore = () => {};
28
-
29
- // ../../src/core/auth/membershipContext.ts
30
- init_asyncContextStore();
31
-
32
- // ../../src/core/auth/accessControl.ts
33
- import { ForbiddenError } from "@getstrata/core/errors/http";
34
-
35
- // ../../src/core/auth/authContext.ts
36
- init_asyncContextStore();
37
- var authContext = createAsyncContextStore("@getstrata/authContext");
38
- var credentialSourceContext = createAsyncContextStore("@getstrata/credentialSource");
39
- function currentAuthUser() {
40
- return authContext.getStore() ?? null;
41
- }
42
-
43
- // ../../src/core/auth/accessControl.ts
44
- var ROLE_RANK = {
45
- member: 1,
46
- admin: 2,
47
- owner: 3
48
- };
49
- function isGlobalAdmin(user) {
50
- return user?.role === "admin";
51
- }
52
- function hasMinimumOrgRole(role, minimum) {
53
- if (!role) {
54
- return false;
55
- }
56
- return ROLE_RANK[role] >= ROLE_RANK[minimum];
57
- }
58
- function requireAuthenticatedUser() {
59
- const user = currentAuthUser();
60
- if (!user) {
61
- throw new ForbiddenError("Authentication required.");
62
- }
63
- return user;
64
- }
65
- function resolveUserId(user) {
66
- const userId = typeof user.id === "number" ? user.id : Number(user.id);
67
- if (!Number.isInteger(userId) || userId <= 0) {
68
- throw new ForbiddenError("Invalid authenticated user.");
69
- }
70
- return userId;
71
- }
72
-
73
- // ../../src/core/auth/membershipContext.ts
74
- var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
75
- var uninitializedMembershipLookup = {
76
- async listForUser() {
77
- return [];
78
- },
79
- async findMembership() {
80
- return null;
81
- },
82
- async listForOrganization() {
83
- return [];
84
- },
85
- async addMember() {
86
- throw new Error("configureMembershipLookup() must be called before mutating memberships.");
87
- },
88
- async removeMember() {
89
- throw new Error("configureMembershipLookup() must be called before mutating memberships.");
90
- },
91
- async updateMemberRole() {
92
- throw new Error("configureMembershipLookup() must be called before mutating memberships.");
93
- }
94
- };
95
- var membershipRepository = uninitializedMembershipLookup;
96
- function resolveMembershipLookup() {
97
- return membershipRepository;
98
- }
99
- async function runWithMembershipContext(callback) {
100
- const user = currentAuthUser();
101
- if (!user || isGlobalAdmin(user)) {
102
- return await callback();
103
- }
104
- const memberships = await membershipRepository.listForUser(resolveUserId(user));
105
- const context = {
106
- organizationIds: memberships.map((membership) => membership.organization_id),
107
- rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
108
- };
109
- return await membershipContext.run(context, callback);
110
- }
111
- function currentOrganizationIds() {
112
- return membershipContext.getStore()?.organizationIds ?? [];
113
- }
114
-
115
2
  // ../../src/core/auth/membershipContextMiddleware.ts
3
+ import { runWithMembershipContext } from "@getstrata/core/auth/membershipContext";
116
4
  function createMembershipContextMiddleware() {
117
5
  return async (_request, next) => {
118
6
  return await runWithMembershipContext(async () => await next());
@@ -74,42 +74,6 @@ function resolveUserId(user) {
74
74
  // ../../src/core/auth/membershipContext.ts
75
75
  init_asyncContextStore();
76
76
  var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
77
- var uninitializedMembershipLookup = {
78
- async listForUser() {
79
- return [];
80
- },
81
- async findMembership() {
82
- return null;
83
- },
84
- async listForOrganization() {
85
- return [];
86
- },
87
- async addMember() {
88
- throw new Error("configureMembershipLookup() must be called before mutating memberships.");
89
- },
90
- async removeMember() {
91
- throw new Error("configureMembershipLookup() must be called before mutating memberships.");
92
- },
93
- async updateMemberRole() {
94
- throw new Error("configureMembershipLookup() must be called before mutating memberships.");
95
- }
96
- };
97
- var membershipRepository = uninitializedMembershipLookup;
98
- function resolveMembershipLookup() {
99
- return membershipRepository;
100
- }
101
- async function runWithMembershipContext(callback) {
102
- const user = currentAuthUser();
103
- if (!user || isGlobalAdmin(user)) {
104
- return await callback();
105
- }
106
- const memberships = await membershipRepository.listForUser(resolveUserId(user));
107
- const context = {
108
- organizationIds: memberships.map((membership) => membership.organization_id),
109
- rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
110
- };
111
- return await membershipContext.run(context, callback);
112
- }
113
77
  function currentOrganizationIds() {
114
78
  return membershipContext.getStore()?.organizationIds ?? [];
115
79
  }
@@ -27,6 +27,7 @@ function createAsyncContextStore(key) {
27
27
  var init_asyncContextStore = () => {};
28
28
 
29
29
  // ../../src/core/auth/membershipService.ts
30
+ import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
30
31
  import { ForbiddenError as ForbiddenError2 } from "@getstrata/core/errors/http";
31
32
 
32
33
  // ../../src/core/auth/accessControl.ts
@@ -70,49 +71,6 @@ function resolveUserId(user) {
70
71
  return userId;
71
72
  }
72
73
 
73
- // ../../src/core/auth/membershipContext.ts
74
- init_asyncContextStore();
75
- var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
76
- var uninitializedMembershipLookup = {
77
- async listForUser() {
78
- return [];
79
- },
80
- async findMembership() {
81
- return null;
82
- },
83
- async listForOrganization() {
84
- return [];
85
- },
86
- async addMember() {
87
- throw new Error("configureMembershipLookup() must be called before mutating memberships.");
88
- },
89
- async removeMember() {
90
- throw new Error("configureMembershipLookup() must be called before mutating memberships.");
91
- },
92
- async updateMemberRole() {
93
- throw new Error("configureMembershipLookup() must be called before mutating memberships.");
94
- }
95
- };
96
- var membershipRepository = uninitializedMembershipLookup;
97
- function resolveMembershipLookup() {
98
- return membershipRepository;
99
- }
100
- async function runWithMembershipContext(callback) {
101
- const user = currentAuthUser();
102
- if (!user || isGlobalAdmin(user)) {
103
- return await callback();
104
- }
105
- const memberships = await membershipRepository.listForUser(resolveUserId(user));
106
- const context = {
107
- organizationIds: memberships.map((membership) => membership.organization_id),
108
- rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
109
- };
110
- return await membershipContext.run(context, callback);
111
- }
112
- function currentOrganizationIds() {
113
- return membershipContext.getStore()?.organizationIds ?? [];
114
- }
115
-
116
74
  // ../../src/core/auth/resolveMembershipService.ts
117
75
  import { resolveApplicationDependencies } from "@getstrata/core/runtime/applicationRegistry";
118
76
  function resolveMembershipService() {