nestjs-slightly-better-auth 1.0.0 → 1.1.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 +75 -3
- package/dist/microservices.cjs +252 -0
- package/dist/microservices.cjs.map +1 -0
- package/dist/microservices.d.cts +39 -0
- package/dist/microservices.d.cts.map +1 -0
- package/dist/microservices.d.mts +39 -0
- package/dist/microservices.d.mts.map +1 -0
- package/dist/microservices.mjs +222 -0
- package/dist/microservices.mjs.map +1 -0
- package/package.json +35 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
A NestJS integration for [Better Auth](https://www.better-auth.com), built on an independently reviewed specification.
|
|
8
8
|
|
|
9
|
-
> **Scope
|
|
9
|
+
> **Scope.** The package covers HTTP and RPC. HTTP support comprises the Nest authentication kernel, the Better Auth construction plugin, the Express and Fastify platforms, and the admin, organization and API-key authorization units. The `./microservices` entry authenticates Nest microservice messages. GraphQL and WebSocket transports and the conformance testing kit are not included; they arrive in later minor versions behind their own entry points. The [changelog](CHANGELOG.md) records the version that introduces each entry point.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -16,9 +16,17 @@ pnpm add nestjs-slightly-better-auth better-auth
|
|
|
16
16
|
|
|
17
17
|
Peer dependencies: `@nestjs/common` and `@nestjs/core` 12, `better-auth` 1.7.5 or newer, `reflect-metadata` and `rxjs`. Node.js 24.11 or newer.
|
|
18
18
|
|
|
19
|
+
The `./microservices` entry also needs the optional peer `@nestjs/microservices` 12 and the client library Nest uses for your transport, for example:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add @nestjs/microservices @nats-io/transport-node
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Nest's gRPC transport uses `@grpc/grpc-js` and `@grpc/proto-loader`, Kafka uses `kafkajs`, RabbitMQ uses `amqplib` and `amqp-connection-manager`, MQTT uses `mqtt`, and Redis uses `ioredis`. TCP needs no client library. Applications that do not import `./microservices` do not need any of these packages.
|
|
26
|
+
|
|
19
27
|
## What this release covers
|
|
20
28
|
|
|
21
|
-
The library separates a Better Auth construction plugin, a NestJS integration kernel and optional transports. The `./plugin` entry installs the hook and cookie bridge before Better Auth creates its pipeline. The `./platform` entry provides Node/Web request and response helpers. Neither entry requires Express, Fastify, GraphQL or
|
|
29
|
+
The library separates a Better Auth construction plugin, a NestJS integration kernel and optional transports. The `./plugin` entry installs the hook and cookie bridge before Better Auth creates its pipeline. The `./platform` entry provides Node/Web request and response helpers. Neither entry requires Express, Fastify, GraphQL, WebSocket or microservice packages.
|
|
22
30
|
|
|
23
31
|
The root entry provides synchronous and asynchronous module registration, default and named instances, service readers, guards, scoped execution and compositional authorization. Tests exercise actual Nest dependency injection and Better Auth sessions, including isolation between instances, caller-origin enforcement and application shutdown. Built ESM and CommonJS consumers verify the same registration and type contracts.
|
|
24
32
|
|
|
@@ -28,10 +36,74 @@ Express preserves native controller parsing for unconditional routes that overla
|
|
|
28
36
|
|
|
29
37
|
Fastify does not expose its configured `trustProxy` value through a public inspection API. The boot summary therefore reports proxy trust as `unknown`, and diagnostics that depend on the setting are unavailable. Client IP resolution still uses the native Fastify request. Configure proxy trust on your Nest Fastify adapter and verify it against your deployment topology.
|
|
30
38
|
|
|
31
|
-
GraphQL
|
|
39
|
+
GraphQL and WebSocket integrations have separate implementation and end-to-end acceptance gates. They will use optional entry points so applications install only the transports they use.
|
|
32
40
|
|
|
33
41
|
Start with the [design workspace](docs/design/README.md), [reviewed specification](docs/design/design-v7.md), [review ledger](docs/design/ledger.md) and [implementation plan](../../docs/superpowers/plans/2026-09-21-better-auth.md). Independent Better Auth, NestJS and security reviews approved the final v7 snapshot after resolving the round-6 and round-7 findings. The remaining entry points are tracked in [issue #534](https://github.com/thilllon/nestjs-kit/issues/534).
|
|
34
42
|
|
|
43
|
+
## RPC authentication
|
|
44
|
+
|
|
45
|
+
The `./microservices` entry authenticates messages in standalone and hybrid Nest microservices. `rpcTransport()` reads credentials from the transport's native carrier: gRPC metadata, NATS headers, Kafka headers, RabbitMQ message headers and MQTT 5 user properties. TCP and Redis messages have no header carrier, so they send credentials in a payload envelope field, `auth` by default. Every message is authenticated separately.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { Controller, Module } from "@nestjs/common";
|
|
49
|
+
import { NestFactory } from "@nestjs/core";
|
|
50
|
+
import {
|
|
51
|
+
MessagePattern,
|
|
52
|
+
Transport,
|
|
53
|
+
type MicroserviceOptions,
|
|
54
|
+
} from "@nestjs/microservices";
|
|
55
|
+
import { betterAuth } from "better-auth";
|
|
56
|
+
import { bearer } from "better-auth/plugins";
|
|
57
|
+
import {
|
|
58
|
+
BetterAuthModule,
|
|
59
|
+
CurrentUser,
|
|
60
|
+
Public,
|
|
61
|
+
} from "nestjs-slightly-better-auth";
|
|
62
|
+
import { rpcTransport } from "nestjs-slightly-better-auth/microservices";
|
|
63
|
+
import { nestjs } from "nestjs-slightly-better-auth/plugin";
|
|
64
|
+
|
|
65
|
+
const auth = betterAuth({
|
|
66
|
+
// RPC messages have no HTTP origin, so the base URL must be static.
|
|
67
|
+
baseURL: "https://auth.example.com",
|
|
68
|
+
// Add your database adapter and other Better Auth options.
|
|
69
|
+
plugins: [bearer(), nestjs()],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
@Controller()
|
|
73
|
+
class ProfileController {
|
|
74
|
+
@MessagePattern("profile.get")
|
|
75
|
+
profile(@CurrentUser() user: { id: string }) {
|
|
76
|
+
return { userId: user.id };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Public()
|
|
80
|
+
@MessagePattern("health")
|
|
81
|
+
health() {
|
|
82
|
+
return { ok: true };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Module({
|
|
87
|
+
imports: [BetterAuthModule.forRoot({ auth, transports: [rpcTransport()] })],
|
|
88
|
+
controllers: [ProfileController],
|
|
89
|
+
})
|
|
90
|
+
class AppModule {}
|
|
91
|
+
|
|
92
|
+
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
|
|
93
|
+
AppModule,
|
|
94
|
+
{ transport: Transport.TCP, options: { host: "127.0.0.1", port: 4000 } },
|
|
95
|
+
);
|
|
96
|
+
await app.listen();
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A TCP client puts credentials in the envelope, for example `{ auth: { authorization: "Bearer <token>" }, id: 1 }`; a NATS client sets an `authorization` header through `NatsRecordBuilder`. Carriers accept only the `authorization`, `cookie` and `x-api-key` fields by default. Pass carrier factories such as `grpcCarrier({ metadata: ["authorization"] })` or `payloadCarrier({ field: "credentials" })` in `rpcTransport({ carriers })` to change the accepted fields or the envelope. Host, forwarding and `set-cookie` metadata never reach Better Auth, so a broker message cannot claim an HTTP origin.
|
|
100
|
+
|
|
101
|
+
Configure a static Better Auth base URL, or a fallback for a dynamic one: RPC transports have no request host, and startup fails with `DYNAMIC_BASE_URL_WITHOUT_FALLBACK` otherwise. Message authentication never refreshes or sets cookies. Public messages run without credentials; protected messages without a matching carrier are rejected as unauthenticated. Malformed credential values, such as values containing CR, LF or NUL, are rejected with reason `MALFORMED_CREDENTIALS` without reaching Better Auth, the handler or logs.
|
|
102
|
+
|
|
103
|
+
In a hybrid application, Nest applies global guards to microservice handlers only when `connectMicroservice()` receives `{ inheritAppConfig: true }`. Either pass that option to every `connectMicroservice()` call and pass `rpcTransport({ inheritAppConfig: true })`, or apply `@UseBetterAuth()` to message controllers and `@Public()` to public handlers. Startup verifies the explicit form and fails with `RPC_HANDLER_UNGUARDED` for an uncovered message or event handler; `hybridCoverage: "warn"` or `"off"` lowers that check. The inheritance option is an assertion that the application passes the same flag to Nest, and startup logs the `W_RPC_INHERIT_APP_CONFIG_ASSERTED` warning for it.
|
|
104
|
+
|
|
105
|
+
Failures become `RpcException` payloads with `statusCode`, `code` and, where present, `reason`. gRPC calls receive native status codes: 16 for authentication, 7 for authorization, 8 for throttling and 13 for redacted infrastructure failures. End-to-end tests run real TCP and gRPC servers and real NATS, Kafka, RabbitMQ, MQTT 5 and Redis brokers against Nest 12.0.3, including hybrid applications with both coverage modes and default plus named instances.
|
|
106
|
+
|
|
35
107
|
## Develop in this monorepo
|
|
36
108
|
|
|
37
109
|
From the repository root:
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
const require_auth_errors = require("./auth-errors-CsERBNWI.cjs");
|
|
25
|
+
let _nestjs_common = require("@nestjs/common");
|
|
26
|
+
let _nestjs_core = require("@nestjs/core");
|
|
27
|
+
let _nestjs_microservices = require("@nestjs/microservices");
|
|
28
|
+
_nestjs_microservices = __toESM(_nestjs_microservices, 1);
|
|
29
|
+
//#region src/rpc-carriers.ts
|
|
30
|
+
const credentialNames = [
|
|
31
|
+
"authorization",
|
|
32
|
+
"cookie",
|
|
33
|
+
"x-api-key"
|
|
34
|
+
];
|
|
35
|
+
function record(value) {
|
|
36
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
37
|
+
}
|
|
38
|
+
function method(value, name) {
|
|
39
|
+
const candidate = record(value)?.[name];
|
|
40
|
+
return typeof candidate === "function" ? candidate.bind(value) : void 0;
|
|
41
|
+
}
|
|
42
|
+
function contextOf(context) {
|
|
43
|
+
return context.switchToRpc().getContext();
|
|
44
|
+
}
|
|
45
|
+
function extract(input, allow) {
|
|
46
|
+
const headers = new Headers();
|
|
47
|
+
const allowed = new Set(allow.map((name) => name.toLowerCase()));
|
|
48
|
+
for (const name of [
|
|
49
|
+
"host",
|
|
50
|
+
"x-forwarded-host",
|
|
51
|
+
"x-forwarded-proto",
|
|
52
|
+
"set-cookie"
|
|
53
|
+
]) allowed.delete(name);
|
|
54
|
+
const source = record(input);
|
|
55
|
+
if (!source) return headers;
|
|
56
|
+
for (const name of Object.keys(source)) {
|
|
57
|
+
const normalized = name.toLowerCase();
|
|
58
|
+
if (!allowed.has(normalized)) continue;
|
|
59
|
+
const value = source[name];
|
|
60
|
+
const values = Array.isArray(value) ? value : [value];
|
|
61
|
+
if (values.length === 0) throw require_auth_errors.AuthFailures.rejected({
|
|
62
|
+
status: 401,
|
|
63
|
+
reason: "MALFORMED_CREDENTIALS"
|
|
64
|
+
});
|
|
65
|
+
for (const item of values) {
|
|
66
|
+
const text = typeof item === "string" ? item : Buffer.isBuffer(item) ? item.toString("utf8") : void 0;
|
|
67
|
+
if (text === void 0 || /[\r\n\0]/.test(text)) throw require_auth_errors.AuthFailures.rejected({
|
|
68
|
+
status: 401,
|
|
69
|
+
reason: "MALFORMED_CREDENTIALS"
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
headers.append(normalized, text);
|
|
73
|
+
} catch {
|
|
74
|
+
throw require_auth_errors.AuthFailures.rejected({
|
|
75
|
+
status: 401,
|
|
76
|
+
reason: "MALFORMED_CREDENTIALS"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return headers;
|
|
82
|
+
}
|
|
83
|
+
function grpcCarrier(options = {}) {
|
|
84
|
+
return {
|
|
85
|
+
id: "grpc",
|
|
86
|
+
matches: (context) => !!method(context.getArgByIndex(1), "get") && !!method(context.getArgByIndex(1), "getMap"),
|
|
87
|
+
headers: (context) => extract(method(context.getArgByIndex(1), "getMap")?.(), options.metadata ?? credentialNames),
|
|
88
|
+
toException(failure) {
|
|
89
|
+
const name = failure.status === 401 ? "GrpcUnauthenticatedException" : failure.status === 403 ? "GrpcPermissionDeniedException" : "GrpcResourceExhaustedException";
|
|
90
|
+
const Native = _nestjs_microservices[name];
|
|
91
|
+
return new _nestjs_microservices.RpcException(Native ? new Native(failure.message).getError() : {
|
|
92
|
+
code: failure.status === 401 ? 16 : failure.status === 403 ? 7 : 8,
|
|
93
|
+
message: failure.message
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function natsCarrier(options = {}) {
|
|
99
|
+
return {
|
|
100
|
+
id: "nats",
|
|
101
|
+
matches: (context) => !!method(contextOf(context), "getHeaders"),
|
|
102
|
+
headers(context) {
|
|
103
|
+
const input = method(contextOf(context), "getHeaders")?.();
|
|
104
|
+
const keys = method(input, "keys")?.();
|
|
105
|
+
const get = method(input, "get");
|
|
106
|
+
const values = Object.create(null);
|
|
107
|
+
if (Array.isArray(keys) && get) {
|
|
108
|
+
for (const key of keys) if (typeof key === "string") values[key] = get(key);
|
|
109
|
+
}
|
|
110
|
+
return extract(values, options.headers ?? credentialNames);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function kafkaCarrier(options = {}) {
|
|
115
|
+
const message = (context) => record(method(contextOf(context), "getMessage")?.());
|
|
116
|
+
return {
|
|
117
|
+
id: "kafka",
|
|
118
|
+
matches: (context) => message(context) !== void 0 && "headers" in message(context),
|
|
119
|
+
headers: (context) => extract(message(context)?.headers, options.headers ?? credentialNames)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function rmqCarrier(options = {}) {
|
|
123
|
+
const message = (context) => record(method(contextOf(context), "getMessage")?.());
|
|
124
|
+
return {
|
|
125
|
+
id: "rmq",
|
|
126
|
+
matches: (context) => message(context) !== void 0 && "properties" in message(context),
|
|
127
|
+
headers: (context) => extract(record(message(context)?.properties)?.headers, options.headers ?? credentialNames)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function mqttCarrier(options = {}) {
|
|
131
|
+
return {
|
|
132
|
+
id: "mqtt",
|
|
133
|
+
matches: (context) => !!method(contextOf(context), "getPacket"),
|
|
134
|
+
headers: (context) => extract(record(record(method(contextOf(context), "getPacket")?.())?.properties)?.userProperties, options.userProperties ?? credentialNames)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function payloadCarrier(options = {}) {
|
|
138
|
+
const payload = (context) => {
|
|
139
|
+
const data = record(context.switchToRpc().getData());
|
|
140
|
+
const field = options.field ?? "auth";
|
|
141
|
+
return data && Object.hasOwn(data, field) ? record(data[field]) : void 0;
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
id: "payload",
|
|
145
|
+
matches: (context) => payload(context) !== void 0,
|
|
146
|
+
headers: (context) => extract(payload(context), credentialNames)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const defaultCarriers = Object.freeze([
|
|
150
|
+
grpcCarrier(),
|
|
151
|
+
natsCarrier(),
|
|
152
|
+
kafkaCarrier(),
|
|
153
|
+
rmqCarrier(),
|
|
154
|
+
mqttCarrier(),
|
|
155
|
+
payloadCarrier()
|
|
156
|
+
]);
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/rpc-transport.ts
|
|
159
|
+
function rpcTransport(options = {}) {
|
|
160
|
+
const carriers = [...options.carriers ?? defaultCarriers];
|
|
161
|
+
const select = (context) => carriers.find((carrier) => carrier.matches(context));
|
|
162
|
+
const grpc = grpcCarrier();
|
|
163
|
+
return {
|
|
164
|
+
id: "rpc",
|
|
165
|
+
requires: { hostlessCalls: true },
|
|
166
|
+
handles: (context) => context.getType() === "rpc",
|
|
167
|
+
describe(context) {
|
|
168
|
+
const candidate = context.switchToRpc().getContext();
|
|
169
|
+
const key = candidate !== null && typeof candidate === "object" ? candidate : context.getArgs();
|
|
170
|
+
return {
|
|
171
|
+
key,
|
|
172
|
+
invocation: key,
|
|
173
|
+
cookies: null,
|
|
174
|
+
clientIp: null,
|
|
175
|
+
headers() {
|
|
176
|
+
const input = select(context)?.headers(context);
|
|
177
|
+
try {
|
|
178
|
+
return new Headers(input);
|
|
179
|
+
} catch {
|
|
180
|
+
throw require_auth_errors.AuthFailures.rejected({
|
|
181
|
+
status: 401,
|
|
182
|
+
reason: "MALFORMED_CREDENTIALS"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
param(name) {
|
|
187
|
+
const data = context.switchToRpc().getData();
|
|
188
|
+
return data !== null && typeof data === "object" && Object.hasOwn(data, name) ? data[name] : void 0;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
toException(failure, context) {
|
|
193
|
+
const carrier = select(context);
|
|
194
|
+
return carrier?.toException ? carrier.toException(failure, context) : new _nestjs_microservices.RpcException({
|
|
195
|
+
status: "error",
|
|
196
|
+
statusCode: failure.status,
|
|
197
|
+
code: failure.code,
|
|
198
|
+
...failure.reason ? { reason: failure.reason } : {},
|
|
199
|
+
message: failure.message
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
toInternalException(error, context, info) {
|
|
203
|
+
if (!grpc.matches(context)) return error;
|
|
204
|
+
if (!info.repeated) new _nestjs_common.Logger("BetterAuthRpc").error(error);
|
|
205
|
+
return new _nestjs_microservices.RpcException({
|
|
206
|
+
code: 13,
|
|
207
|
+
message: "Internal server error"
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
validate(context) {
|
|
211
|
+
class Probe {
|
|
212
|
+
handler() {}
|
|
213
|
+
}
|
|
214
|
+
(0, _nestjs_microservices.MessagePattern)("nsba-probe")(Probe.prototype, "handler", Object.getOwnPropertyDescriptor(Probe.prototype, "handler"));
|
|
215
|
+
const patterns = Reflect.getMetadata("microservices:pattern", Probe.prototype.handler);
|
|
216
|
+
if (!Array.isArray(patterns) || patterns[0] !== "nsba-probe") throw new require_auth_errors.BetterAuthConfigurationError("NEST_METADATA_KEY_CHANGED", "Nest RPC pattern metadata failed its canary");
|
|
217
|
+
const explicit = context.hasHttpAdapter && !options.inheritAppConfig;
|
|
218
|
+
const scanner = new _nestjs_core.MetadataScanner();
|
|
219
|
+
for (const wrapper of context.discovery.getControllers()) {
|
|
220
|
+
const target = wrapper.metatype;
|
|
221
|
+
if (!target?.prototype || wrapper.isAlias) continue;
|
|
222
|
+
for (const name of scanner.getAllMethodNames(target.prototype)) {
|
|
223
|
+
if (Reflect.getMetadata("microservices:pattern", target.prototype[name]) === void 0) continue;
|
|
224
|
+
context.claim(target, name, explicit ? "explicit" : "global", {
|
|
225
|
+
code: "RPC_HANDLER_UNGUARDED",
|
|
226
|
+
everyHandler: explicit,
|
|
227
|
+
coverage: explicit ? options.hybridCoverage ?? "error" : "error",
|
|
228
|
+
hint: "Add @UseBetterAuth() or @Public() to the RPC controller/handler, or assert inheritAppConfig and pass { inheritAppConfig: true } to every connectMicroservice call."
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
advise() {
|
|
234
|
+
return options.inheritAppConfig ? [{
|
|
235
|
+
level: "warn",
|
|
236
|
+
code: "W_RPC_INHERIT_APP_CONFIG_ASSERTED",
|
|
237
|
+
message: "RPC coverage assumes every connectMicroservice() call passes { inheritAppConfig: true }. Prefer @UseBetterAuth() on message controllers for verifiable coverage."
|
|
238
|
+
}] : [];
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
exports.defaultCarriers = defaultCarriers;
|
|
244
|
+
exports.grpcCarrier = grpcCarrier;
|
|
245
|
+
exports.kafkaCarrier = kafkaCarrier;
|
|
246
|
+
exports.mqttCarrier = mqttCarrier;
|
|
247
|
+
exports.natsCarrier = natsCarrier;
|
|
248
|
+
exports.payloadCarrier = payloadCarrier;
|
|
249
|
+
exports.rmqCarrier = rmqCarrier;
|
|
250
|
+
exports.rpcTransport = rpcTransport;
|
|
251
|
+
|
|
252
|
+
//# sourceMappingURL=microservices.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microservices.cjs","names":["AuthFailures","microservices","AuthFailures","RpcException","Logger","BetterAuthConfigurationError","MetadataScanner"],"sources":["../src/rpc-carriers.ts","../src/rpc-transport.ts"],"sourcesContent":["import type { ExecutionContext } from \"@nestjs/common\";\nimport * as microservices from \"@nestjs/microservices\";\nimport { AuthFailures, type AuthFailure } from \"./auth-errors.js\";\n\nexport interface RpcCredentialCarrier {\n readonly id: string;\n matches(context: ExecutionContext): boolean;\n headers(context: ExecutionContext): HeadersInit | undefined;\n toException?(failure: AuthFailure, context: ExecutionContext): unknown;\n}\n\nconst credentialNames = [\"authorization\", \"cookie\", \"x-api-key\"] as const;\n\ntype RecordValue = Record<string, unknown>;\nfunction record(value: unknown): RecordValue | undefined {\n return value !== null && typeof value === \"object\" && !Array.isArray(value)\n ? (value as RecordValue)\n : undefined;\n}\nfunction method(\n value: unknown,\n name: string,\n): ((...args: unknown[]) => unknown) | undefined {\n const target = record(value);\n const candidate = target?.[name];\n return typeof candidate === \"function\" ? candidate.bind(value) : undefined;\n}\nfunction contextOf(context: ExecutionContext): unknown {\n return context.switchToRpc().getContext<unknown>();\n}\nfunction extract(input: unknown, allow: readonly string[]): Headers {\n const headers = new Headers();\n const allowed = new Set(allow.map((name) => name.toLowerCase()));\n // Host and forwarding metadata must never influence hostless authentication.\n for (const name of [\n \"host\",\n \"x-forwarded-host\",\n \"x-forwarded-proto\",\n \"set-cookie\",\n ]) {\n allowed.delete(name);\n }\n const source = record(input);\n if (!source) {\n return headers;\n }\n for (const name of Object.keys(source)) {\n const normalized = name.toLowerCase();\n if (!allowed.has(normalized)) {\n continue;\n }\n const value = source[name];\n const values = Array.isArray(value) ? value : [value];\n if (values.length === 0) {\n throw AuthFailures.rejected({\n status: 401,\n reason: \"MALFORMED_CREDENTIALS\",\n });\n }\n for (const item of values) {\n const text =\n typeof item === \"string\"\n ? item\n : Buffer.isBuffer(item)\n ? item.toString(\"utf8\")\n : undefined;\n if (text === undefined || /[\\r\\n\\0]/.test(text)) {\n throw AuthFailures.rejected({\n status: 401,\n reason: \"MALFORMED_CREDENTIALS\",\n });\n }\n try {\n headers.append(normalized, text);\n } catch {\n // Web Headers errors can embed credentials in their message. Never retain\n // that error, and never discard one bad credential to try another one.\n throw AuthFailures.rejected({\n status: 401,\n reason: \"MALFORMED_CREDENTIALS\",\n });\n }\n }\n }\n return headers;\n}\n\nexport function grpcCarrier(\n options: { metadata?: readonly string[] } = {},\n): RpcCredentialCarrier {\n return {\n id: \"grpc\",\n matches: (context) =>\n !!method(context.getArgByIndex(1), \"get\") &&\n !!method(context.getArgByIndex(1), \"getMap\"),\n headers: (context) =>\n extract(\n method(context.getArgByIndex(1), \"getMap\")?.(),\n options.metadata ?? credentialNames,\n ),\n toException(failure) {\n const name =\n failure.status === 401\n ? \"GrpcUnauthenticatedException\"\n : failure.status === 403\n ? \"GrpcPermissionDeniedException\"\n : \"GrpcResourceExhaustedException\";\n const Native = (\n microservices as unknown as Record<\n string,\n new (\n message: string,\n ) => Error & { getError(): object }\n >\n )[name];\n // Native gRPC exceptions require an opt-in Nest filter. Wrap their payload\n // so the default RPC filter also delivers the numeric status.\n return new microservices.RpcException(\n Native\n ? new Native(failure.message).getError()\n : {\n code:\n failure.status === 401 ? 16 : failure.status === 403 ? 7 : 8,\n message: failure.message,\n },\n );\n },\n };\n}\n\nexport function natsCarrier(\n options: { headers?: readonly string[] } = {},\n): RpcCredentialCarrier {\n return {\n id: \"nats\",\n matches: (context) => !!method(contextOf(context), \"getHeaders\"),\n headers(context) {\n const input = method(contextOf(context), \"getHeaders\")?.();\n const keys = method(input, \"keys\")?.();\n const get = method(input, \"get\");\n const values: RecordValue = Object.create(null);\n if (Array.isArray(keys) && get) {\n for (const key of keys) {\n if (typeof key === \"string\") {\n values[key] = get(key);\n }\n }\n }\n return extract(values, options.headers ?? credentialNames);\n },\n };\n}\n\nexport function kafkaCarrier(\n options: { headers?: readonly string[] } = {},\n): RpcCredentialCarrier {\n const message = (context: ExecutionContext) =>\n record(method(contextOf(context), \"getMessage\")?.());\n return {\n id: \"kafka\",\n matches: (context) =>\n message(context) !== undefined && \"headers\" in message(context)!,\n headers: (context) =>\n extract(message(context)?.headers, options.headers ?? credentialNames),\n };\n}\n\nexport function rmqCarrier(\n options: { headers?: readonly string[] } = {},\n): RpcCredentialCarrier {\n const message = (context: ExecutionContext) =>\n record(method(contextOf(context), \"getMessage\")?.());\n return {\n id: \"rmq\",\n matches: (context) =>\n message(context) !== undefined && \"properties\" in message(context)!,\n headers: (context) =>\n extract(\n record(message(context)?.properties)?.headers,\n options.headers ?? credentialNames,\n ),\n };\n}\n\nexport function mqttCarrier(\n options: { userProperties?: readonly string[] } = {},\n): RpcCredentialCarrier {\n return {\n id: \"mqtt\",\n matches: (context) => !!method(contextOf(context), \"getPacket\"),\n headers: (context) =>\n extract(\n record(record(method(contextOf(context), \"getPacket\")?.())?.properties)\n ?.userProperties,\n options.userProperties ?? credentialNames,\n ),\n };\n}\n\nexport function payloadCarrier(\n options: { field?: string } = {},\n): RpcCredentialCarrier {\n const payload = (context: ExecutionContext) => {\n const data = record(context.switchToRpc().getData<unknown>());\n const field = options.field ?? \"auth\";\n return data && Object.hasOwn(data, field) ? record(data[field]) : undefined;\n };\n return {\n id: \"payload\",\n matches: (context) => payload(context) !== undefined,\n headers: (context) => extract(payload(context), credentialNames),\n };\n}\n\nexport const defaultCarriers: readonly RpcCredentialCarrier[] = Object.freeze([\n grpcCarrier(),\n natsCarrier(),\n kafkaCarrier(),\n rmqCarrier(),\n mqttCarrier(),\n payloadCarrier(),\n]);\n","import { Logger, type ExecutionContext } from \"@nestjs/common\";\nimport { MetadataScanner } from \"@nestjs/core\";\nimport { MessagePattern, RpcException } from \"@nestjs/microservices\";\nimport type {\n AuthTransport,\n BootAdvice,\n ExtensionRef,\n TransportCall,\n TransportValidationContext,\n} from \"./auth-contracts.js\";\nimport {\n BetterAuthConfigurationError,\n AuthFailures,\n type AuthFailure,\n} from \"./auth-errors.js\";\nimport {\n defaultCarriers,\n grpcCarrier,\n type RpcCredentialCarrier,\n} from \"./rpc-carriers.js\";\n\nexport interface RpcTransportOptions {\n carriers?: readonly RpcCredentialCarrier[];\n hybridCoverage?: \"error\" | \"warn\" | \"off\";\n inheritAppConfig?: boolean;\n}\n\nexport function rpcTransport(\n options: RpcTransportOptions = {},\n): ExtensionRef<AuthTransport> {\n const carriers = [...(options.carriers ?? defaultCarriers)];\n const select = (context: ExecutionContext) =>\n carriers.find((carrier) => carrier.matches(context));\n const grpc = grpcCarrier();\n return {\n id: \"rpc\",\n requires: { hostlessCalls: true },\n handles: (context) => context.getType() === \"rpc\",\n describe(context): TransportCall {\n const candidate = context.switchToRpc().getContext<unknown>();\n const key =\n candidate !== null && typeof candidate === \"object\"\n ? candidate\n : context.getArgs();\n return {\n key,\n invocation: key,\n cookies: null,\n clientIp: null,\n headers() {\n const input = select(context)?.headers(context);\n try {\n return new Headers(input);\n } catch {\n // Custom carriers may return a raw HeadersInit. Conversion errors\n // include its values, so discard the original error and its cause.\n throw AuthFailures.rejected({\n status: 401,\n reason: \"MALFORMED_CREDENTIALS\",\n });\n }\n },\n param(name) {\n const data = context.switchToRpc().getData<unknown>();\n return data !== null &&\n typeof data === \"object\" &&\n Object.hasOwn(data, name)\n ? (data as Record<string, unknown>)[name]\n : undefined;\n },\n };\n },\n toException(failure: AuthFailure, context) {\n const carrier = select(context);\n return carrier?.toException\n ? carrier.toException(failure, context)\n : new RpcException({\n status: \"error\",\n statusCode: failure.status,\n code: failure.code,\n ...(failure.reason ? { reason: failure.reason } : {}),\n message: failure.message,\n });\n },\n toInternalException(error, context, info) {\n if (!grpc.matches(context)) {\n return error;\n }\n if (!info.repeated) {\n new Logger(\"BetterAuthRpc\").error(error);\n }\n return new RpcException({ code: 13, message: \"Internal server error\" });\n },\n validate(context: TransportValidationContext) {\n class Probe {\n handler(): void {}\n }\n MessagePattern(\"nsba-probe\")(\n Probe.prototype,\n \"handler\",\n Object.getOwnPropertyDescriptor(Probe.prototype, \"handler\")!,\n );\n const patterns: unknown = Reflect.getMetadata(\n \"microservices:pattern\",\n Probe.prototype.handler,\n );\n if (!Array.isArray(patterns) || patterns[0] !== \"nsba-probe\") {\n throw new BetterAuthConfigurationError(\n \"NEST_METADATA_KEY_CHANGED\",\n \"Nest RPC pattern metadata failed its canary\",\n );\n }\n const explicit = context.hasHttpAdapter && !options.inheritAppConfig;\n const scanner = new MetadataScanner();\n for (const wrapper of context.discovery.getControllers()) {\n const target = wrapper.metatype;\n if (!target?.prototype || wrapper.isAlias) {\n continue;\n }\n for (const name of scanner.getAllMethodNames(target.prototype)) {\n if (\n Reflect.getMetadata(\n \"microservices:pattern\",\n target.prototype[name],\n ) === undefined\n ) {\n continue;\n }\n context.claim(target, name, explicit ? \"explicit\" : \"global\", {\n code: \"RPC_HANDLER_UNGUARDED\",\n everyHandler: explicit,\n coverage: explicit ? (options.hybridCoverage ?? \"error\") : \"error\",\n hint: \"Add @UseBetterAuth() or @Public() to the RPC controller/handler, or assert inheritAppConfig and pass { inheritAppConfig: true } to every connectMicroservice call.\",\n });\n }\n }\n },\n advise(): readonly BootAdvice[] {\n return options.inheritAppConfig\n ? [\n {\n level: \"warn\",\n code: \"W_RPC_INHERIT_APP_CONFIG_ASSERTED\",\n message:\n \"RPC coverage assumes every connectMicroservice() call passes { inheritAppConfig: true }. Prefer @UseBetterAuth() on message controllers for verifiable coverage.\",\n },\n ]\n : [];\n },\n } satisfies AuthTransport;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { M as ExtensionRef, c as AuthTransport, zn as AuthFailure } from "./auth-contracts-BBA1C1gj.cjs";
|
|
2
|
+
import { ExecutionContext } from "@nestjs/common";
|
|
3
|
+
//#region src/rpc-carriers.d.ts
|
|
4
|
+
interface RpcCredentialCarrier {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
matches(context: ExecutionContext): boolean;
|
|
7
|
+
headers(context: ExecutionContext): HeadersInit | undefined;
|
|
8
|
+
toException?(failure: AuthFailure, context: ExecutionContext): unknown;
|
|
9
|
+
}
|
|
10
|
+
export declare function grpcCarrier(options?: {
|
|
11
|
+
metadata?: readonly string[];
|
|
12
|
+
}): RpcCredentialCarrier;
|
|
13
|
+
export declare function natsCarrier(options?: {
|
|
14
|
+
headers?: readonly string[];
|
|
15
|
+
}): RpcCredentialCarrier;
|
|
16
|
+
export declare function kafkaCarrier(options?: {
|
|
17
|
+
headers?: readonly string[];
|
|
18
|
+
}): RpcCredentialCarrier;
|
|
19
|
+
export declare function rmqCarrier(options?: {
|
|
20
|
+
headers?: readonly string[];
|
|
21
|
+
}): RpcCredentialCarrier;
|
|
22
|
+
export declare function mqttCarrier(options?: {
|
|
23
|
+
userProperties?: readonly string[];
|
|
24
|
+
}): RpcCredentialCarrier;
|
|
25
|
+
export declare function payloadCarrier(options?: {
|
|
26
|
+
field?: string;
|
|
27
|
+
}): RpcCredentialCarrier;
|
|
28
|
+
export declare const defaultCarriers: readonly RpcCredentialCarrier[];
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/rpc-transport.d.ts
|
|
31
|
+
interface RpcTransportOptions {
|
|
32
|
+
carriers?: readonly RpcCredentialCarrier[];
|
|
33
|
+
hybridCoverage?: "error" | "warn" | "off";
|
|
34
|
+
inheritAppConfig?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function rpcTransport(options?: RpcTransportOptions): ExtensionRef<AuthTransport>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export type { RpcCredentialCarrier, RpcTransportOptions };
|
|
39
|
+
//# sourceMappingURL=microservices.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microservices.d.cts","names":[],"sources":["../src/rpc-carriers.ts","../src/rpc-transport.ts"],"mappings":";;;UAIiB;WACN;EACT,QAAQ,SAAS;EACjB,QAAQ,SAAS,mBAAmB;EACpC,aAAa,SAAS,aAAa,SAAS;;wBA+E9B,YACd;EAAW;IACV;wBAyCa,YACd;EAAW;IACV;wBAqBa,aACd;EAAW;IACV;wBAYa,WACd;EAAW;IACV;wBAea,YACd;EAAW;IACV;wBAaa,eACd;EAAW;IACV;qBAaU,0BAA0B;;;UCjMtB;EACf,oBAAoB;EACpB;EACA;;wBAGc,aACd,UAAS,sBACR,aAAa"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { M as ExtensionRef, c as AuthTransport, zn as AuthFailure } from "./auth-contracts-tqsfBZ8B.mjs";
|
|
2
|
+
import { ExecutionContext } from "@nestjs/common";
|
|
3
|
+
//#region src/rpc-carriers.d.ts
|
|
4
|
+
interface RpcCredentialCarrier {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
matches(context: ExecutionContext): boolean;
|
|
7
|
+
headers(context: ExecutionContext): HeadersInit | undefined;
|
|
8
|
+
toException?(failure: AuthFailure, context: ExecutionContext): unknown;
|
|
9
|
+
}
|
|
10
|
+
export declare function grpcCarrier(options?: {
|
|
11
|
+
metadata?: readonly string[];
|
|
12
|
+
}): RpcCredentialCarrier;
|
|
13
|
+
export declare function natsCarrier(options?: {
|
|
14
|
+
headers?: readonly string[];
|
|
15
|
+
}): RpcCredentialCarrier;
|
|
16
|
+
export declare function kafkaCarrier(options?: {
|
|
17
|
+
headers?: readonly string[];
|
|
18
|
+
}): RpcCredentialCarrier;
|
|
19
|
+
export declare function rmqCarrier(options?: {
|
|
20
|
+
headers?: readonly string[];
|
|
21
|
+
}): RpcCredentialCarrier;
|
|
22
|
+
export declare function mqttCarrier(options?: {
|
|
23
|
+
userProperties?: readonly string[];
|
|
24
|
+
}): RpcCredentialCarrier;
|
|
25
|
+
export declare function payloadCarrier(options?: {
|
|
26
|
+
field?: string;
|
|
27
|
+
}): RpcCredentialCarrier;
|
|
28
|
+
export declare const defaultCarriers: readonly RpcCredentialCarrier[];
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/rpc-transport.d.ts
|
|
31
|
+
interface RpcTransportOptions {
|
|
32
|
+
carriers?: readonly RpcCredentialCarrier[];
|
|
33
|
+
hybridCoverage?: "error" | "warn" | "off";
|
|
34
|
+
inheritAppConfig?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function rpcTransport(options?: RpcTransportOptions): ExtensionRef<AuthTransport>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export type { RpcCredentialCarrier, RpcTransportOptions };
|
|
39
|
+
//# sourceMappingURL=microservices.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microservices.d.mts","names":[],"sources":["../src/rpc-carriers.ts","../src/rpc-transport.ts"],"mappings":";;;UAIiB;WACN;EACT,QAAQ,SAAS;EACjB,QAAQ,SAAS,mBAAmB;EACpC,aAAa,SAAS,aAAa,SAAS;;wBA+E9B,YACd;EAAW;IACV;wBAyCa,YACd;EAAW;IACV;wBAqBa,aACd;EAAW;IACV;wBAYa,WACd;EAAW;IACV;wBAea,YACd;EAAW;IACV;wBAaa,eACd;EAAW;IACV;qBAaU,0BAA0B;;;UCjMtB;EACf,oBAAoB;EACpB;EACA;;wBAGc,aACd,UAAS,sBACR,aAAa"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { n as BetterAuthConfigurationError, t as AuthFailures } from "./auth-errors-CQjfgjaO.mjs";
|
|
2
|
+
import { Logger } from "@nestjs/common";
|
|
3
|
+
import { MetadataScanner } from "@nestjs/core";
|
|
4
|
+
import * as microservices from "@nestjs/microservices";
|
|
5
|
+
import { MessagePattern, RpcException } from "@nestjs/microservices";
|
|
6
|
+
//#region src/rpc-carriers.ts
|
|
7
|
+
const credentialNames = [
|
|
8
|
+
"authorization",
|
|
9
|
+
"cookie",
|
|
10
|
+
"x-api-key"
|
|
11
|
+
];
|
|
12
|
+
function record(value) {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
14
|
+
}
|
|
15
|
+
function method(value, name) {
|
|
16
|
+
const candidate = record(value)?.[name];
|
|
17
|
+
return typeof candidate === "function" ? candidate.bind(value) : void 0;
|
|
18
|
+
}
|
|
19
|
+
function contextOf(context) {
|
|
20
|
+
return context.switchToRpc().getContext();
|
|
21
|
+
}
|
|
22
|
+
function extract(input, allow) {
|
|
23
|
+
const headers = new Headers();
|
|
24
|
+
const allowed = new Set(allow.map((name) => name.toLowerCase()));
|
|
25
|
+
for (const name of [
|
|
26
|
+
"host",
|
|
27
|
+
"x-forwarded-host",
|
|
28
|
+
"x-forwarded-proto",
|
|
29
|
+
"set-cookie"
|
|
30
|
+
]) allowed.delete(name);
|
|
31
|
+
const source = record(input);
|
|
32
|
+
if (!source) return headers;
|
|
33
|
+
for (const name of Object.keys(source)) {
|
|
34
|
+
const normalized = name.toLowerCase();
|
|
35
|
+
if (!allowed.has(normalized)) continue;
|
|
36
|
+
const value = source[name];
|
|
37
|
+
const values = Array.isArray(value) ? value : [value];
|
|
38
|
+
if (values.length === 0) throw AuthFailures.rejected({
|
|
39
|
+
status: 401,
|
|
40
|
+
reason: "MALFORMED_CREDENTIALS"
|
|
41
|
+
});
|
|
42
|
+
for (const item of values) {
|
|
43
|
+
const text = typeof item === "string" ? item : Buffer.isBuffer(item) ? item.toString("utf8") : void 0;
|
|
44
|
+
if (text === void 0 || /[\r\n\0]/.test(text)) throw AuthFailures.rejected({
|
|
45
|
+
status: 401,
|
|
46
|
+
reason: "MALFORMED_CREDENTIALS"
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
headers.append(normalized, text);
|
|
50
|
+
} catch {
|
|
51
|
+
throw AuthFailures.rejected({
|
|
52
|
+
status: 401,
|
|
53
|
+
reason: "MALFORMED_CREDENTIALS"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return headers;
|
|
59
|
+
}
|
|
60
|
+
function grpcCarrier(options = {}) {
|
|
61
|
+
return {
|
|
62
|
+
id: "grpc",
|
|
63
|
+
matches: (context) => !!method(context.getArgByIndex(1), "get") && !!method(context.getArgByIndex(1), "getMap"),
|
|
64
|
+
headers: (context) => extract(method(context.getArgByIndex(1), "getMap")?.(), options.metadata ?? credentialNames),
|
|
65
|
+
toException(failure) {
|
|
66
|
+
const name = failure.status === 401 ? "GrpcUnauthenticatedException" : failure.status === 403 ? "GrpcPermissionDeniedException" : "GrpcResourceExhaustedException";
|
|
67
|
+
const Native = microservices[name];
|
|
68
|
+
return new microservices.RpcException(Native ? new Native(failure.message).getError() : {
|
|
69
|
+
code: failure.status === 401 ? 16 : failure.status === 403 ? 7 : 8,
|
|
70
|
+
message: failure.message
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function natsCarrier(options = {}) {
|
|
76
|
+
return {
|
|
77
|
+
id: "nats",
|
|
78
|
+
matches: (context) => !!method(contextOf(context), "getHeaders"),
|
|
79
|
+
headers(context) {
|
|
80
|
+
const input = method(contextOf(context), "getHeaders")?.();
|
|
81
|
+
const keys = method(input, "keys")?.();
|
|
82
|
+
const get = method(input, "get");
|
|
83
|
+
const values = Object.create(null);
|
|
84
|
+
if (Array.isArray(keys) && get) {
|
|
85
|
+
for (const key of keys) if (typeof key === "string") values[key] = get(key);
|
|
86
|
+
}
|
|
87
|
+
return extract(values, options.headers ?? credentialNames);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function kafkaCarrier(options = {}) {
|
|
92
|
+
const message = (context) => record(method(contextOf(context), "getMessage")?.());
|
|
93
|
+
return {
|
|
94
|
+
id: "kafka",
|
|
95
|
+
matches: (context) => message(context) !== void 0 && "headers" in message(context),
|
|
96
|
+
headers: (context) => extract(message(context)?.headers, options.headers ?? credentialNames)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function rmqCarrier(options = {}) {
|
|
100
|
+
const message = (context) => record(method(contextOf(context), "getMessage")?.());
|
|
101
|
+
return {
|
|
102
|
+
id: "rmq",
|
|
103
|
+
matches: (context) => message(context) !== void 0 && "properties" in message(context),
|
|
104
|
+
headers: (context) => extract(record(message(context)?.properties)?.headers, options.headers ?? credentialNames)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function mqttCarrier(options = {}) {
|
|
108
|
+
return {
|
|
109
|
+
id: "mqtt",
|
|
110
|
+
matches: (context) => !!method(contextOf(context), "getPacket"),
|
|
111
|
+
headers: (context) => extract(record(record(method(contextOf(context), "getPacket")?.())?.properties)?.userProperties, options.userProperties ?? credentialNames)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function payloadCarrier(options = {}) {
|
|
115
|
+
const payload = (context) => {
|
|
116
|
+
const data = record(context.switchToRpc().getData());
|
|
117
|
+
const field = options.field ?? "auth";
|
|
118
|
+
return data && Object.hasOwn(data, field) ? record(data[field]) : void 0;
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
id: "payload",
|
|
122
|
+
matches: (context) => payload(context) !== void 0,
|
|
123
|
+
headers: (context) => extract(payload(context), credentialNames)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const defaultCarriers = Object.freeze([
|
|
127
|
+
grpcCarrier(),
|
|
128
|
+
natsCarrier(),
|
|
129
|
+
kafkaCarrier(),
|
|
130
|
+
rmqCarrier(),
|
|
131
|
+
mqttCarrier(),
|
|
132
|
+
payloadCarrier()
|
|
133
|
+
]);
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/rpc-transport.ts
|
|
136
|
+
function rpcTransport(options = {}) {
|
|
137
|
+
const carriers = [...options.carriers ?? defaultCarriers];
|
|
138
|
+
const select = (context) => carriers.find((carrier) => carrier.matches(context));
|
|
139
|
+
const grpc = grpcCarrier();
|
|
140
|
+
return {
|
|
141
|
+
id: "rpc",
|
|
142
|
+
requires: { hostlessCalls: true },
|
|
143
|
+
handles: (context) => context.getType() === "rpc",
|
|
144
|
+
describe(context) {
|
|
145
|
+
const candidate = context.switchToRpc().getContext();
|
|
146
|
+
const key = candidate !== null && typeof candidate === "object" ? candidate : context.getArgs();
|
|
147
|
+
return {
|
|
148
|
+
key,
|
|
149
|
+
invocation: key,
|
|
150
|
+
cookies: null,
|
|
151
|
+
clientIp: null,
|
|
152
|
+
headers() {
|
|
153
|
+
const input = select(context)?.headers(context);
|
|
154
|
+
try {
|
|
155
|
+
return new Headers(input);
|
|
156
|
+
} catch {
|
|
157
|
+
throw AuthFailures.rejected({
|
|
158
|
+
status: 401,
|
|
159
|
+
reason: "MALFORMED_CREDENTIALS"
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
param(name) {
|
|
164
|
+
const data = context.switchToRpc().getData();
|
|
165
|
+
return data !== null && typeof data === "object" && Object.hasOwn(data, name) ? data[name] : void 0;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
toException(failure, context) {
|
|
170
|
+
const carrier = select(context);
|
|
171
|
+
return carrier?.toException ? carrier.toException(failure, context) : new RpcException({
|
|
172
|
+
status: "error",
|
|
173
|
+
statusCode: failure.status,
|
|
174
|
+
code: failure.code,
|
|
175
|
+
...failure.reason ? { reason: failure.reason } : {},
|
|
176
|
+
message: failure.message
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
toInternalException(error, context, info) {
|
|
180
|
+
if (!grpc.matches(context)) return error;
|
|
181
|
+
if (!info.repeated) new Logger("BetterAuthRpc").error(error);
|
|
182
|
+
return new RpcException({
|
|
183
|
+
code: 13,
|
|
184
|
+
message: "Internal server error"
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
validate(context) {
|
|
188
|
+
class Probe {
|
|
189
|
+
handler() {}
|
|
190
|
+
}
|
|
191
|
+
MessagePattern("nsba-probe")(Probe.prototype, "handler", Object.getOwnPropertyDescriptor(Probe.prototype, "handler"));
|
|
192
|
+
const patterns = Reflect.getMetadata("microservices:pattern", Probe.prototype.handler);
|
|
193
|
+
if (!Array.isArray(patterns) || patterns[0] !== "nsba-probe") throw new BetterAuthConfigurationError("NEST_METADATA_KEY_CHANGED", "Nest RPC pattern metadata failed its canary");
|
|
194
|
+
const explicit = context.hasHttpAdapter && !options.inheritAppConfig;
|
|
195
|
+
const scanner = new MetadataScanner();
|
|
196
|
+
for (const wrapper of context.discovery.getControllers()) {
|
|
197
|
+
const target = wrapper.metatype;
|
|
198
|
+
if (!target?.prototype || wrapper.isAlias) continue;
|
|
199
|
+
for (const name of scanner.getAllMethodNames(target.prototype)) {
|
|
200
|
+
if (Reflect.getMetadata("microservices:pattern", target.prototype[name]) === void 0) continue;
|
|
201
|
+
context.claim(target, name, explicit ? "explicit" : "global", {
|
|
202
|
+
code: "RPC_HANDLER_UNGUARDED",
|
|
203
|
+
everyHandler: explicit,
|
|
204
|
+
coverage: explicit ? options.hybridCoverage ?? "error" : "error",
|
|
205
|
+
hint: "Add @UseBetterAuth() or @Public() to the RPC controller/handler, or assert inheritAppConfig and pass { inheritAppConfig: true } to every connectMicroservice call."
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
advise() {
|
|
211
|
+
return options.inheritAppConfig ? [{
|
|
212
|
+
level: "warn",
|
|
213
|
+
code: "W_RPC_INHERIT_APP_CONFIG_ASSERTED",
|
|
214
|
+
message: "RPC coverage assumes every connectMicroservice() call passes { inheritAppConfig: true }. Prefer @UseBetterAuth() on message controllers for verifiable coverage."
|
|
215
|
+
}] : [];
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
export { defaultCarriers, grpcCarrier, kafkaCarrier, mqttCarrier, natsCarrier, payloadCarrier, rmqCarrier, rpcTransport };
|
|
221
|
+
|
|
222
|
+
//# sourceMappingURL=microservices.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microservices.mjs","names":[],"sources":["../src/rpc-carriers.ts","../src/rpc-transport.ts"],"sourcesContent":["import type { ExecutionContext } from \"@nestjs/common\";\nimport * as microservices from \"@nestjs/microservices\";\nimport { AuthFailures, type AuthFailure } from \"./auth-errors.js\";\n\nexport interface RpcCredentialCarrier {\n readonly id: string;\n matches(context: ExecutionContext): boolean;\n headers(context: ExecutionContext): HeadersInit | undefined;\n toException?(failure: AuthFailure, context: ExecutionContext): unknown;\n}\n\nconst credentialNames = [\"authorization\", \"cookie\", \"x-api-key\"] as const;\n\ntype RecordValue = Record<string, unknown>;\nfunction record(value: unknown): RecordValue | undefined {\n return value !== null && typeof value === \"object\" && !Array.isArray(value)\n ? (value as RecordValue)\n : undefined;\n}\nfunction method(\n value: unknown,\n name: string,\n): ((...args: unknown[]) => unknown) | undefined {\n const target = record(value);\n const candidate = target?.[name];\n return typeof candidate === \"function\" ? candidate.bind(value) : undefined;\n}\nfunction contextOf(context: ExecutionContext): unknown {\n return context.switchToRpc().getContext<unknown>();\n}\nfunction extract(input: unknown, allow: readonly string[]): Headers {\n const headers = new Headers();\n const allowed = new Set(allow.map((name) => name.toLowerCase()));\n // Host and forwarding metadata must never influence hostless authentication.\n for (const name of [\n \"host\",\n \"x-forwarded-host\",\n \"x-forwarded-proto\",\n \"set-cookie\",\n ]) {\n allowed.delete(name);\n }\n const source = record(input);\n if (!source) {\n return headers;\n }\n for (const name of Object.keys(source)) {\n const normalized = name.toLowerCase();\n if (!allowed.has(normalized)) {\n continue;\n }\n const value = source[name];\n const values = Array.isArray(value) ? value : [value];\n if (values.length === 0) {\n throw AuthFailures.rejected({\n status: 401,\n reason: \"MALFORMED_CREDENTIALS\",\n });\n }\n for (const item of values) {\n const text =\n typeof item === \"string\"\n ? item\n : Buffer.isBuffer(item)\n ? item.toString(\"utf8\")\n : undefined;\n if (text === undefined || /[\\r\\n\\0]/.test(text)) {\n throw AuthFailures.rejected({\n status: 401,\n reason: \"MALFORMED_CREDENTIALS\",\n });\n }\n try {\n headers.append(normalized, text);\n } catch {\n // Web Headers errors can embed credentials in their message. Never retain\n // that error, and never discard one bad credential to try another one.\n throw AuthFailures.rejected({\n status: 401,\n reason: \"MALFORMED_CREDENTIALS\",\n });\n }\n }\n }\n return headers;\n}\n\nexport function grpcCarrier(\n options: { metadata?: readonly string[] } = {},\n): RpcCredentialCarrier {\n return {\n id: \"grpc\",\n matches: (context) =>\n !!method(context.getArgByIndex(1), \"get\") &&\n !!method(context.getArgByIndex(1), \"getMap\"),\n headers: (context) =>\n extract(\n method(context.getArgByIndex(1), \"getMap\")?.(),\n options.metadata ?? credentialNames,\n ),\n toException(failure) {\n const name =\n failure.status === 401\n ? \"GrpcUnauthenticatedException\"\n : failure.status === 403\n ? \"GrpcPermissionDeniedException\"\n : \"GrpcResourceExhaustedException\";\n const Native = (\n microservices as unknown as Record<\n string,\n new (\n message: string,\n ) => Error & { getError(): object }\n >\n )[name];\n // Native gRPC exceptions require an opt-in Nest filter. Wrap their payload\n // so the default RPC filter also delivers the numeric status.\n return new microservices.RpcException(\n Native\n ? new Native(failure.message).getError()\n : {\n code:\n failure.status === 401 ? 16 : failure.status === 403 ? 7 : 8,\n message: failure.message,\n },\n );\n },\n };\n}\n\nexport function natsCarrier(\n options: { headers?: readonly string[] } = {},\n): RpcCredentialCarrier {\n return {\n id: \"nats\",\n matches: (context) => !!method(contextOf(context), \"getHeaders\"),\n headers(context) {\n const input = method(contextOf(context), \"getHeaders\")?.();\n const keys = method(input, \"keys\")?.();\n const get = method(input, \"get\");\n const values: RecordValue = Object.create(null);\n if (Array.isArray(keys) && get) {\n for (const key of keys) {\n if (typeof key === \"string\") {\n values[key] = get(key);\n }\n }\n }\n return extract(values, options.headers ?? credentialNames);\n },\n };\n}\n\nexport function kafkaCarrier(\n options: { headers?: readonly string[] } = {},\n): RpcCredentialCarrier {\n const message = (context: ExecutionContext) =>\n record(method(contextOf(context), \"getMessage\")?.());\n return {\n id: \"kafka\",\n matches: (context) =>\n message(context) !== undefined && \"headers\" in message(context)!,\n headers: (context) =>\n extract(message(context)?.headers, options.headers ?? credentialNames),\n };\n}\n\nexport function rmqCarrier(\n options: { headers?: readonly string[] } = {},\n): RpcCredentialCarrier {\n const message = (context: ExecutionContext) =>\n record(method(contextOf(context), \"getMessage\")?.());\n return {\n id: \"rmq\",\n matches: (context) =>\n message(context) !== undefined && \"properties\" in message(context)!,\n headers: (context) =>\n extract(\n record(message(context)?.properties)?.headers,\n options.headers ?? credentialNames,\n ),\n };\n}\n\nexport function mqttCarrier(\n options: { userProperties?: readonly string[] } = {},\n): RpcCredentialCarrier {\n return {\n id: \"mqtt\",\n matches: (context) => !!method(contextOf(context), \"getPacket\"),\n headers: (context) =>\n extract(\n record(record(method(contextOf(context), \"getPacket\")?.())?.properties)\n ?.userProperties,\n options.userProperties ?? credentialNames,\n ),\n };\n}\n\nexport function payloadCarrier(\n options: { field?: string } = {},\n): RpcCredentialCarrier {\n const payload = (context: ExecutionContext) => {\n const data = record(context.switchToRpc().getData<unknown>());\n const field = options.field ?? \"auth\";\n return data && Object.hasOwn(data, field) ? record(data[field]) : undefined;\n };\n return {\n id: \"payload\",\n matches: (context) => payload(context) !== undefined,\n headers: (context) => extract(payload(context), credentialNames),\n };\n}\n\nexport const defaultCarriers: readonly RpcCredentialCarrier[] = Object.freeze([\n grpcCarrier(),\n natsCarrier(),\n kafkaCarrier(),\n rmqCarrier(),\n mqttCarrier(),\n payloadCarrier(),\n]);\n","import { Logger, type ExecutionContext } from \"@nestjs/common\";\nimport { MetadataScanner } from \"@nestjs/core\";\nimport { MessagePattern, RpcException } from \"@nestjs/microservices\";\nimport type {\n AuthTransport,\n BootAdvice,\n ExtensionRef,\n TransportCall,\n TransportValidationContext,\n} from \"./auth-contracts.js\";\nimport {\n BetterAuthConfigurationError,\n AuthFailures,\n type AuthFailure,\n} from \"./auth-errors.js\";\nimport {\n defaultCarriers,\n grpcCarrier,\n type RpcCredentialCarrier,\n} from \"./rpc-carriers.js\";\n\nexport interface RpcTransportOptions {\n carriers?: readonly RpcCredentialCarrier[];\n hybridCoverage?: \"error\" | \"warn\" | \"off\";\n inheritAppConfig?: boolean;\n}\n\nexport function rpcTransport(\n options: RpcTransportOptions = {},\n): ExtensionRef<AuthTransport> {\n const carriers = [...(options.carriers ?? defaultCarriers)];\n const select = (context: ExecutionContext) =>\n carriers.find((carrier) => carrier.matches(context));\n const grpc = grpcCarrier();\n return {\n id: \"rpc\",\n requires: { hostlessCalls: true },\n handles: (context) => context.getType() === \"rpc\",\n describe(context): TransportCall {\n const candidate = context.switchToRpc().getContext<unknown>();\n const key =\n candidate !== null && typeof candidate === \"object\"\n ? candidate\n : context.getArgs();\n return {\n key,\n invocation: key,\n cookies: null,\n clientIp: null,\n headers() {\n const input = select(context)?.headers(context);\n try {\n return new Headers(input);\n } catch {\n // Custom carriers may return a raw HeadersInit. Conversion errors\n // include its values, so discard the original error and its cause.\n throw AuthFailures.rejected({\n status: 401,\n reason: \"MALFORMED_CREDENTIALS\",\n });\n }\n },\n param(name) {\n const data = context.switchToRpc().getData<unknown>();\n return data !== null &&\n typeof data === \"object\" &&\n Object.hasOwn(data, name)\n ? (data as Record<string, unknown>)[name]\n : undefined;\n },\n };\n },\n toException(failure: AuthFailure, context) {\n const carrier = select(context);\n return carrier?.toException\n ? carrier.toException(failure, context)\n : new RpcException({\n status: \"error\",\n statusCode: failure.status,\n code: failure.code,\n ...(failure.reason ? { reason: failure.reason } : {}),\n message: failure.message,\n });\n },\n toInternalException(error, context, info) {\n if (!grpc.matches(context)) {\n return error;\n }\n if (!info.repeated) {\n new Logger(\"BetterAuthRpc\").error(error);\n }\n return new RpcException({ code: 13, message: \"Internal server error\" });\n },\n validate(context: TransportValidationContext) {\n class Probe {\n handler(): void {}\n }\n MessagePattern(\"nsba-probe\")(\n Probe.prototype,\n \"handler\",\n Object.getOwnPropertyDescriptor(Probe.prototype, \"handler\")!,\n );\n const patterns: unknown = Reflect.getMetadata(\n \"microservices:pattern\",\n Probe.prototype.handler,\n );\n if (!Array.isArray(patterns) || patterns[0] !== \"nsba-probe\") {\n throw new BetterAuthConfigurationError(\n \"NEST_METADATA_KEY_CHANGED\",\n \"Nest RPC pattern metadata failed its canary\",\n );\n }\n const explicit = context.hasHttpAdapter && !options.inheritAppConfig;\n const scanner = new MetadataScanner();\n for (const wrapper of context.discovery.getControllers()) {\n const target = wrapper.metatype;\n if (!target?.prototype || wrapper.isAlias) {\n continue;\n }\n for (const name of scanner.getAllMethodNames(target.prototype)) {\n if (\n Reflect.getMetadata(\n \"microservices:pattern\",\n target.prototype[name],\n ) === undefined\n ) {\n continue;\n }\n context.claim(target, name, explicit ? \"explicit\" : \"global\", {\n code: \"RPC_HANDLER_UNGUARDED\",\n everyHandler: explicit,\n coverage: explicit ? (options.hybridCoverage ?? \"error\") : \"error\",\n hint: \"Add @UseBetterAuth() or @Public() to the RPC controller/handler, or assert inheritAppConfig and pass { inheritAppConfig: true } to every connectMicroservice call.\",\n });\n }\n }\n },\n advise(): readonly BootAdvice[] {\n return options.inheritAppConfig\n ? [\n {\n level: \"warn\",\n code: \"W_RPC_INHERIT_APP_CONFIG_ASSERTED\",\n message:\n \"RPC coverage assumes every connectMicroservice() call passes { inheritAppConfig: true }. Prefer @UseBetterAuth() on message controllers for verifiable coverage.\",\n },\n ]\n : [];\n },\n } satisfies AuthTransport;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestjs-slightly-better-auth",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A Better Auth integration for NestJS with Express and Fastify platforms, admin, organization and API-key authorization",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A Better Auth integration for NestJS with Express and Fastify platforms, microservice RPC transports, and admin, organization and API-key authorization",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "thilllon",
|
|
7
7
|
"type": "module",
|
|
@@ -79,6 +79,20 @@
|
|
|
79
79
|
"default": "./dist/fastify.cjs"
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
|
+
"./microservices": {
|
|
83
|
+
"module-sync": {
|
|
84
|
+
"types": "./dist/microservices.d.mts",
|
|
85
|
+
"default": "./dist/microservices.mjs"
|
|
86
|
+
},
|
|
87
|
+
"import": {
|
|
88
|
+
"types": "./dist/microservices.d.mts",
|
|
89
|
+
"default": "./dist/microservices.mjs"
|
|
90
|
+
},
|
|
91
|
+
"require": {
|
|
92
|
+
"types": "./dist/microservices.d.cts",
|
|
93
|
+
"default": "./dist/microservices.cjs"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
82
96
|
"./admin": {
|
|
83
97
|
"module-sync": {
|
|
84
98
|
"types": "./dist/admin.d.mts",
|
|
@@ -150,11 +164,20 @@
|
|
|
150
164
|
"nest",
|
|
151
165
|
"better-auth",
|
|
152
166
|
"authentication",
|
|
167
|
+
"microservices",
|
|
168
|
+
"rpc",
|
|
169
|
+
"grpc",
|
|
153
170
|
"typescript"
|
|
154
171
|
],
|
|
172
|
+
"peerDependenciesMeta": {
|
|
173
|
+
"@nestjs/microservices": {
|
|
174
|
+
"optional": true
|
|
175
|
+
}
|
|
176
|
+
},
|
|
155
177
|
"peerDependencies": {
|
|
156
178
|
"@nestjs/common": "^12.0.3",
|
|
157
179
|
"@nestjs/core": "^12.0.3",
|
|
180
|
+
"@nestjs/microservices": "^12.0.3",
|
|
158
181
|
"better-auth": ">=1.7.5 <2",
|
|
159
182
|
"reflect-metadata": "^0.2.2",
|
|
160
183
|
"rxjs": "^7.8.2"
|
|
@@ -165,22 +188,31 @@
|
|
|
165
188
|
},
|
|
166
189
|
"devDependencies": {
|
|
167
190
|
"@better-auth/api-key": "1.7.5",
|
|
191
|
+
"@grpc/grpc-js": "1.14.5",
|
|
192
|
+
"@grpc/proto-loader": "0.8.1",
|
|
193
|
+
"@nats-io/transport-node": "3.4.0",
|
|
168
194
|
"@nestjs/common": "12.0.3",
|
|
169
195
|
"@nestjs/core": "12.0.3",
|
|
196
|
+
"@nestjs/microservices": "12.0.3",
|
|
170
197
|
"@nestjs/platform-express": "12.0.3",
|
|
171
198
|
"@nestjs/platform-fastify": "12.0.3",
|
|
172
199
|
"@nestjs/testing": "12.0.3",
|
|
173
200
|
"@types/express": "5.0.6",
|
|
174
201
|
"@types/node": "24.13.5",
|
|
175
202
|
"@types/pg": "8.23.1",
|
|
203
|
+
"amqp-connection-manager": "5.0.0",
|
|
204
|
+
"amqplib": "2.0.1",
|
|
176
205
|
"better-auth": "1.7.5",
|
|
177
206
|
"express": "5.2.1",
|
|
207
|
+
"ioredis": "6.0.0",
|
|
208
|
+
"kafkajs": "2.2.4",
|
|
209
|
+
"mqtt": "5.16.0",
|
|
178
210
|
"pg": "8.23.0",
|
|
179
211
|
"reflect-metadata": "0.2.2",
|
|
180
212
|
"rxjs": "7.8.2"
|
|
181
213
|
},
|
|
182
214
|
"scripts": {
|
|
183
|
-
"typecheck": "tsc
|
|
215
|
+
"typecheck": "tsc --project tsconfig.test.json",
|
|
184
216
|
"build": "tsdown"
|
|
185
217
|
}
|
|
186
218
|
}
|