@plantops/auth-kit 0.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.
Files changed (42) hide show
  1. package/README.md +7 -0
  2. package/dist/adapters/fetch/index.d.ts +38 -0
  3. package/dist/adapters/fetch/index.d.ts.map +1 -0
  4. package/dist/adapters/fetch/index.js +52 -0
  5. package/dist/adapters/nestjs/auth.guard.d.ts +120 -0
  6. package/dist/adapters/nestjs/auth.guard.d.ts.map +1 -0
  7. package/dist/adapters/nestjs/auth.guard.js +165 -0
  8. package/dist/adapters/nestjs/index.d.ts +10 -0
  9. package/dist/adapters/nestjs/index.d.ts.map +1 -0
  10. package/dist/adapters/nestjs/index.js +12 -0
  11. package/dist/adapters/nestjs/permission.guard.d.ts +115 -0
  12. package/dist/adapters/nestjs/permission.guard.d.ts.map +1 -0
  13. package/dist/adapters/nestjs/permission.guard.js +167 -0
  14. package/dist/adapters/nestjs/require-permission.decorator.d.ts +31 -0
  15. package/dist/adapters/nestjs/require-permission.decorator.d.ts.map +1 -0
  16. package/dist/adapters/nestjs/require-permission.decorator.js +41 -0
  17. package/dist/adapters/nestjs/scope-resolver.d.ts +12 -0
  18. package/dist/adapters/nestjs/scope-resolver.d.ts.map +1 -0
  19. package/dist/adapters/nestjs/scope-resolver.js +24 -0
  20. package/dist/core/claims.d.ts +114 -0
  21. package/dist/core/claims.d.ts.map +1 -0
  22. package/dist/core/claims.js +183 -0
  23. package/dist/core/index.d.ts +13 -0
  24. package/dist/core/index.d.ts.map +1 -0
  25. package/dist/core/index.js +15 -0
  26. package/dist/core/jwks-verifier.d.ts +78 -0
  27. package/dist/core/jwks-verifier.d.ts.map +1 -0
  28. package/dist/core/jwks-verifier.js +183 -0
  29. package/dist/core/jws.d.ts +96 -0
  30. package/dist/core/jws.d.ts.map +1 -0
  31. package/dist/core/jws.js +183 -0
  32. package/dist/core/revocation-cache.d.ts +79 -0
  33. package/dist/core/revocation-cache.d.ts.map +1 -0
  34. package/dist/core/revocation-cache.js +68 -0
  35. package/dist/core/scope-resolver.d.ts +235 -0
  36. package/dist/core/scope-resolver.d.ts.map +1 -0
  37. package/dist/core/scope-resolver.js +206 -0
  38. package/dist/index.d.ts +16 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +18 -0
  41. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  42. package/package.json +64 -0
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # auth-kit
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Running unit tests
6
+
7
+ Run `nx test auth-kit` to execute the unit tests via [Jest](https://jestjs.io).
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The fetch adapter (roadmap Session 50): authorization for runtimes that
3
+ * have a Request and no Nest container — Next.js route handlers, workers,
4
+ * any WinterCG runtime.
5
+ *
6
+ * Deliberately smaller than the Nest guard: no decorators, no metadata, no
7
+ * denial audit. It answers the question a route handler actually has — who is
8
+ * this, and may they proceed? — and leaves policy to the caller. The pieces it
9
+ * delegates to are the same framework-free core the Nest guard uses, so a
10
+ * module and the IAM can never disagree about what a token means.
11
+ */
12
+ import { type RevocationChecker, type TokenVerifier } from '../../core';
13
+ export interface FetchAuthOptions {
14
+ /** Verifies the bearer token. JwksVerifier for every non-IAM process. */
15
+ verifier: TokenVerifier;
16
+ /**
17
+ * Answers whether a session is still live. Optional: without one,
18
+ * revocation is not checked and the decision rests on signature alone.
19
+ */
20
+ revocations?: RevocationChecker;
21
+ }
22
+ export type FetchDecision<V extends TokenVerifier = TokenVerifier> = {
23
+ ok: true;
24
+ claims: Awaited<ReturnType<V['verify']>>;
25
+ } | {
26
+ ok: false;
27
+ status: 401;
28
+ code: string;
29
+ };
30
+ /**
31
+ * Verifies the bearer token of a fetch-style Request, and checks its session
32
+ * against revocations when a checker was provided.
33
+ *
34
+ * Every refusal is a bare 401 with the closed error code — the reason stays
35
+ * server-side (Doc 03 §6), exactly as the Nest guard draws the boundary.
36
+ */
37
+ export declare function authorizeRequest(request: Request, options: FetchAuthOptions): Promise<FetchDecision>;
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/fetch/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,QAAQ,EAAE,aAAa,CAAC;IACxB;;;OAGG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,IAC7D;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAS7C;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,aAAa,CAAC,CAuBxB"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * The fetch adapter (roadmap Session 50): authorization for runtimes that
4
+ * have a Request and no Nest container — Next.js route handlers, workers,
5
+ * any WinterCG runtime.
6
+ *
7
+ * Deliberately smaller than the Nest guard: no decorators, no metadata, no
8
+ * denial audit. It answers the question a route handler actually has — who is
9
+ * this, and may they proceed? — and leaves policy to the caller. The pieces it
10
+ * delegates to are the same framework-free core the Nest guard uses, so a
11
+ * module and the IAM can never disagree about what a token means.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.authorizeRequest = authorizeRequest;
15
+ const core_1 = require("../../core");
16
+ function bearerOf(request) {
17
+ const header = request.headers.get('authorization');
18
+ if (header === null)
19
+ return undefined;
20
+ const match = /^Bearer (.+)$/.exec(header);
21
+ return match === null ? undefined : match[1];
22
+ }
23
+ /**
24
+ * Verifies the bearer token of a fetch-style Request, and checks its session
25
+ * against revocations when a checker was provided.
26
+ *
27
+ * Every refusal is a bare 401 with the closed error code — the reason stays
28
+ * server-side (Doc 03 §6), exactly as the Nest guard draws the boundary.
29
+ */
30
+ async function authorizeRequest(request, options) {
31
+ const token = bearerOf(request);
32
+ if (token === undefined) {
33
+ return { ok: false, status: 401, code: "no-bearer-token" };
34
+ }
35
+ let claims;
36
+ try {
37
+ claims = await options.verifier.verify(token);
38
+ }
39
+ catch (error) {
40
+ if (error instanceof core_1.TokenVerificationError) {
41
+ void core_1.TokenRejection;
42
+ return { ok: false, status: 401, code: "invalid-token" };
43
+ }
44
+ throw error;
45
+ }
46
+ if (options.revocations !== undefined) {
47
+ const revoked = await options.revocations.isRevoked(claims.sid);
48
+ if (revoked)
49
+ return { ok: false, status: 401, code: "session-revoked" };
50
+ }
51
+ return { ok: true, claims };
52
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The guard every authenticated request passes through (Doc 03 §6).
3
+ *
4
+ * Three checks, in this order, and the order is the design:
5
+ *
6
+ * 1. **Signature and claims** — delegated to a {@link TokenVerifier}. The IAM
7
+ * verifies with its own keys; a module verifies against the fetched JWKS.
8
+ * 2. **Revocation** — `sid` against the shared cache, so a force-logout lands
9
+ * within seconds without a database round-trip on the happy path.
10
+ * 3. **Hand-off** — the verified claims are given to a {@link VerifiedClaimsSink},
11
+ * which is the *only* way claims reach the RLS context (Doc 07 §5).
12
+ *
13
+ * Revocation runs after verification, never before: a `sid` read out of an
14
+ * unverified token is attacker-chosen, and looking it up would leak whether a
15
+ * guessed session exists through response timing.
16
+ *
17
+ * ## Deny by default
18
+ *
19
+ * The guard is registered app-wide and a route is authenticated unless it says
20
+ * otherwise with {@link Public}. That direction matters. An opt-in guard makes
21
+ * every new controller unprotected until someone remembers a decorator, and the
22
+ * symptom of forgetting is a route that works — which is the failure mode no
23
+ * test ever catches, because working is what tests assert.
24
+ *
25
+ * `@Public()` is for the routes that genuinely cannot carry a token: login and
26
+ * refresh (there is none yet), the JWKS (the thing that verifies tokens), and
27
+ * the ops probes.
28
+ *
29
+ * ## Everything is a bare 401
30
+ *
31
+ * A malformed token, a forged `kid`, an expired token and a revoked session all
32
+ * produce the same `AUTH_REQUIRED`. The reason is kept server-side on
33
+ * {@link TokenVerificationError} for logs and metrics — telling the caller
34
+ * which check failed tells an attacker whether a forged `kid` was a near miss,
35
+ * or whether a guessed `sid` exists.
36
+ */
37
+ import { type CanActivate, type ExecutionContext } from '@nestjs/common';
38
+ import { Reflector } from '@nestjs/core';
39
+ import type { JwtClaims } from '@plantops/contracts';
40
+ import type { TokenVerifier } from '../../core/jwks-verifier';
41
+ import type { RevocationChecker } from '../../core/revocation-cache';
42
+ /** Marks a route reachable without a bearer token. */
43
+ export declare const IS_PUBLIC_METADATA = "auth-kit:public";
44
+ /**
45
+ * Opts a route out of authentication.
46
+ *
47
+ * Use sparingly and say why at the call site: every one of these is a route
48
+ * with no subject, and therefore no RLS context and no tenant.
49
+ */
50
+ export declare const Public: () => import("@nestjs/common").CustomDecorator<string>;
51
+ /**
52
+ * Receives claims the guard has fully verified.
53
+ *
54
+ * The indirection exists because of a boundary: `auth-kit` may not import
55
+ * `@plantops/db` (Doc 08 §2), and it is `@plantops/db` that owns the branded
56
+ * `VerifiedClaims` type the RLS context accepts. So the host application
57
+ * supplies the one adapter that brands them, and that adapter is the single
58
+ * place in the system where an unverified object could ever become a verified
59
+ * one — a thing far easier to review in one file than in a guard shared by
60
+ * every module.
61
+ */
62
+ export interface VerifiedClaimsSink {
63
+ accept(request: unknown, claims: JwtClaims): void;
64
+ }
65
+ /** What to do when the revocation cache cannot answer. */
66
+ export interface RevocationFallback {
67
+ /**
68
+ * The authoritative answer, from the database.
69
+ *
70
+ * Present only in the IAM, which owns the `session` table. A module has no
71
+ * such table and therefore no fallback — see {@link AuthGuardOptions}.
72
+ *
73
+ * It receives the whole claim set, not just `sid`, because the right answer
74
+ * depends on `sty`: a service token's `sid` is ephemeral and backed by no
75
+ * row at all (Doc 03 §5), so looking it up would find nothing and read that
76
+ * absence as revoked — killing every machine identity the moment Redis
77
+ * blinked.
78
+ *
79
+ * @throws when it too cannot answer.
80
+ */
81
+ isRevoked(claims: JwtClaims): Promise<boolean>;
82
+ }
83
+ export interface AuthGuardOptions {
84
+ /**
85
+ * What a request means when neither the cache nor the fallback can answer.
86
+ *
87
+ * `'deny'` (the default) is correct: uncertainty about revocation must fall
88
+ * to refusal, or a Redis outage becomes a window in which every revoked
89
+ * session works again. `'allow'` exists for a deployment that would rather
90
+ * survive a cache outage than hold revocation — a real trade, but one that
91
+ * has to be made explicitly and written down, never inherited from a default.
92
+ */
93
+ onRevocationUnavailable?: 'deny' | 'allow';
94
+ }
95
+ export declare const TOKEN_VERIFIER: unique symbol;
96
+ export declare const REVOCATION_CHECKER: unique symbol;
97
+ export declare const VERIFIED_CLAIMS_SINK: unique symbol;
98
+ export declare const REVOCATION_FALLBACK: unique symbol;
99
+ export declare const AUTH_GUARD_OPTIONS: unique symbol;
100
+ export declare class AuthGuard implements CanActivate {
101
+ private readonly reflector;
102
+ private readonly verifier;
103
+ private readonly revocations;
104
+ private readonly sink;
105
+ private readonly fallback;
106
+ private readonly options;
107
+ private readonly logger;
108
+ constructor(reflector: Reflector, verifier: TokenVerifier, revocations: RevocationChecker, sink: VerifiedClaimsSink, fallback?: RevocationFallback | null, options?: AuthGuardOptions);
109
+ canActivate(context: ExecutionContext): Promise<boolean>;
110
+ /**
111
+ * Cache first; database only when the cache could not answer.
112
+ *
113
+ * The happy path is one Redis `EXISTS` and no SQL, which is what makes this
114
+ * affordable on every request (Doc 03 §6).
115
+ */
116
+ private isRevoked;
117
+ /** One shape of refusal, with the reason kept out of the response. */
118
+ private refuse;
119
+ }
120
+ //# sourceMappingURL=auth.guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.guard.d.ts","sourceRoot":"","sources":["../../../src/adapters/nestjs/auth.guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EAOtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAErE,sDAAsD;AACtD,eAAO,MAAM,kBAAkB,oBAAoB,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,MAAM,wDAA8C,CAAC;AAElE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;CACnD;AAED,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;OAQG;IACH,uBAAuB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5C;AAED,eAAO,MAAM,cAAc,eAAmC,CAAC;AAC/D,eAAO,MAAM,kBAAkB,eAAuC,CAAC;AACvE,eAAO,MAAM,oBAAoB,eAAwC,CAAC;AAC1E,eAAO,MAAM,mBAAmB,eAAwC,CAAC;AACzE,eAAO,MAAM,kBAAkB,eAAsC,CAAC;AAEtE,qBACa,SAAU,YAAW,WAAW;IAIzC,OAAO,CAAC,QAAQ,CAAC,SAAS;IACF,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACrB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAOnD,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAGzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAhB1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;gBAGlC,SAAS,EAAE,SAAS,EACI,QAAQ,EAAE,aAAa,EACnB,WAAW,EAAE,iBAAiB,EAC5B,IAAI,EAAE,kBAAkB,EAOtD,QAAQ,GAAE,kBAAkB,GAAG,IAAW,EAG1C,OAAO,GAAE,gBAAqB;IAG3C,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoC9D;;;;;OAKG;YACW,SAAS;IAwBvB,sEAAsE;IACtE,OAAO,CAAC,MAAM;CAQf"}
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ /**
3
+ * The guard every authenticated request passes through (Doc 03 §6).
4
+ *
5
+ * Three checks, in this order, and the order is the design:
6
+ *
7
+ * 1. **Signature and claims** — delegated to a {@link TokenVerifier}. The IAM
8
+ * verifies with its own keys; a module verifies against the fetched JWKS.
9
+ * 2. **Revocation** — `sid` against the shared cache, so a force-logout lands
10
+ * within seconds without a database round-trip on the happy path.
11
+ * 3. **Hand-off** — the verified claims are given to a {@link VerifiedClaimsSink},
12
+ * which is the *only* way claims reach the RLS context (Doc 07 §5).
13
+ *
14
+ * Revocation runs after verification, never before: a `sid` read out of an
15
+ * unverified token is attacker-chosen, and looking it up would leak whether a
16
+ * guessed session exists through response timing.
17
+ *
18
+ * ## Deny by default
19
+ *
20
+ * The guard is registered app-wide and a route is authenticated unless it says
21
+ * otherwise with {@link Public}. That direction matters. An opt-in guard makes
22
+ * every new controller unprotected until someone remembers a decorator, and the
23
+ * symptom of forgetting is a route that works — which is the failure mode no
24
+ * test ever catches, because working is what tests assert.
25
+ *
26
+ * `@Public()` is for the routes that genuinely cannot carry a token: login and
27
+ * refresh (there is none yet), the JWKS (the thing that verifies tokens), and
28
+ * the ops probes.
29
+ *
30
+ * ## Everything is a bare 401
31
+ *
32
+ * A malformed token, a forged `kid`, an expired token and a revoked session all
33
+ * produce the same `AUTH_REQUIRED`. The reason is kept server-side on
34
+ * {@link TokenVerificationError} for logs and metrics — telling the caller
35
+ * which check failed tells an attacker whether a forged `kid` was a near miss,
36
+ * or whether a guessed `sid` exists.
37
+ */
38
+ var AuthGuard_1;
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.AuthGuard = exports.AUTH_GUARD_OPTIONS = exports.REVOCATION_FALLBACK = exports.VERIFIED_CLAIMS_SINK = exports.REVOCATION_CHECKER = exports.TOKEN_VERIFIER = exports.Public = exports.IS_PUBLIC_METADATA = void 0;
41
+ const tslib_1 = require("tslib");
42
+ const common_1 = require("@nestjs/common");
43
+ const core_1 = require("@nestjs/core");
44
+ const claims_1 = require("../../core/claims");
45
+ /** Marks a route reachable without a bearer token. */
46
+ exports.IS_PUBLIC_METADATA = 'auth-kit:public';
47
+ /**
48
+ * Opts a route out of authentication.
49
+ *
50
+ * Use sparingly and say why at the call site: every one of these is a route
51
+ * with no subject, and therefore no RLS context and no tenant.
52
+ */
53
+ const Public = () => (0, common_1.SetMetadata)(exports.IS_PUBLIC_METADATA, true);
54
+ exports.Public = Public;
55
+ exports.TOKEN_VERIFIER = Symbol('auth-kit:TokenVerifier');
56
+ exports.REVOCATION_CHECKER = Symbol('auth-kit:RevocationChecker');
57
+ exports.VERIFIED_CLAIMS_SINK = Symbol('auth-kit:VerifiedClaimsSink');
58
+ exports.REVOCATION_FALLBACK = Symbol('auth-kit:RevocationFallback');
59
+ exports.AUTH_GUARD_OPTIONS = Symbol('auth-kit:AuthGuardOptions');
60
+ let AuthGuard = AuthGuard_1 = class AuthGuard {
61
+ constructor(reflector, verifier, revocations, sink, fallback = null, options = {}) {
62
+ this.reflector = reflector;
63
+ this.verifier = verifier;
64
+ this.revocations = revocations;
65
+ this.sink = sink;
66
+ this.fallback = fallback;
67
+ this.options = options;
68
+ this.logger = new common_1.Logger(AuthGuard_1.name);
69
+ }
70
+ async canActivate(context) {
71
+ if (context.getType() !== 'http')
72
+ return true;
73
+ const isPublic = this.reflector.getAllAndOverride(exports.IS_PUBLIC_METADATA, [
74
+ context.getHandler(),
75
+ context.getClass(),
76
+ ]);
77
+ // A public route is let through with **no** claims attached, so the
78
+ // transaction wrapper applies no RLS context and every tenant policy
79
+ // matches nothing. Public means unauthenticated, not privileged.
80
+ if (isPublic)
81
+ return true;
82
+ const request = context.switchToHttp().getRequest();
83
+ const token = bearerTokenOf(request.headers['authorization']);
84
+ if (token === undefined)
85
+ throw new common_1.UnauthorizedException('Authentication is required');
86
+ let claims;
87
+ try {
88
+ claims = await this.verifier.verify(token);
89
+ }
90
+ catch (error) {
91
+ throw this.refuse(error);
92
+ }
93
+ if (await this.isRevoked(claims)) {
94
+ throw this.refuse(new claims_1.TokenVerificationError(claims_1.TokenRejection.REVOKED, 'Session has been revoked'));
95
+ }
96
+ this.sink.accept(request, claims);
97
+ return true;
98
+ }
99
+ /**
100
+ * Cache first; database only when the cache could not answer.
101
+ *
102
+ * The happy path is one Redis `EXISTS` and no SQL, which is what makes this
103
+ * affordable on every request (Doc 03 §6).
104
+ */
105
+ async isRevoked(claims) {
106
+ try {
107
+ return await this.revocations.isRevoked(claims.sid);
108
+ }
109
+ catch (cacheError) {
110
+ this.logger.warn(`Revocation cache unavailable (${messageOf(cacheError)}); ` +
111
+ (this.fallback ? 'falling back to the database' : 'no database fallback'));
112
+ if (this.fallback) {
113
+ try {
114
+ return await this.fallback.isRevoked(claims);
115
+ }
116
+ catch (dbError) {
117
+ this.logger.error(`Revocation fallback also failed (${messageOf(dbError)})`);
118
+ }
119
+ }
120
+ // Both sources are down. Deny unless the deployment said otherwise.
121
+ return (this.options.onRevocationUnavailable ?? 'deny') === 'deny';
122
+ }
123
+ }
124
+ /** One shape of refusal, with the reason kept out of the response. */
125
+ refuse(error) {
126
+ if (error instanceof claims_1.TokenVerificationError) {
127
+ this.logger.debug(`Token refused: ${error.reason}`);
128
+ }
129
+ else {
130
+ this.logger.warn(`Token verification failed: ${messageOf(error)}`);
131
+ }
132
+ return new common_1.UnauthorizedException('Authentication is required');
133
+ }
134
+ };
135
+ exports.AuthGuard = AuthGuard;
136
+ exports.AuthGuard = AuthGuard = AuthGuard_1 = tslib_1.__decorate([
137
+ (0, common_1.Injectable)(),
138
+ tslib_1.__param(1, (0, common_1.Inject)(exports.TOKEN_VERIFIER)),
139
+ tslib_1.__param(2, (0, common_1.Inject)(exports.REVOCATION_CHECKER)),
140
+ tslib_1.__param(3, (0, common_1.Inject)(exports.VERIFIED_CLAIMS_SINK)),
141
+ tslib_1.__param(4, (0, common_1.Optional)()),
142
+ tslib_1.__param(4, (0, common_1.Inject)(exports.REVOCATION_FALLBACK)),
143
+ tslib_1.__param(5, (0, common_1.Optional)()),
144
+ tslib_1.__param(5, (0, common_1.Inject)(exports.AUTH_GUARD_OPTIONS)),
145
+ tslib_1.__metadata("design:paramtypes", [core_1.Reflector, Object, Object, Object, Object, Object])
146
+ ], AuthGuard);
147
+ /**
148
+ * The token from an `Authorization` header, or `undefined`.
149
+ *
150
+ * The scheme is matched case-insensitively (RFC 7235 makes it so) but the token
151
+ * is taken verbatim: trimming or re-casing it would change the bytes the
152
+ * signature covers. A repeated header arrives as an array — ambiguous, and
153
+ * therefore refused rather than resolved by picking one.
154
+ */
155
+ function bearerTokenOf(header) {
156
+ if (typeof header !== 'string')
157
+ return undefined;
158
+ const [scheme, ...rest] = header.split(' ');
159
+ if (scheme?.toLowerCase() !== 'bearer' || rest.length !== 1)
160
+ return undefined;
161
+ return rest[0] === '' ? undefined : rest[0];
162
+ }
163
+ function messageOf(error) {
164
+ return error instanceof Error ? error.message : String(error);
165
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The NestJS adapter: guards, decorators and the injectable resolver,
3
+ * re-exported exactly as they were before Session 50 split them out of a flat
4
+ * directory. Consumers import from the package barrel and see no difference.
5
+ */
6
+ export * from './auth.guard';
7
+ export * from './permission.guard';
8
+ export * from './require-permission.decorator';
9
+ export * from './scope-resolver';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/nestjs/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /**
3
+ * The NestJS adapter: guards, decorators and the injectable resolver,
4
+ * re-exported exactly as they were before Session 50 split them out of a flat
5
+ * directory. Consumers import from the package barrel and see no difference.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const tslib_1 = require("tslib");
9
+ tslib_1.__exportStar(require("./auth.guard"), exports);
10
+ tslib_1.__exportStar(require("./permission.guard"), exports);
11
+ tslib_1.__exportStar(require("./require-permission.decorator"), exports);
12
+ tslib_1.__exportStar(require("./scope-resolver"), exports);
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The guard that enforces `@RequirePermission` (Doc 04 §8, Doc 08 §4).
3
+ *
4
+ * `AuthGuard` establishes **who** is calling. This one establishes **whether
5
+ * they may** — the WHO × WHAT × WHERE question of Doc 04 §1, asked once per
6
+ * request, before the handler and before the transaction that would carry out
7
+ * whatever it was going to do.
8
+ *
9
+ * Four steps, and each one is a line of Doc 04 §8's contract:
10
+ *
11
+ * 1. Read the route's requirement. No requirement and no explicit opt-out is a
12
+ * refusal — see {@link RequirePermission} for why the direction is that way
13
+ * round.
14
+ * 2. Load the subject's resolved grants, through {@link GrantsSource}.
15
+ * 3. Confirm the permission is held, and — where the route names a scope node —
16
+ * that the subject's grants cover it.
17
+ * 4. Refuse with `PERMISSION_DENIED` or `SCOPE_DENIED`, and audit the attempt.
18
+ *
19
+ * Steps 2 and 3 are {@link ScopeResolver.decide}, which is where the rule lives
20
+ * and where it is unit-tested. What is left here is the framework adapter: read
21
+ * metadata, read the request, translate an outcome into an exception. That
22
+ * split is deliberate — a guard is the one place a test has to build an
23
+ * `ExecutionContext` to reach, and authorization logic should not be behind
24
+ * that.
25
+ *
26
+ * ## Which connection step 2 runs on
27
+ *
28
+ * Not this file's decision, and deliberately not this file's problem.
29
+ * `docs/adr/0001-permission-guard-connection-strategy.md` settles it: a guard
30
+ * runs *before* the per-request transaction exists (Nest runs guards ahead of
31
+ * interceptors), so on a grants-cache miss the IAM's {@link GrantsSource} opens
32
+ * its own `QueryRunner`, applies the RLS context from the verified claims,
33
+ * resolves on it, and commits and releases in a `finally`. It does **not** open,
34
+ * reuse or leave open the request transaction: a guard has no "after" phase in
35
+ * which to close one, so `TenantContextInterceptor` stays its sole owner.
36
+ *
37
+ * `auth-kit` may depend on `@plantops/contracts` and nothing else (Doc 08 §2),
38
+ * so it could not name a `DataSource` here in any case — which is why the port
39
+ * exists and why a future module can satisfy it with a cached HTTP call instead.
40
+ *
41
+ * ## Every denial is audited, and a failure to audit is not a failure to deny
42
+ *
43
+ * Doc 04 §8 step 5 and Doc 10 §3: the attempt is recorded with the permission
44
+ * that was wanted and the target that was named. The IAM binds
45
+ * {@link DENIAL_AUDITOR} to `AuditService.recordDenial`, which commits on its
46
+ * own connection precisely because the request it accompanies is about to be
47
+ * rolled back by its own 403 — and which never throws, because turning a lost
48
+ * audit row into a 500 would tell a caller which requests were refused for which
49
+ * reason.
50
+ *
51
+ * The auditor is optional: a downstream module has no `audit_trail` table and
52
+ * records nothing. Binding it is the IAM's business.
53
+ *
54
+ * ## The two refusals say different things, and neither says whether the target
55
+ * exists
56
+ *
57
+ * `PERMISSION_DENIED` means the subject does not hold it anywhere;
58
+ * `SCOPE_DENIED` means they hold it, but not over the node they named. A node
59
+ * belonging to another tenant produces `SCOPE_DENIED` too — indistinguishable
60
+ * from one they simply do not cover, because RLS makes it so and Doc 06 §2
61
+ * requires that a denial never reveal cross-tenant existence.
62
+ */
63
+ import { type CanActivate, type ExecutionContext, ForbiddenException } from '@nestjs/common';
64
+ import { Reflector } from '@nestjs/core';
65
+ import { IamErrorCode, type PermissionKey } from '@plantops/contracts';
66
+ import { ScopeResolver } from './scope-resolver';
67
+ import { AuthorizationOutcome, type SubjectClaims } from '../../core/scope-resolver';
68
+ /** The two 403 codes of Doc 06 §2, and no others. */
69
+ export type DenialErrorCode = typeof IamErrorCode.PERMISSION_DENIED | typeof IamErrorCode.SCOPE_DENIED;
70
+ /**
71
+ * A 403 that knows which of Doc 06 §2's two codes it is.
72
+ *
73
+ * `ForbiddenException` alone would come back as `PERMISSION_DENIED`, because
74
+ * that is the less specific of the two and the only one a bare status can imply
75
+ * (`http-exception.filter.ts`). The distinction is worth carrying: a client that
76
+ * sees `SCOPE_DENIED` can tell its user *where* they lack access, which is the
77
+ * one actionable thing about a refusal.
78
+ *
79
+ * `IamErrorCode` is a `@plantops/contracts` export, so naming it here crosses no
80
+ * boundary — the code table is part of the published contract, not of the IAM.
81
+ */
82
+ export declare class AuthorizationDeniedException extends ForbiddenException {
83
+ readonly code: DenialErrorCode;
84
+ constructor(code: DenialErrorCode, message: string);
85
+ /** The subject does not hold the permission at all. */
86
+ static permission(): AuthorizationDeniedException;
87
+ /** They hold it, but not over the node they named. */
88
+ static scope(): AuthorizationDeniedException;
89
+ }
90
+ /** How the guard reads back the claims `AuthGuard` verified. */
91
+ export interface VerifiedClaimsSource {
92
+ claimsOf(request: unknown): SubjectClaims | undefined;
93
+ }
94
+ /** Records a refused attempt (Doc 10 §3). Must not throw. */
95
+ export interface DenialAuditor {
96
+ recordDenial(claims: SubjectClaims, outcome: Exclude<AuthorizationOutcome, 'allowed'>,
97
+ /**
98
+ * The keys the refusal was about — one on all but the routes that admit
99
+ * either tier's key, and never empty. `AuthorizationDecision` in
100
+ * `scope-resolver.ts` says which keys a given outcome names.
101
+ */
102
+ permissions: readonly PermissionKey[], scopeNodeId?: string): Promise<void>;
103
+ }
104
+ export declare const VERIFIED_CLAIMS_SOURCE: unique symbol;
105
+ export declare const DENIAL_AUDITOR: unique symbol;
106
+ export declare class PermissionGuard implements CanActivate {
107
+ private readonly reflector;
108
+ private readonly resolver;
109
+ private readonly claims;
110
+ private readonly auditor;
111
+ private readonly logger;
112
+ constructor(reflector: Reflector, resolver: ScopeResolver, claims: VerifiedClaimsSource, auditor?: DenialAuditor | null);
113
+ canActivate(context: ExecutionContext): Promise<boolean>;
114
+ }
115
+ //# sourceMappingURL=permission.guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.guard.d.ts","sourceRoot":"","sources":["../../../src/adapters/nestjs/permission.guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,kBAAkB,EAKnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAQvE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,KAAK,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAErF,qDAAqD;AACrD,MAAM,MAAM,eAAe,GACvB,OAAO,YAAY,CAAC,iBAAiB,GACrC,OAAO,YAAY,CAAC,YAAY,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,qBAAa,4BAA6B,SAAQ,kBAAkB;IAEhE,QAAQ,CAAC,IAAI,EAAE,eAAe;gBAArB,IAAI,EAAE,eAAe,EAC9B,OAAO,EAAE,MAAM;IAMjB,uDAAuD;IACvD,MAAM,CAAC,UAAU,IAAI,4BAA4B;IAOjD,sDAAsD;IACtD,MAAM,CAAC,KAAK,IAAI,4BAA4B;CAM7C;AAED,gEAAgE;AAChE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAAC;CACvD;AAED,6DAA6D;AAC7D,MAAM,WAAW,aAAa;IAC5B,YAAY,CACV,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,OAAO,CAAC,oBAAoB,EAAE,SAAS,CAAC;IACjD;;;;OAIG;IACH,WAAW,EAAE,SAAS,aAAa,EAAE,EACrC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,eAAO,MAAM,sBAAsB,eAA0C,CAAC;AAC9E,eAAO,MAAM,cAAc,eAAmC,CAAC;AAE/D,qBACa,eAAgB,YAAW,WAAW;IAI/C,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACO,OAAO,CAAC,QAAQ,CAAC,MAAM;IAGvD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;gBAGxC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,aAAa,EACS,MAAM,EAAE,oBAAoB,EAG5D,OAAO,GAAE,aAAa,GAAG,IAAW;IAGjD,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CAiE/D"}