nestjs-slightly-better-auth 1.9.2 → 1.10.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/README.md +1 -1
- package/dist/testing/conformance.cjs +83 -42
- package/dist/testing/conformance.cjs.map +1 -1
- package/dist/testing/conformance.d.cts +21 -5
- package/dist/testing/conformance.d.cts.map +1 -1
- package/dist/testing/conformance.d.mts +21 -5
- package/dist/testing/conformance.d.mts.map +1 -1
- package/dist/testing/conformance.mjs +83 -42
- package/dist/testing/conformance.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -948,7 +948,7 @@ Better Auth treats a process with `NODE_ENV=test` or a truthy `TEST` variable (V
|
|
|
948
948
|
|
|
949
949
|
The kits verify extension units against the invariants of the [reviewed specification](docs/design/design-v7.md) (§4 and §14.1). Each kit returns `ConformanceCase[]` (`{ id, title, skip?, run() }`) that `runConformance(cases, { describe, it })` registers with Vitest, Jest or `node:test`. Assertions use `node:assert/strict`; the kits import no test runner.
|
|
950
950
|
|
|
951
|
-
Applicability comes from the unit, not from the options a harness passes. A platform that declares `capabilities.http2` needs the `http2` option. The platform kit's trusted-hop rows need `trustOneProxy`, and
|
|
951
|
+
Applicability comes from the unit, not from the options a harness passes. A platform that declares `capabilities.http2` needs the `http2` option. The platform kit's trusted-hop rows need `trustOneProxy`, which trusts one forwarding hop on a created application (Express `app.set("trust proxy", 1)`), or `createTrustingHttpAdapter` (and `http2.createTrustingHttpAdapter` for their HTTP/2 rows), which returns a fresh adapter that already trusts it, for platforms that fix proxy trust at construction (Fastify `new FastifyAdapter({ trustProxy: "loopback" })`; the kit's requests come from loopback). Its microservice rows of `H-no-adapter` need `microservice`, which returns the transport options to boot with (for example `{ transport: Transport.TCP, options: { host: "127.0.0.1", port: 0 } }`); the kit does not load the optional `@nestjs/microservices` peer itself. The production-mode rows set `process.env.NODE_ENV` to `production` while they run and restore it afterwards, so run the kit's cases sequentially. A transport with a lineage needs `invokeTwice`, one that nests handlers (`defaultAccessFor`) needs `invokeGraph`, and one that claims the kit's federation reference resolver needs `invokeEntities`. A transport whose browser leg is a connection's handshake, shared by the connection's messages (the WebSocket transports), runs the `T-ws-origin-*` cases instead of the `T-csrf-*` cases and needs `invokeConnection`, plus `authenticateConnection` when the app provides `WS_CONNECTION_AUTH`. Such a case fails when the helper is missing, and `T-selection` fails `expectBrowserLeg: false` for a transport that describes a browser leg. A transport that nests handlers needs `graphqlContexts` to list `'static'` and `'cached'` for `T-stale-context`, whose cached-context row runs where the browser leg follows each operation; the custom-context rows of `T-subscription-origin` run for the listed `'fresh'` and `'plain-request'` contexts. The `T-subscription-*` cases run when `invocationShapes` includes `'subscriptions'` and need `invokeConnection` with its `connectionParams` option. `T-inherited-handler` runs with `invokeInherited`; harnesses of transports that address a handler by a name one inherited method shares across subclasses (an RPC pattern, a GraphQL root field) omit it, and the case is skipped with the reason. A case whose optional capability is absent is skipped with the reason. The kits read these members from unit objects before running; for classes and `defineExtension()` definitions they boot once and read the resolved unit. A case known to be inapplicable registers with its reason in the test name. A case that finds out at run time resolves with `{ skipped: reason }`, which `runConformance` reports through the runner's `context.skip(reason)` (Vitest, `node:test`; Jest has no runtime skip, so the test passes).
|
|
952
952
|
|
|
953
953
|
```ts
|
|
954
954
|
import { ExpressAdapter } from "@nestjs/platform-express";
|
|
@@ -853,17 +853,6 @@ async function capabilitiesOf(options) {
|
|
|
853
853
|
await boot.close();
|
|
854
854
|
}
|
|
855
855
|
}
|
|
856
|
-
/** Whether the resolved platform implements an optional member, booting a testing app when the reference cannot tell. */
|
|
857
|
-
async function implementsMember(options, name) {
|
|
858
|
-
const known = staticMember$1(options.platform, name);
|
|
859
|
-
if (known !== void 0) return known;
|
|
860
|
-
const boot = await bootHttp(options, "testing", {});
|
|
861
|
-
try {
|
|
862
|
-
return typeof Reflect.get(resolvedPlatform(boot.app), name) === "function";
|
|
863
|
-
} finally {
|
|
864
|
-
await boot.close();
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
856
|
function kitModule$1(options, auth, probe, config, state) {
|
|
868
857
|
const hooks = state.hooks ?? config.hooks;
|
|
869
858
|
const record = async (call, next) => {
|
|
@@ -1059,6 +1048,17 @@ function kitModule$1(options, auth, probe, config, state) {
|
|
|
1059
1048
|
})], ConformanceHttpModule);
|
|
1060
1049
|
return ConformanceHttpModule;
|
|
1061
1050
|
}
|
|
1051
|
+
/** The factory of an adapter that trusts one forwarding hop, for HTTP/1.1 or HTTP/2, when the options give one. */
|
|
1052
|
+
function trustingAdapterOf(options, http2) {
|
|
1053
|
+
const source = http2 ? options.http2 : options;
|
|
1054
|
+
return source?.createTrustingHttpAdapter ? () => source.createTrustingHttpAdapter() : void 0;
|
|
1055
|
+
}
|
|
1056
|
+
/** A fresh adapter for a boot: the trusting adapter for a trusted-hop boot when given, else the default one. */
|
|
1057
|
+
function createAdapter(options, config) {
|
|
1058
|
+
const trusting = config.trustProxy ? trustingAdapterOf(options, config.http2) : void 0;
|
|
1059
|
+
if (trusting) return trusting();
|
|
1060
|
+
return config.http2 ? options.http2.createHttpAdapter() : options.createHttpAdapter();
|
|
1061
|
+
}
|
|
1062
1062
|
async function bootHttp(options, bootstrap, config) {
|
|
1063
1063
|
const auth = config.instance ?? createConformanceAuth(config.auth);
|
|
1064
1064
|
const probe = await probeOf(auth);
|
|
@@ -1067,7 +1067,7 @@ async function bootHttp(options, bootstrap, config) {
|
|
|
1067
1067
|
const around = [];
|
|
1068
1068
|
const accessor = {};
|
|
1069
1069
|
let application;
|
|
1070
|
-
const adapter =
|
|
1070
|
+
const adapter = createAdapter(options, config);
|
|
1071
1071
|
const requests = () => resolvedPlatform(application).requests;
|
|
1072
1072
|
const module = kitModule$1(options, auth, probe, config, {
|
|
1073
1073
|
filtered,
|
|
@@ -1095,8 +1095,8 @@ async function bootHttp(options, bootstrap, config) {
|
|
|
1095
1095
|
}
|
|
1096
1096
|
if (config.prefix) app.setGlobalPrefix(config.prefix);
|
|
1097
1097
|
if (config.versioning) app.enableVersioning({ type: _nestjs_common.VersioningType.URI });
|
|
1098
|
-
if (config.trustProxy) {
|
|
1099
|
-
node_assert_strict.default.ok(options.trustOneProxy, "trustOneProxy is required here");
|
|
1098
|
+
if (config.trustProxy && !trustingAdapterOf(options, config.http2)) {
|
|
1099
|
+
node_assert_strict.default.ok(options.trustOneProxy, "trustOneProxy or createTrustingHttpAdapter is required here");
|
|
1100
1100
|
await options.trustOneProxy(app);
|
|
1101
1101
|
}
|
|
1102
1102
|
await app.init();
|
|
@@ -1104,6 +1104,7 @@ async function bootHttp(options, bootstrap, config) {
|
|
|
1104
1104
|
return {
|
|
1105
1105
|
app,
|
|
1106
1106
|
url,
|
|
1107
|
+
http2: config.http2 === true,
|
|
1107
1108
|
auth,
|
|
1108
1109
|
probe,
|
|
1109
1110
|
logger,
|
|
@@ -1263,9 +1264,10 @@ const multipartBody = [
|
|
|
1263
1264
|
/**
|
|
1264
1265
|
* The HTTP platform kit (invariants H1–H14). Each case boots a real Nest application per bootstrap with a real Better
|
|
1265
1266
|
* Auth instance that includes conformanceProbePlugin(), an app controller, a @BeforeAuth() hook provider, a recording
|
|
1266
|
-
* global filter and an `around` recorder. HTTP/2 is required when the platform declares capabilities.http2
|
|
1267
|
-
* without the capability
|
|
1268
|
-
*
|
|
1267
|
+
* global filter and an `around` recorder. HTTP/2 is required when the platform declares capabilities.http2. A platform
|
|
1268
|
+
* without the capability or the optional proxyTrust(), and a harness with neither trustOneProxy nor
|
|
1269
|
+
* createTrustingHttpAdapter for the trusted-hop rows, get a skip with the reason: before the run for platform objects,
|
|
1270
|
+
* at run time for classes and definitions. The microservice rows of H-no-adapter need the
|
|
1269
1271
|
* `microservice` option. Production-mode rows set process.env.NODE_ENV to 'production' while they run and restore it,
|
|
1270
1272
|
* so run the cases sequentially.
|
|
1271
1273
|
*/
|
|
@@ -1282,7 +1284,13 @@ function httpPlatformConformance(options) {
|
|
|
1282
1284
|
node_assert_strict.default.ok(o.http2, "the platform declares capabilities.http2, so the kit requires the http2 option");
|
|
1283
1285
|
};
|
|
1284
1286
|
const noProxyTrust = () => staticMember$1(options.platform, "proxyTrust") === false ? "the platform does not implement the optional proxyTrust()" : void 0;
|
|
1285
|
-
|
|
1287
|
+
/** A skip when neither trustOneProxy nor the protocol's createTrustingHttpAdapter can make a trusted-hop boot. */
|
|
1288
|
+
const noTrustHook = (http2 = false) => options.trustOneProxy || trustingAdapterOf(options, http2) ? void 0 : http2 ? "neither trustOneProxy nor http2.createTrustingHttpAdapter was given" : "neither trustOneProxy nor createTrustingHttpAdapter was given";
|
|
1289
|
+
/** The static skip of a trusted-hop row: the HTTP/2 row also needs the http2 capability. */
|
|
1290
|
+
const trustedHop = (http2) => () => (http2 ? noHttp2() : void 0) ?? noTrustHook(http2);
|
|
1291
|
+
/** The run-time skip of a trusted-hop row, decided from the resolved platform for HTTP/2. */
|
|
1292
|
+
const trustedHopReady = async (o, http2) => http2 ? http2Ready(o) : void 0;
|
|
1293
|
+
const overHttp2 = (http2, title) => http2 ? `over HTTP/2, ${title}` : title;
|
|
1286
1294
|
add("H-mount-methods", "every method under the base path reaches Better Auth", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
1287
1295
|
for (const method of [
|
|
1288
1296
|
"GET",
|
|
@@ -1586,11 +1594,16 @@ function httpPlatformConformance(options) {
|
|
|
1586
1594
|
});
|
|
1587
1595
|
for (const [name, value] of Object.entries(headers)) node_assert_strict.default.equal(result.headers.get(name), value, `${name} changed`);
|
|
1588
1596
|
}));
|
|
1597
|
+
/** GET /probe/ip over the boot's protocol. */
|
|
1589
1598
|
const probeIp = async (boot, headers = {}) => {
|
|
1590
|
-
const response = await
|
|
1599
|
+
const response = boot.http2 ? await http2Request(http2Origin(boot), {
|
|
1600
|
+
":method": "GET",
|
|
1601
|
+
":path": `${AUTH}/probe/ip`,
|
|
1602
|
+
...headers
|
|
1603
|
+
}) : await sendRaw(`${boot.url}${AUTH}/probe/ip`, { headers });
|
|
1591
1604
|
return response.status === 200 ? {
|
|
1592
1605
|
status: response.status,
|
|
1593
|
-
...
|
|
1606
|
+
...JSON.parse(response.body.toString("utf8"))
|
|
1594
1607
|
} : { status: response.status };
|
|
1595
1608
|
};
|
|
1596
1609
|
add("H-ip-platform", "Better Auth's getIP sees the platform client IP", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
@@ -1632,12 +1645,15 @@ function httpPlatformConformance(options) {
|
|
|
1632
1645
|
node_assert_strict.default.deepEqual([...new Set(keys)], [`${LOOPBACK}|/probe/ip`], "the rate limiter was not keyed by the platform IP");
|
|
1633
1646
|
}));
|
|
1634
1647
|
});
|
|
1635
|
-
add("H-ip-platform", "in production mode behind a trusted hop, every forwarded client gets its own rate-limit bucket", (o, b) => {
|
|
1648
|
+
for (const http2 of [false, true]) add("H-ip-platform", overHttp2(http2, "in production mode behind a trusted hop, every forwarded client gets its own rate-limit bucket"), async (o, b) => {
|
|
1649
|
+
const skip = await trustedHopReady(o, http2);
|
|
1650
|
+
if (skip) return skip;
|
|
1636
1651
|
const keys = [];
|
|
1637
1652
|
const [left, right] = ["203.0.113.30", "203.0.113.31"];
|
|
1638
1653
|
return inProduction(() => withBoot(o, b, {
|
|
1639
1654
|
...rateLimited(keys),
|
|
1640
|
-
trustProxy: true
|
|
1655
|
+
trustProxy: true,
|
|
1656
|
+
http2
|
|
1641
1657
|
}, async (boot) => {
|
|
1642
1658
|
const statuses = [];
|
|
1643
1659
|
for (let index = 0; index <= IP_RULE.max; index++) statuses.push((await probeIp(boot, { "x-forwarded-for": left })).status);
|
|
@@ -1651,7 +1667,7 @@ function httpPlatformConformance(options) {
|
|
|
1651
1667
|
node_assert_strict.default.equal(other.resolved, right);
|
|
1652
1668
|
node_assert_strict.default.deepEqual([...new Set(keys)].sort(), [`${left}|/probe/ip`, `${right}|/probe/ip`]);
|
|
1653
1669
|
}));
|
|
1654
|
-
},
|
|
1670
|
+
}, trustedHop(http2));
|
|
1655
1671
|
add("H-ip-platform", "with trustedProxies covering the platform IP and trust off, Better Auth resolves no IP and B18 warns where proxyTrust() reports 'none'", (o, b) => inProduction(() => withBoot(o, b, loopbackProxies, async (boot) => {
|
|
1656
1672
|
const value = await probeIp(boot, { "x-forwarded-for": "203.0.113.40" });
|
|
1657
1673
|
node_assert_strict.default.equal(value.resolved, null, `Better Auth resolved ${String(value.resolved)} although trustedProxies covers the socket IP`);
|
|
@@ -1659,14 +1675,19 @@ function httpPlatformConformance(options) {
|
|
|
1659
1675
|
const b18 = warningsWith(boot, "W_CLIENT_IP", "trustedProxies");
|
|
1660
1676
|
if (trust?.mode === "none") node_assert_strict.default.equal(b18.length, 1, boot.logger.text());
|
|
1661
1677
|
})));
|
|
1662
|
-
add("H-ip-platform", "with trustedProxies covering the proxy and trust on, Better Auth resolves the client IP without a B18 warning", (o, b) =>
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1678
|
+
for (const http2 of [false, true]) add("H-ip-platform", overHttp2(http2, "with trustedProxies covering the proxy and trust on, Better Auth resolves the client IP without a B18 warning"), async (o, b) => {
|
|
1679
|
+
const skip = await trustedHopReady(o, http2);
|
|
1680
|
+
if (skip) return skip;
|
|
1681
|
+
return inProduction(() => withBoot(o, b, {
|
|
1682
|
+
...loopbackProxies,
|
|
1683
|
+
trustProxy: true,
|
|
1684
|
+
http2
|
|
1685
|
+
}, async (boot) => {
|
|
1686
|
+
const value = await probeIp(boot, { "x-forwarded-for": "203.0.113.41" });
|
|
1687
|
+
node_assert_strict.default.equal(value.resolved, "203.0.113.41", "Better Auth did not resolve the client IP behind the trusted hop");
|
|
1688
|
+
node_assert_strict.default.deepEqual(warningsWith(boot, "W_CLIENT_IP", "trustedProxies"), []);
|
|
1689
|
+
}));
|
|
1690
|
+
}, trustedHop(http2));
|
|
1670
1691
|
add("H-ip-spoof", "client-sent client-IP headers are ignored", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
1671
1692
|
const ip = json$1(await sendRaw(`${boot.url}${AUTH}/probe/ip`, { headers: {
|
|
1672
1693
|
"x-forwarded-for": "203.0.113.7",
|
|
@@ -1679,20 +1700,30 @@ function httpPlatformConformance(options) {
|
|
|
1679
1700
|
"203.0.113.9"
|
|
1680
1701
|
].includes(String(ip)), `a spoofed IP reached Better Auth: ${String(ip)}`);
|
|
1681
1702
|
}));
|
|
1682
|
-
add("H-proxy-trust", "proxyTrust() reports the effective setting before and after
|
|
1703
|
+
add("H-proxy-trust", "proxyTrust() reports the effective setting before and after trusting one hop", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
1683
1704
|
const platform = resolvedPlatform(boot.app);
|
|
1684
1705
|
if (typeof platform.proxyTrust !== "function") return conformanceSkip("the platform does not implement the optional proxyTrust()");
|
|
1685
1706
|
const adapter = boot.app.getHttpAdapter();
|
|
1686
1707
|
const before = platform.proxyTrust(adapter);
|
|
1687
|
-
|
|
1688
|
-
|
|
1708
|
+
let after;
|
|
1709
|
+
if (o.trustOneProxy) {
|
|
1710
|
+
await o.trustOneProxy(boot.app);
|
|
1711
|
+
after = platform.proxyTrust(adapter);
|
|
1712
|
+
} else {
|
|
1713
|
+
const trusted = await bootHttp(o, b, { trustProxy: true });
|
|
1714
|
+
try {
|
|
1715
|
+
after = resolvedPlatform(trusted.app).proxyTrust(trusted.app.getHttpAdapter());
|
|
1716
|
+
} finally {
|
|
1717
|
+
await trusted.close();
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1689
1720
|
if (before.mode === "unknown" || after.mode === "unknown") {
|
|
1690
1721
|
node_assert_strict.default.equal(before.mode, after.mode, "a platform that cannot inspect proxy trust must report unknown consistently");
|
|
1691
1722
|
return;
|
|
1692
1723
|
}
|
|
1693
|
-
node_assert_strict.default.notDeepEqual(after, before, "proxyTrust() did not reflect
|
|
1724
|
+
node_assert_strict.default.notDeepEqual(after, before, "proxyTrust() did not reflect the trusted hop");
|
|
1694
1725
|
node_assert_strict.default.notEqual(after.mode, "none");
|
|
1695
|
-
}), () =>
|
|
1726
|
+
}), () => noProxyTrust() ?? noTrustHook());
|
|
1696
1727
|
const forwarded = {
|
|
1697
1728
|
"x-forwarded-proto": "https",
|
|
1698
1729
|
"x-forwarded-host": "proxy.example"
|
|
@@ -1712,7 +1743,7 @@ function httpPlatformConformance(options) {
|
|
|
1712
1743
|
trustProxy: true
|
|
1713
1744
|
}, async (boot) => {
|
|
1714
1745
|
node_assert_strict.default.equal((await probedUrl(boot)).origin, "https://proxy.example");
|
|
1715
|
-
}), () =>
|
|
1746
|
+
}), () => noTrustHook());
|
|
1716
1747
|
add("H-h2-pseudo", "HTTP/2 pseudo-headers never reach Better Auth", async (o, b) => {
|
|
1717
1748
|
const skip = await http2Ready(o);
|
|
1718
1749
|
if (skip) return skip;
|
|
@@ -2085,13 +2116,23 @@ function httpPlatformConformance(options) {
|
|
|
2085
2116
|
await sendRaw(`${boot.url}${AUTH}/probe/html`, { headers: { "x-forwarded-for": "203.0.113.54" } });
|
|
2086
2117
|
node_assert_strict.default.deepEqual(proxyWarnings(boot), []);
|
|
2087
2118
|
})), noProxyTrust);
|
|
2088
|
-
add("H-proxy-untrusted-warning", "in production behind a trusted hop, forwarded auth requests log no W_PROXY_UNTRUSTED", async (o, b) => {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2119
|
+
for (const http2 of [false, true]) add("H-proxy-untrusted-warning", overHttp2(http2, "in production behind a trusted hop, forwarded auth requests log no W_PROXY_UNTRUSTED"), async (o, b) => {
|
|
2120
|
+
const skip = await trustedHopReady(o, http2);
|
|
2121
|
+
if (skip) return skip;
|
|
2122
|
+
return inProduction(() => withBoot(o, b, {
|
|
2123
|
+
trustProxy: true,
|
|
2124
|
+
http2
|
|
2125
|
+
}, async (boot) => {
|
|
2126
|
+
const headers = { "x-forwarded-for": "203.0.113.55" };
|
|
2127
|
+
const response = boot.http2 ? await http2Request(http2Origin(boot), {
|
|
2128
|
+
":method": "GET",
|
|
2129
|
+
":path": `${AUTH}/probe/html`,
|
|
2130
|
+
...headers
|
|
2131
|
+
}) : await sendRaw(`${boot.url}${AUTH}/probe/html`, { headers });
|
|
2132
|
+
node_assert_strict.default.equal(response.status, 200);
|
|
2092
2133
|
node_assert_strict.default.deepEqual(proxyWarnings(boot), []);
|
|
2093
2134
|
}));
|
|
2094
|
-
}, ()
|
|
2135
|
+
}, trustedHop(http2));
|
|
2095
2136
|
add("H-log-hygiene", "session and probe cookies never appear in captured logs", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
2096
2137
|
const identity = await kitIdentity(boot.auth);
|
|
2097
2138
|
const token = identity.cookie.split("=")[1].split(".")[0];
|