nestjs-slightly-better-auth 1.5.2 → 1.6.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 +3 -3
- package/dist/testing/conformance.cjs +346 -10
- package/dist/testing/conformance.cjs.map +1 -1
- package/dist/testing/conformance.d.cts +12 -2
- package/dist/testing/conformance.d.cts.map +1 -1
- package/dist/testing/conformance.d.mts +12 -2
- package/dist/testing/conformance.d.mts.map +1 -1
- package/dist/testing/conformance.mjs +346 -10
- package/dist/testing/conformance.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -343,7 +343,7 @@ Better Auth treats a process with `NODE_ENV=test` or a truthy `TEST` variable (V
|
|
|
343
343
|
|
|
344
344
|
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.
|
|
345
345
|
|
|
346
|
-
Applicability comes from the unit, not from the options a harness passes. A platform that declares `capabilities.http2` needs the `http2` option. 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 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).
|
|
346
|
+
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 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 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).
|
|
347
347
|
|
|
348
348
|
```ts
|
|
349
349
|
import { ExpressAdapter } from "@nestjs/platform-express";
|
|
@@ -380,14 +380,14 @@ runConformance(
|
|
|
380
380
|
|
|
381
381
|
| Kit | Unit | Covers |
|
|
382
382
|
| ---------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
383
|
-
| `httpPlatformConformance` | `HttpPlatform` | Mounting, byte-exact bodies, limits with host CORS, URL and header facts, client IP, HTTP/2, write-back, request accessors, error delivery, lifecycle (no adapter, late adapter, reused `TestingModule`, double `forRoot`), diagnostics and log hygiene
|
|
383
|
+
| `httpPlatformConformance` | `HttpPlatform` | Mounting (including a dynamic `allowedHosts` base URL), byte-exact bodies, limits with host CORS, URL and header facts, client IP (production-mode per-IP rate-limit buckets, Better Auth `trustedProxies`), HTTP/2, write-back, request accessors, error delivery, lifecycle (no adapter, including microservices, late adapter, reused `TestingModule`, double `forRoot`, hooks kept after `close()`), diagnostics (404 hints, `W_PROXY_UNTRUSTED`) and log hygiene |
|
|
384
384
|
| `transportConformance` | `AuthTransport` | Selection, credentials, per-request memoization, per-invocation decisions, acceptance, cookie forwarding or refresh suppression, origin and form-CSRF rules including forwarding on safe operations, WebSocket handshake origins on every message and at connection time (untrusted, missing and forwarded origins, junk tokens, dynamic base URLs, function-valued `trustedOrigins`), error shapes and logging, the exact coverage report of the transport's claims, `globalScope: false`, mixed-kind session reads, lazy scope construction and fail-closed extraction |
|
|
385
385
|
| `principalSourceConformance` | `PrincipalSource` | Rejection without throwing, storage outages, delivery of every own-credential `Set-Cookie` exactly once, refresh suppression, clean absence, principal shape, declared credential headers, `sessionBacked` and `delegates` declarations |
|
|
386
386
|
| `policyConformance` | `AuthorizationPolicy` | Deny values, storage outages, APIError mapping and re-classification, delegation scope, unnamed-kind gating, per-invocation decisions with shared I/O and plugin prerequisites; requirements name policy objects (construct a DI policy with its dependencies) |
|
|
387
387
|
|
|
388
388
|
Every case boots real Nest applications or modules with a real Better Auth instance that includes `conformanceProbePlugin()`. The probe counts and faults every operation of the instance's database adapter, so storage outages reach endpoints that would swallow them, and records the `Set-Cookie` lines each call produces. `createConformanceAuth({ plugins })` builds such an instance: memory adapter, `testUtils()`, `bearer()`, the probe plugin, `nestjs()` last, and `session.updateAge: 0` and origin checks on, whatever `options` says. The principal-source and policy kits take that instance as `auth`; the principal-source kit rejects an instance without `session.updateAge: 0`. The transport kit creates its own instance and passes it to your `createApp` harness together with the kit's API-key source in `principals`, a `logger` to boot with, and the `globalScope`, `appEnhancers`, `fieldResolverEnhancers` and `federation` settings of each boot. Apply every fixture decorator, including the kit's `SetMetadata()` marker, which maps the transport's claims to fixtures. On a connection-shaped browser leg, the kit's cookie credential carries the instance's base URL, `http://localhost:3000`, as `Origin`, as a browser page of that origin does; the `T-ws-origin-*` cases pass their own `Origin`, `Host` and forwarding headers, which the harness sends in the handshake.
|
|
389
389
|
|
|
390
|
-
This repository runs the platform kit against Express and Fastify; the transport kit against the built-in HTTP transport, an in-process reference transport, Socket.IO and `ws` (each with and without a principal TTL), the RPC transport over TCP (standalone and hybrid), gRPC, NATS, RabbitMQ, MQTT, Redis and Kafka, and the Apollo (Express and Fastify) and Mercurius transports over HTTP and over graphql-ws (with and without a subscription principal TTL; Mercurius also without `mercuriusSubscriptionContext()`), with federation 2 subgraphs and a federation 1 run of `T-reference-resolver`; the principal-source kit against the session and API-key sources; and the policy kit against the admin, organization, API-key and fresh-session policies. gRPC skips `T-error-shape`, because it delivers only a status and a message. The kit reads each call's browser leg while its invocation runs, because the GraphQL transports reject a completed request. Mutation tests confirm that each of these defects fails the intended case: a platform merging `Set-Cookie`, a transport sharing invocation state across aliases, a transport without a browser leg on safe operations, a `defineExtension()` transport with a lineage or nesting and no helper, a WebSocket transport that enforces its handshake like a safe read, exempts handshakes that carry a token, builds its leg URL from the bare handshake path or from forwarded headers, or answers internal errors with their cause, a GraphQL transport whose aliases share one invocation, whose lineage gives every field of an operation one position, whose field resolvers do not inherit their root's access, or that serves reference resolvers without field guards, a GraphQL harness that omits `invokeGraph`, `invokeEntities` or `invokeConnection`, a source throwing its denial, a source dropping its refresh cookie, a source turning a storage outage into a 401, and a policy widening delegated principals.
|
|
390
|
+
This repository runs the platform kit against Express and Fastify, including the Fastify HTTP/2 adapter for the HTTP/2 rows; the transport kit against the built-in HTTP transport, an in-process reference transport, Socket.IO and `ws` (each with and without a principal TTL), the RPC transport over TCP (standalone and hybrid), gRPC, NATS, RabbitMQ, MQTT, Redis and Kafka, and the Apollo (Express and Fastify) and Mercurius transports over HTTP and over graphql-ws (with and without a subscription principal TTL; Mercurius also without `mercuriusSubscriptionContext()`), with federation 2 subgraphs and a federation 1 run of `T-reference-resolver`; the principal-source kit against the session and API-key sources; and the policy kit against the admin, organization, API-key and fresh-session policies. gRPC skips `T-error-shape`, because it delivers only a status and a message. The kit reads each call's browser leg while its invocation runs, because the GraphQL transports reject a completed request. Mutation tests confirm that each of these defects fails the intended case: a platform merging `Set-Cookie`, handing Better Auth no client IP, ignoring its proxy trust, dropping the request host or stripping forwarding headers, a kernel that unbinds hooks on `close()`, reports taking over a closed binding or keeps a microservice's binding open, a transport sharing invocation state across aliases, a transport without a browser leg on safe operations, a `defineExtension()` transport with a lineage or nesting and no helper, a WebSocket transport that enforces its handshake like a safe read, exempts handshakes that carry a token, builds its leg URL from the bare handshake path or from forwarded headers, or answers internal errors with their cause, a GraphQL transport whose aliases share one invocation, whose lineage gives every field of an operation one position, whose field resolvers do not inherit their root's access, or that serves reference resolvers without field guards, a GraphQL harness that omits `invokeGraph`, `invokeEntities` or `invokeConnection`, a source throwing its denial, a source dropping its refresh cookie, a source turning a storage outage into a 401, and a policy widening delegated principals.
|
|
391
391
|
|
|
392
392
|
## Develop in this monorepo
|
|
393
393
|
|
|
@@ -493,7 +493,10 @@ function probePlugin(options = {}) {
|
|
|
493
493
|
probeIp: (0, better_auth_api.createAuthEndpoint)("/probe/ip", {
|
|
494
494
|
method: "GET",
|
|
495
495
|
requireRequest: true
|
|
496
|
-
}, async (ctx) => ctx.json({
|
|
496
|
+
}, async (ctx) => ctx.json({
|
|
497
|
+
ip: (0, better_auth_api.getIP)(ctx.request, ctx.context.options),
|
|
498
|
+
resolved: resolvedIp(ctx.request, ctx.context.options)
|
|
499
|
+
})),
|
|
497
500
|
probeThrowApiError: (0, better_auth_api.createAuthEndpoint)("/probe/throw-api-error", { method: "GET" }, async () => {
|
|
498
501
|
throw new better_auth_api.APIError("BAD_REQUEST", {
|
|
499
502
|
code: "PROBE_API_ERROR",
|
|
@@ -536,6 +539,30 @@ function probePlugin(options = {}) {
|
|
|
536
539
|
Object.defineProperty(plugin, PROBE_STATE, { value: state });
|
|
537
540
|
return plugin;
|
|
538
541
|
}
|
|
542
|
+
/** A TEST-NET-2 address that no kit request sends and no kit configuration trusts. */
|
|
543
|
+
const IP_SENTINEL = "198.51.100.254";
|
|
544
|
+
const IP_SENTINEL_HEADER = "x-conformance-ip-sentinel";
|
|
545
|
+
/**
|
|
546
|
+
* The client IP Better Auth resolves from the request's IP headers, without the localhost fallback getIP() returns
|
|
547
|
+
* under NODE_ENV=development/test or a truthy TEST (vitest sets it): the sentinel header is read last, so getIP()
|
|
548
|
+
* returns the sentinel exactly when the configured headers yield no trusted address.
|
|
549
|
+
*/
|
|
550
|
+
function resolvedIp(request, options) {
|
|
551
|
+
const ipAddress = options.advanced?.ipAddress;
|
|
552
|
+
const headers = new Headers(request.headers);
|
|
553
|
+
headers.set(IP_SENTINEL_HEADER, IP_SENTINEL);
|
|
554
|
+
const ip = (0, better_auth_api.getIP)(headers, {
|
|
555
|
+
...options,
|
|
556
|
+
advanced: {
|
|
557
|
+
...options.advanced,
|
|
558
|
+
ipAddress: {
|
|
559
|
+
...ipAddress,
|
|
560
|
+
ipAddressHeaders: [...ipAddress?.ipAddressHeaders ?? ["x-forwarded-for"], IP_SENTINEL_HEADER]
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
return ip === IP_SENTINEL ? null : ip;
|
|
565
|
+
}
|
|
539
566
|
/** The probe state of an instance that includes conformanceProbePlugin(). */
|
|
540
567
|
async function probeOf(auth) {
|
|
541
568
|
const plugin = (await auth.$context).getPlugin(PROBE_PLUGIN_ID);
|
|
@@ -756,6 +783,9 @@ function bootIssueCodes(error) {
|
|
|
756
783
|
//#endregion
|
|
757
784
|
//#region src/conformance-http.ts
|
|
758
785
|
const AUTH = "/api/auth";
|
|
786
|
+
const DYNAMIC_AUTH = "/tenant/auth";
|
|
787
|
+
/** The client IP of the kit's requests: baseUrl() always connects over IPv4 loopback. */
|
|
788
|
+
const LOOPBACK = "127.0.0.1";
|
|
759
789
|
const CORS_ORIGIN = "https://app.example";
|
|
760
790
|
/** Makes the kit's `around` interceptor throw, so auth.handler rejects with a non-APIError. */
|
|
761
791
|
const THROW_HEADER = "x-conformance-throw";
|
|
@@ -796,7 +826,19 @@ async function capabilitiesOf(options) {
|
|
|
796
826
|
await boot.close();
|
|
797
827
|
}
|
|
798
828
|
}
|
|
829
|
+
/** Whether the resolved platform implements an optional member, booting a testing app when the reference cannot tell. */
|
|
830
|
+
async function implementsMember(options, name) {
|
|
831
|
+
const known = staticMember$1(options.platform, name);
|
|
832
|
+
if (known !== void 0) return known;
|
|
833
|
+
const boot = await bootHttp(options, "testing", {});
|
|
834
|
+
try {
|
|
835
|
+
return typeof Reflect.get(resolvedPlatform(boot.app), name) === "function";
|
|
836
|
+
} finally {
|
|
837
|
+
await boot.close();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
799
840
|
function kitModule(options, auth, probe, config, state) {
|
|
841
|
+
const hooks = state.hooks ?? config.hooks;
|
|
800
842
|
const record = async (call, next) => {
|
|
801
843
|
state.around.push(`${call.request.method.toUpperCase()} ${new URL(call.request.url).pathname}`);
|
|
802
844
|
const failure = call.request.headers.get(THROW_HEADER);
|
|
@@ -949,7 +991,7 @@ function kitModule(options, auth, probe, config, state) {
|
|
|
949
991
|
ConformanceAppController = require_decorate.__decorate([(0, _nestjs_common.Controller)()], ConformanceAppController);
|
|
950
992
|
let ConformanceHookProvider = class ConformanceHookProvider {
|
|
951
993
|
record(_context) {
|
|
952
|
-
|
|
994
|
+
hooks?.push("/probe/html");
|
|
953
995
|
}
|
|
954
996
|
};
|
|
955
997
|
require_decorate.__decorate([
|
|
@@ -976,6 +1018,9 @@ function kitModule(options, auth, probe, config, state) {
|
|
|
976
1018
|
method: _nestjs_common.RequestMethod.ALL
|
|
977
1019
|
});
|
|
978
1020
|
}
|
|
1021
|
+
async onApplicationShutdown() {
|
|
1022
|
+
await config.onShutdown?.();
|
|
1023
|
+
}
|
|
979
1024
|
};
|
|
980
1025
|
ConformanceHttpModule = require_decorate.__decorate([(0, _nestjs_common.Module)({
|
|
981
1026
|
imports,
|
|
@@ -988,7 +1033,7 @@ function kitModule(options, auth, probe, config, state) {
|
|
|
988
1033
|
return ConformanceHttpModule;
|
|
989
1034
|
}
|
|
990
1035
|
async function bootHttp(options, bootstrap, config) {
|
|
991
|
-
const auth = createConformanceAuth(config.auth);
|
|
1036
|
+
const auth = config.instance ?? createConformanceAuth(config.auth);
|
|
992
1037
|
const probe = await probeOf(auth);
|
|
993
1038
|
const logger = new CapturingLogger();
|
|
994
1039
|
const filtered = [];
|
|
@@ -1104,6 +1149,53 @@ async function waitFor(predicate, message, timeoutMs = 2e3) {
|
|
|
1104
1149
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1105
1150
|
}
|
|
1106
1151
|
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Runs `fn` with process.env.NODE_ENV set to `value`, then restores it. The kit's production-mode rows use it: the
|
|
1154
|
+
* library treats every NODE_ENV except development, dev and test as production (§5.4). Run kit cases sequentially.
|
|
1155
|
+
*/
|
|
1156
|
+
async function withNodeEnv(value, fn) {
|
|
1157
|
+
const previous = process.env.NODE_ENV;
|
|
1158
|
+
process.env.NODE_ENV = value;
|
|
1159
|
+
try {
|
|
1160
|
+
return await fn();
|
|
1161
|
+
} finally {
|
|
1162
|
+
if (previous === void 0) Reflect.deleteProperty(process.env, "NODE_ENV");
|
|
1163
|
+
else process.env.NODE_ENV = previous;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const inProduction = (fn) => withNodeEnv("production", fn);
|
|
1167
|
+
/** The per-IP rule the rate-limit rows apply to /probe/ip, counted in a per-boot store that records every key. */
|
|
1168
|
+
const IP_RULE = {
|
|
1169
|
+
window: 60,
|
|
1170
|
+
max: 2
|
|
1171
|
+
};
|
|
1172
|
+
function rateLimited(keys) {
|
|
1173
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1174
|
+
return { auth: { rateLimit: {
|
|
1175
|
+
enabled: true,
|
|
1176
|
+
customRules: { "/probe/ip": IP_RULE },
|
|
1177
|
+
customStorage: { async consume(key, rule) {
|
|
1178
|
+
keys.push(key);
|
|
1179
|
+
const count = (counts.get(key) ?? 0) + 1;
|
|
1180
|
+
counts.set(key, count);
|
|
1181
|
+
return count > rule.max ? {
|
|
1182
|
+
allowed: false,
|
|
1183
|
+
retryAfter: rule.window
|
|
1184
|
+
} : {
|
|
1185
|
+
allowed: true,
|
|
1186
|
+
retryAfter: null
|
|
1187
|
+
};
|
|
1188
|
+
} }
|
|
1189
|
+
} } };
|
|
1190
|
+
}
|
|
1191
|
+
/** Better Auth trustedProxies covering every loopback address the kit's requests come from. */
|
|
1192
|
+
const loopbackProxies = { auth: { advanced: { ipAddress: { trustedProxies: ["127.0.0.0/8", "::1/128"] } } } };
|
|
1193
|
+
function warningsWith(boot, ...parts) {
|
|
1194
|
+
return boot.logger.entries.filter((entry) => entry.level === "warn" && parts.every((part) => entry.text.includes(part))).map((entry) => entry.text);
|
|
1195
|
+
}
|
|
1196
|
+
function http2Origin(boot) {
|
|
1197
|
+
return boot.url.replace(/^https?:/, "http:");
|
|
1198
|
+
}
|
|
1107
1199
|
function http2Request(origin, headers, body) {
|
|
1108
1200
|
return new Promise((resolve, reject) => {
|
|
1109
1201
|
const session = (0, node_http2.connect)(origin);
|
|
@@ -1146,7 +1238,9 @@ const multipartBody = [
|
|
|
1146
1238
|
* Auth instance that includes conformanceProbePlugin(), an app controller, a @BeforeAuth() hook provider, a recording
|
|
1147
1239
|
* global filter and an `around` recorder. HTTP/2 is required when the platform declares capabilities.http2; a platform
|
|
1148
1240
|
* without the capability, or without the optional proxyTrust() or trustOneProxy, gets a skip with the reason: before
|
|
1149
|
-
* the run for platform objects, at run time for classes and definitions.
|
|
1241
|
+
* the run for platform objects, at run time for classes and definitions. The microservice rows of H-no-adapter need the
|
|
1242
|
+
* `microservice` option. Production-mode rows set process.env.NODE_ENV to 'production' while they run and restore it,
|
|
1243
|
+
* so run the cases sequentially.
|
|
1150
1244
|
*/
|
|
1151
1245
|
function httpPlatformConformance(options) {
|
|
1152
1246
|
const bootstraps = options.bootstrap ?? ["factory", "testing"];
|
|
@@ -1154,6 +1248,14 @@ function httpPlatformConformance(options) {
|
|
|
1154
1248
|
const add = (id, title, run, only) => {
|
|
1155
1249
|
for (const bootstrap of bootstraps) cases.push(conformanceCase(id, `${title} [${bootstrap}]`, () => run(options, bootstrap), only?.(bootstrap)));
|
|
1156
1250
|
};
|
|
1251
|
+
const noHttp2 = () => declaredCapabilities(options.platform) !== void 0 && !declaredCapabilities(options.platform)?.http2 ? "the platform declares no http2 capability" : void 0;
|
|
1252
|
+
/** A skip when the resolved platform declares no http2 capability; requires the http2 option when it does. */
|
|
1253
|
+
const http2Ready = async (o) => {
|
|
1254
|
+
if (!(await capabilitiesOf(o))?.http2) return conformanceSkip("the platform declares no http2 capability");
|
|
1255
|
+
node_assert_strict.default.ok(o.http2, "the platform declares capabilities.http2, so the kit requires the http2 option");
|
|
1256
|
+
};
|
|
1257
|
+
const noProxyTrust = () => staticMember$1(options.platform, "proxyTrust") === false ? "the platform does not implement the optional proxyTrust()" : void 0;
|
|
1258
|
+
const noTrustHook = () => options.trustOneProxy ? void 0 : "no trustOneProxy option was given";
|
|
1157
1259
|
add("H-mount-methods", "every method under the base path reaches Better Auth", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
1158
1260
|
for (const method of [
|
|
1159
1261
|
"GET",
|
|
@@ -1203,6 +1305,53 @@ function httpPlatformConformance(options) {
|
|
|
1203
1305
|
node_assert_strict.default.equal(response.status, 200);
|
|
1204
1306
|
});
|
|
1205
1307
|
});
|
|
1308
|
+
const dynamicHosts = [new URL(KIT_BASE_URL).host, "tenant.example"];
|
|
1309
|
+
const dynamicMount = {
|
|
1310
|
+
auth: {
|
|
1311
|
+
baseURL: {
|
|
1312
|
+
allowedHosts: dynamicHosts,
|
|
1313
|
+
fallback: KIT_BASE_URL
|
|
1314
|
+
},
|
|
1315
|
+
basePath: DYNAMIC_AUTH
|
|
1316
|
+
},
|
|
1317
|
+
prefix: "v1",
|
|
1318
|
+
versioning: true
|
|
1319
|
+
};
|
|
1320
|
+
const assertDynamicUrl = (value, host) => {
|
|
1321
|
+
const url = new URL(String(value));
|
|
1322
|
+
node_assert_strict.default.equal(url.host, host, `the request URL lost the allowed host ${host}`);
|
|
1323
|
+
node_assert_strict.default.equal(url.pathname, `${DYNAMIC_AUTH}/probe/url`);
|
|
1324
|
+
};
|
|
1325
|
+
add("H-mount-custom-path", "a dynamic allowedHosts baseURL mounts at basePath for every allowed host, unaffected by a global prefix and URI versioning", (o, b) => withBoot(o, b, dynamicMount, async (boot) => {
|
|
1326
|
+
for (const host of dynamicHosts) {
|
|
1327
|
+
const response = await sendRaw(`${boot.url}${DYNAMIC_AUTH}/probe/url`, { headers: { host } });
|
|
1328
|
+
node_assert_strict.default.equal(response.status, 200, `${host}: ${response.body.toString("utf8").slice(0, 200)}`);
|
|
1329
|
+
assertDynamicUrl(json(response).url, host);
|
|
1330
|
+
}
|
|
1331
|
+
const ping = await sendRaw(`${boot.url}/v1/app/ping`);
|
|
1332
|
+
node_assert_strict.default.equal(ping.status, 200);
|
|
1333
|
+
const prefixed = await sendRaw(`${boot.url}/v1${DYNAMIC_AUTH}/probe/html`);
|
|
1334
|
+
node_assert_strict.default.notEqual(prefixed.status, 200);
|
|
1335
|
+
const fallthrough = await sendRaw(`${boot.url}${AUTH}/probe/html`);
|
|
1336
|
+
node_assert_strict.default.equal(fallthrough.status, 404);
|
|
1337
|
+
node_assert_strict.default.ok(!boot.around.some((entry) => entry.includes(AUTH)), `the default base path reached Better Auth: ${boot.around.join(", ")}`);
|
|
1338
|
+
}));
|
|
1339
|
+
add("H-mount-custom-path", "over HTTP/2, a dynamic allowedHosts baseURL serves the :authority host at basePath", async (o, b) => {
|
|
1340
|
+
const skip = await http2Ready(o);
|
|
1341
|
+
if (skip) return skip;
|
|
1342
|
+
await withBoot(o, b, {
|
|
1343
|
+
...dynamicMount,
|
|
1344
|
+
http2: true
|
|
1345
|
+
}, async (boot) => {
|
|
1346
|
+
const response = await http2Request(http2Origin(boot), {
|
|
1347
|
+
":method": "GET",
|
|
1348
|
+
":path": `${DYNAMIC_AUTH}/probe/url`,
|
|
1349
|
+
":authority": "tenant.example"
|
|
1350
|
+
});
|
|
1351
|
+
node_assert_strict.default.equal(response.status, 200, response.body.toString("utf8"));
|
|
1352
|
+
assertDynamicUrl(JSON.parse(response.body.toString("utf8")).url, "tenant.example");
|
|
1353
|
+
});
|
|
1354
|
+
}, noHttp2);
|
|
1206
1355
|
add("H-app-route-precedence", "a controller route under the base path is served by Nest", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
1207
1356
|
const response = await sendRaw(`${boot.url}${AUTH}/app-owned`);
|
|
1208
1357
|
node_assert_strict.default.equal(response.status, 200);
|
|
@@ -1410,10 +1559,87 @@ function httpPlatformConformance(options) {
|
|
|
1410
1559
|
});
|
|
1411
1560
|
for (const [name, value] of Object.entries(headers)) node_assert_strict.default.equal(result.headers.get(name), value, `${name} changed`);
|
|
1412
1561
|
}));
|
|
1562
|
+
const probeIp = async (boot, headers = {}) => {
|
|
1563
|
+
const response = await sendRaw(`${boot.url}${AUTH}/probe/ip`, { headers });
|
|
1564
|
+
return response.status === 200 ? {
|
|
1565
|
+
status: response.status,
|
|
1566
|
+
...json(response)
|
|
1567
|
+
} : { status: response.status };
|
|
1568
|
+
};
|
|
1413
1569
|
add("H-ip-platform", "Better Auth's getIP sees the platform client IP", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
1414
|
-
const
|
|
1415
|
-
node_assert_strict.default.equal(
|
|
1570
|
+
const value = await probeIp(boot);
|
|
1571
|
+
node_assert_strict.default.equal(value.resolved, LOOPBACK, "Better Auth resolved no client IP from the request headers the platform set");
|
|
1572
|
+
node_assert_strict.default.equal(value.ip, LOOPBACK);
|
|
1416
1573
|
}));
|
|
1574
|
+
add("H-ip-platform", "over HTTP/2, Better Auth's getIP sees the platform client IP", async (o, b) => {
|
|
1575
|
+
const skip = await http2Ready(o);
|
|
1576
|
+
if (skip) return skip;
|
|
1577
|
+
await withBoot(o, b, { http2: true }, async (boot) => {
|
|
1578
|
+
const response = await http2Request(http2Origin(boot), {
|
|
1579
|
+
":method": "GET",
|
|
1580
|
+
":path": `${AUTH}/probe/ip`
|
|
1581
|
+
});
|
|
1582
|
+
node_assert_strict.default.equal(response.status, 200, response.body.toString("utf8"));
|
|
1583
|
+
const value = JSON.parse(response.body.toString("utf8"));
|
|
1584
|
+
node_assert_strict.default.equal(value.resolved, LOOPBACK, "Better Auth resolved no client IP over HTTP/2");
|
|
1585
|
+
});
|
|
1586
|
+
}, noHttp2);
|
|
1587
|
+
const spoofedIp = {
|
|
1588
|
+
"x-forwarded-for": "203.0.113.20",
|
|
1589
|
+
"x-real-ip": "203.0.113.21",
|
|
1590
|
+
[`x-nsba-ip-${"0".repeat(32)}`]: "203.0.113.22"
|
|
1591
|
+
};
|
|
1592
|
+
add("H-ip-platform", "in production mode, the platform IP keys a per-IP rate-limit bucket that client-sent IP headers cannot leave", (o, b) => {
|
|
1593
|
+
const keys = [];
|
|
1594
|
+
return inProduction(() => withBoot(o, b, rateLimited(keys), async (boot) => {
|
|
1595
|
+
const first = await probeIp(boot);
|
|
1596
|
+
node_assert_strict.default.equal(first.resolved, LOOPBACK);
|
|
1597
|
+
const statuses = [first.status];
|
|
1598
|
+
for (let index = 1; index <= IP_RULE.max; index++) statuses.push((await probeIp(boot)).status);
|
|
1599
|
+
node_assert_strict.default.deepEqual(statuses, [
|
|
1600
|
+
200,
|
|
1601
|
+
200,
|
|
1602
|
+
429
|
|
1603
|
+
], `the per-IP rule (max ${IP_RULE.max}) answered ${statuses.join(", ")}`);
|
|
1604
|
+
node_assert_strict.default.equal((await probeIp(boot, spoofedIp)).status, 429, "client-sent IP headers moved the request out of the platform IP's bucket");
|
|
1605
|
+
node_assert_strict.default.deepEqual([...new Set(keys)], [`${LOOPBACK}|/probe/ip`], "the rate limiter was not keyed by the platform IP");
|
|
1606
|
+
}));
|
|
1607
|
+
});
|
|
1608
|
+
add("H-ip-platform", "in production mode behind a trusted hop, every forwarded client gets its own rate-limit bucket", (o, b) => {
|
|
1609
|
+
const keys = [];
|
|
1610
|
+
const [left, right] = ["203.0.113.30", "203.0.113.31"];
|
|
1611
|
+
return inProduction(() => withBoot(o, b, {
|
|
1612
|
+
...rateLimited(keys),
|
|
1613
|
+
trustProxy: true
|
|
1614
|
+
}, async (boot) => {
|
|
1615
|
+
const statuses = [];
|
|
1616
|
+
for (let index = 0; index <= IP_RULE.max; index++) statuses.push((await probeIp(boot, { "x-forwarded-for": left })).status);
|
|
1617
|
+
node_assert_strict.default.deepEqual(statuses, [
|
|
1618
|
+
200,
|
|
1619
|
+
200,
|
|
1620
|
+
429
|
|
1621
|
+
]);
|
|
1622
|
+
const other = await probeIp(boot, { "x-forwarded-for": right });
|
|
1623
|
+
node_assert_strict.default.equal(other.status, 200, "a second client shared the first client's bucket");
|
|
1624
|
+
node_assert_strict.default.equal(other.resolved, right);
|
|
1625
|
+
node_assert_strict.default.deepEqual([...new Set(keys)].sort(), [`${left}|/probe/ip`, `${right}|/probe/ip`]);
|
|
1626
|
+
}));
|
|
1627
|
+
}, noTrustHook);
|
|
1628
|
+
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) => {
|
|
1629
|
+
const value = await probeIp(boot, { "x-forwarded-for": "203.0.113.40" });
|
|
1630
|
+
node_assert_strict.default.equal(value.resolved, null, `Better Auth resolved ${String(value.resolved)} although trustedProxies covers the socket IP`);
|
|
1631
|
+
const trust = resolvedPlatform(boot.app).proxyTrust?.(boot.app.getHttpAdapter());
|
|
1632
|
+
const b18 = warningsWith(boot, "W_CLIENT_IP", "trustedProxies");
|
|
1633
|
+
if (trust?.mode === "none") node_assert_strict.default.equal(b18.length, 1, boot.logger.text());
|
|
1634
|
+
})));
|
|
1635
|
+
add("H-ip-platform", "with trustedProxies covering the proxy and trust on, Better Auth resolves the client IP without a B18 warning", (o, b) => inProduction(() => withBoot(o, b, {
|
|
1636
|
+
...loopbackProxies,
|
|
1637
|
+
trustProxy: true
|
|
1638
|
+
}, async (boot) => {
|
|
1639
|
+
const value = await probeIp(boot, { "x-forwarded-for": "203.0.113.41" });
|
|
1640
|
+
node_assert_strict.default.equal(value.resolved, "203.0.113.41");
|
|
1641
|
+
node_assert_strict.default.deepEqual(warningsWith(boot, "W_CLIENT_IP", "trustedProxies"), []);
|
|
1642
|
+
})), noTrustHook);
|
|
1417
1643
|
add("H-ip-spoof", "client-sent client-IP headers are ignored", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
1418
1644
|
const ip = json(await sendRaw(`${boot.url}${AUTH}/probe/ip`, { headers: {
|
|
1419
1645
|
"x-forwarded-for": "203.0.113.7",
|
|
@@ -1461,10 +1687,10 @@ function httpPlatformConformance(options) {
|
|
|
1461
1687
|
node_assert_strict.default.equal((await probedUrl(boot)).origin, "https://proxy.example");
|
|
1462
1688
|
}), () => options.trustOneProxy ? void 0 : "no trustOneProxy option was given");
|
|
1463
1689
|
add("H-h2-pseudo", "HTTP/2 pseudo-headers never reach Better Auth", async (o, b) => {
|
|
1464
|
-
|
|
1465
|
-
|
|
1690
|
+
const skip = await http2Ready(o);
|
|
1691
|
+
if (skip) return skip;
|
|
1466
1692
|
await withBoot(o, b, { http2: true }, async (boot) => {
|
|
1467
|
-
const response = await http2Request(boot
|
|
1693
|
+
const response = await http2Request(http2Origin(boot), {
|
|
1468
1694
|
":method": "POST",
|
|
1469
1695
|
":path": `${AUTH}/probe/echo-raw`,
|
|
1470
1696
|
origin: KIT_BASE_URL,
|
|
@@ -1475,7 +1701,7 @@ function httpPlatformConformance(options) {
|
|
|
1475
1701
|
node_assert_strict.default.equal(Buffer.from(value.body, "base64").toString(), "h2 body");
|
|
1476
1702
|
node_assert_strict.default.ok(value.headers.every(([name]) => !name.startsWith(":")), "pseudo-headers reached Better Auth");
|
|
1477
1703
|
});
|
|
1478
|
-
},
|
|
1704
|
+
}, noHttp2);
|
|
1479
1705
|
add("H-resp-multicookie", "three Set-Cookie values arrive on separate lines", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
1480
1706
|
const lines = setCookieLines((await sendRaw(`${boot.url}${AUTH}/probe/multi-cookie`)).headers);
|
|
1481
1707
|
node_assert_strict.default.equal(lines.length, 3, `Set-Cookie lines: ${lines.join(" | ")}`);
|
|
@@ -1640,6 +1866,39 @@ function httpPlatformConformance(options) {
|
|
|
1640
1866
|
await moduleRef.close();
|
|
1641
1867
|
node_assert_strict.default.equal(bridge.state, "closed");
|
|
1642
1868
|
});
|
|
1869
|
+
add("H-no-adapter", "a microservice boots once without an HTTP adapter, with listen() and with init() then listen()", async (o, b) => {
|
|
1870
|
+
for (const initFirst of [false, true]) {
|
|
1871
|
+
const label = `${b === "factory" ? "NestFactory.createMicroservice()" : "Test.createNestMicroservice()"} with ${initFirst ? "init() then listen()" : "listen()"}`;
|
|
1872
|
+
const auth = createConformanceAuth();
|
|
1873
|
+
const bridge = await bridgeOf(auth);
|
|
1874
|
+
const logger = new CapturingLogger();
|
|
1875
|
+
const module = kitModule(o, auth, await probeOf(auth), { module: { logSummary: true } }, {
|
|
1876
|
+
filtered: [],
|
|
1877
|
+
around: [],
|
|
1878
|
+
accessor: {},
|
|
1879
|
+
requests: () => node_assert_strict.default.fail("no HTTP platform expected"),
|
|
1880
|
+
adapter: () => node_assert_strict.default.fail("no HTTP adapter expected")
|
|
1881
|
+
});
|
|
1882
|
+
const microserviceOptions = {
|
|
1883
|
+
...o.microservice(),
|
|
1884
|
+
logger,
|
|
1885
|
+
abortOnError: false
|
|
1886
|
+
};
|
|
1887
|
+
const microservice = b === "factory" ? await _nestjs_core.NestFactory.createMicroservice(module, microserviceOptions) : (await _nestjs_testing.Test.createTestingModule({ imports: [module] }).compile()).createNestMicroservice(microserviceOptions);
|
|
1888
|
+
try {
|
|
1889
|
+
if (initFirst) await microservice.init();
|
|
1890
|
+
await microservice.listen();
|
|
1891
|
+
node_assert_strict.default.equal(bridge.state, "bound", `${label}: the instance is not bound`);
|
|
1892
|
+
const summaries = logger.entries.filter((entry) => entry.text.startsWith("'default': "));
|
|
1893
|
+
node_assert_strict.default.equal(summaries.length, 1, `${label}: the lifecycle ran ${summaries.length} times\n${logger.text()}`);
|
|
1894
|
+
node_assert_strict.default.match(summaries[0].text, /not mounted/, label);
|
|
1895
|
+
node_assert_strict.default.deepEqual(logger.errors(), [], `${label}: ${logger.text()}`);
|
|
1896
|
+
} finally {
|
|
1897
|
+
await microservice.close();
|
|
1898
|
+
}
|
|
1899
|
+
node_assert_strict.default.equal(bridge.state, "closed", `${label}: close() left the binding ${bridge.state}`);
|
|
1900
|
+
}
|
|
1901
|
+
}, () => options.microservice ? void 0 : "no microservice option was given");
|
|
1643
1902
|
add("H-late-adapter", "module init followed by an application init fails with LATE_HTTP_ADAPTER", async (o) => {
|
|
1644
1903
|
const auth = createConformanceAuth();
|
|
1645
1904
|
const module = kitModule(o, auth, await probeOf(auth), {}, {
|
|
@@ -1713,6 +1972,46 @@ function httpPlatformConformance(options) {
|
|
|
1713
1972
|
}
|
|
1714
1973
|
node_assert_strict.default.ok(bootIssueCodes(result.error).includes("DUPLICATE_INSTANCE"), String(result.error));
|
|
1715
1974
|
});
|
|
1975
|
+
add("H-close-keeps-hooks", "app.close() closes the binding without unbinding its hooks, and the next application takes it over silently", async (o, b) => {
|
|
1976
|
+
const auth = createConformanceAuth();
|
|
1977
|
+
const bridge = await bridgeOf(auth);
|
|
1978
|
+
const direct = async () => (await auth.handler(new Request(`${KIT_BASE_URL}${AUTH}/probe/html`))).status;
|
|
1979
|
+
const closedHooks = [];
|
|
1980
|
+
let duringShutdown;
|
|
1981
|
+
const first = await bootHttp(o, b, {
|
|
1982
|
+
instance: auth,
|
|
1983
|
+
hooks: closedHooks,
|
|
1984
|
+
onShutdown: async () => {
|
|
1985
|
+
duringShutdown = await direct();
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
try {
|
|
1989
|
+
const served = await sendRaw(`${first.url}${AUTH}/probe/html`);
|
|
1990
|
+
node_assert_strict.default.equal(served.status, 200);
|
|
1991
|
+
node_assert_strict.default.equal(closedHooks.length, 1, "the @BeforeAuth() hook did not run");
|
|
1992
|
+
} finally {
|
|
1993
|
+
await first.close();
|
|
1994
|
+
}
|
|
1995
|
+
const warns = (boot) => boot.logger.entries.filter((entry) => entry.level === "warn").map((entry) => entry.text);
|
|
1996
|
+
const expected = new Set(warns(first));
|
|
1997
|
+
node_assert_strict.default.equal(duringShutdown, 200);
|
|
1998
|
+
node_assert_strict.default.equal(closedHooks.length, 2, "a Better Auth call from another module's shutdown hook skipped the application's @BeforeAuth() hook");
|
|
1999
|
+
node_assert_strict.default.equal(bridge.state, "closed", `app.close() left the binding ${bridge.state}`);
|
|
2000
|
+
node_assert_strict.default.equal(await direct(), 200);
|
|
2001
|
+
node_assert_strict.default.equal(closedHooks.length, 3, "a Better Auth call after app.close() skipped the closed application's @BeforeAuth() hook");
|
|
2002
|
+
const nextHooks = [];
|
|
2003
|
+
await withBoot(o, b, {
|
|
2004
|
+
instance: auth,
|
|
2005
|
+
hooks: nextHooks
|
|
2006
|
+
}, async (next) => {
|
|
2007
|
+
node_assert_strict.default.equal(bridge.state, "bound");
|
|
2008
|
+
node_assert_strict.default.deepEqual(warns(next).filter((text) => !expected.has(text)), [], "the next application warned while taking over the closed binding");
|
|
2009
|
+
const served = await sendRaw(`${next.url}${AUTH}/probe/html`);
|
|
2010
|
+
node_assert_strict.default.equal(served.status, 200);
|
|
2011
|
+
node_assert_strict.default.equal(nextHooks.length, 1, "the next application's hook did not run");
|
|
2012
|
+
node_assert_strict.default.equal(closedHooks.length, 3, "the closed application's hook still ran after the next application bound");
|
|
2013
|
+
});
|
|
2014
|
+
});
|
|
1716
2015
|
add("H-diagnostics-404", "an unknown auth path logs one WARN with a suggestion and keeps the 404", (o, b) => withBoot(o, b, { module: { http: { diagnostics: true } } }, async (boot) => {
|
|
1717
2016
|
for (let index = 0; index < 2; index++) {
|
|
1718
2017
|
const response = await sendRaw(`${boot.url}${AUTH}/sign-inn/email`, {
|
|
@@ -1729,6 +2028,43 @@ function httpPlatformConformance(options) {
|
|
|
1729
2028
|
node_assert_strict.default.equal(warnings.length, 1, boot.logger.text());
|
|
1730
2029
|
node_assert_strict.default.match(warnings[0].text, /Did you mean/);
|
|
1731
2030
|
}));
|
|
2031
|
+
const proxyWarnings = (boot) => warningsWith(boot, "W_PROXY_UNTRUSTED");
|
|
2032
|
+
add("H-proxy-untrusted-warning", "in production with proxy trust off, the first forwarded auth request logs W_PROXY_UNTRUSTED once and changes no response", (o, b) => inProduction(() => withBoot(o, b, {}, async (boot) => {
|
|
2033
|
+
const platform = resolvedPlatform(boot.app);
|
|
2034
|
+
if (typeof platform.proxyTrust !== "function") return conformanceSkip("the platform does not implement the optional proxyTrust()");
|
|
2035
|
+
const trust = platform.proxyTrust(boot.app.getHttpAdapter());
|
|
2036
|
+
if (trust.mode !== "none") return conformanceSkip(`a fresh adapter reports proxy trust '${trust.mode}', not 'none'`);
|
|
2037
|
+
const html = async (headers = {}) => {
|
|
2038
|
+
const response = await sendRaw(`${boot.url}${AUTH}/probe/html`, { headers });
|
|
2039
|
+
node_assert_strict.default.equal(response.status, 200);
|
|
2040
|
+
node_assert_strict.default.equal(response.body.toString("utf8"), "<p>probe</p>");
|
|
2041
|
+
};
|
|
2042
|
+
await html();
|
|
2043
|
+
node_assert_strict.default.deepEqual(proxyWarnings(boot), [], "a request without forwarding headers logged");
|
|
2044
|
+
const ping = await sendRaw(`${boot.url}/app/ping`, { headers: { "x-forwarded-for": "203.0.113.50" } });
|
|
2045
|
+
node_assert_strict.default.equal(ping.status, 200);
|
|
2046
|
+
node_assert_strict.default.deepEqual(proxyWarnings(boot), [], "an app route logged");
|
|
2047
|
+
await html({ "x-forwarded-for": "203.0.113.51" });
|
|
2048
|
+
const [warning, ...repeated] = proxyWarnings(boot);
|
|
2049
|
+
node_assert_strict.default.ok(warning, "a forwarded auth request logged no W_PROXY_UNTRUSTED");
|
|
2050
|
+
node_assert_strict.default.ok(warning.includes(`socket IP ${LOOPBACK}.`), `the warning did not name the platform's socket IP: ${warning}`);
|
|
2051
|
+
node_assert_strict.default.ok(!warning.includes("203.0.113.51"), "the warning named a client-sent address as the socket IP");
|
|
2052
|
+
node_assert_strict.default.deepEqual(repeated, []);
|
|
2053
|
+
await html({ forwarded: "for=203.0.113.52" });
|
|
2054
|
+
await html({ "x-real-ip": "203.0.113.53" });
|
|
2055
|
+
node_assert_strict.default.equal(proxyWarnings(boot).length, 1, "W_PROXY_UNTRUSTED was logged more than once per application");
|
|
2056
|
+
})), noProxyTrust);
|
|
2057
|
+
add("H-proxy-untrusted-warning", "outside production, forwarded auth requests log no W_PROXY_UNTRUSTED", (o, b) => withNodeEnv("test", () => withBoot(o, b, {}, async (boot) => {
|
|
2058
|
+
await sendRaw(`${boot.url}${AUTH}/probe/html`, { headers: { "x-forwarded-for": "203.0.113.54" } });
|
|
2059
|
+
node_assert_strict.default.deepEqual(proxyWarnings(boot), []);
|
|
2060
|
+
})), noProxyTrust);
|
|
2061
|
+
add("H-proxy-untrusted-warning", "in production behind a trusted hop, forwarded auth requests log no W_PROXY_UNTRUSTED", async (o, b) => {
|
|
2062
|
+
if (!await implementsMember(o, "proxyTrust")) return conformanceSkip("the platform does not implement the optional proxyTrust()");
|
|
2063
|
+
return inProduction(() => withBoot(o, b, { trustProxy: true }, async (boot) => {
|
|
2064
|
+
await sendRaw(`${boot.url}${AUTH}/probe/html`, { headers: { "x-forwarded-for": "203.0.113.55" } });
|
|
2065
|
+
node_assert_strict.default.deepEqual(proxyWarnings(boot), []);
|
|
2066
|
+
}));
|
|
2067
|
+
}, () => noProxyTrust() ?? noTrustHook());
|
|
1732
2068
|
add("H-log-hygiene", "session and probe cookies never appear in captured logs", (o, b) => withBoot(o, b, {}, async (boot) => {
|
|
1733
2069
|
const identity = await kitIdentity(boot.auth);
|
|
1734
2070
|
const token = identity.cookie.split("=")[1].split(".")[0];
|