@heyatom/bastion-client 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 (87) hide show
  1. package/README.md +80 -0
  2. package/dist/cjs/core/bastion-http.d.ts +32 -0
  3. package/dist/cjs/core/bastion-http.js +47 -0
  4. package/dist/cjs/core/errors.d.ts +11 -0
  5. package/dist/cjs/core/errors.js +23 -0
  6. package/dist/cjs/core/index.d.ts +5 -0
  7. package/dist/cjs/core/index.js +25 -0
  8. package/dist/cjs/core/jwks-verifier.d.ts +35 -0
  9. package/dist/cjs/core/jwks-verifier.js +118 -0
  10. package/dist/cjs/core/service-token-provider.d.ts +21 -0
  11. package/dist/cjs/core/service-token-provider.js +47 -0
  12. package/dist/cjs/core/types.d.ts +48 -0
  13. package/dist/cjs/core/types.js +12 -0
  14. package/dist/cjs/nest/bastion-audit.service.d.ts +29 -0
  15. package/dist/cjs/nest/bastion-audit.service.js +97 -0
  16. package/dist/cjs/nest/bastion-jwks.service.d.ts +8 -0
  17. package/dist/cjs/nest/bastion-jwks.service.js +49 -0
  18. package/dist/cjs/nest/bastion.module.d.ts +20 -0
  19. package/dist/cjs/nest/bastion.module.js +60 -0
  20. package/dist/cjs/nest/bastion.service.d.ts +18 -0
  21. package/dist/cjs/nest/bastion.service.js +57 -0
  22. package/dist/cjs/nest/decorators/audit.decorator.d.ts +15 -0
  23. package/dist/cjs/nest/decorators/audit.decorator.js +8 -0
  24. package/dist/cjs/nest/decorators/current-user.decorator.d.ts +4 -0
  25. package/dist/cjs/nest/decorators/current-user.decorator.js +8 -0
  26. package/dist/cjs/nest/decorators/public.decorator.d.ts +3 -0
  27. package/dist/cjs/nest/decorators/public.decorator.js +8 -0
  28. package/dist/cjs/nest/decorators/require-scope.decorator.d.ts +3 -0
  29. package/dist/cjs/nest/decorators/require-scope.decorator.js +8 -0
  30. package/dist/cjs/nest/guards/bastion-user.guard.d.ts +20 -0
  31. package/dist/cjs/nest/guards/bastion-user.guard.js +87 -0
  32. package/dist/cjs/nest/guards/service-client-jwt.guard.d.ts +17 -0
  33. package/dist/cjs/nest/guards/service-client-jwt.guard.js +66 -0
  34. package/dist/cjs/nest/index.d.ts +14 -0
  35. package/dist/cjs/nest/index.js +49 -0
  36. package/dist/cjs/nest/interceptors/audit.interceptor.d.ts +12 -0
  37. package/dist/cjs/nest/interceptors/audit.interceptor.js +51 -0
  38. package/dist/cjs/nest/nest-logger.d.ts +2 -0
  39. package/dist/cjs/nest/nest-logger.js +12 -0
  40. package/dist/cjs/nest/options.d.ts +24 -0
  41. package/dist/cjs/nest/options.js +4 -0
  42. package/dist/cjs/nest/tenant-scope.d.ts +4 -0
  43. package/dist/cjs/nest/tenant-scope.js +14 -0
  44. package/dist/esm/core/bastion-http.d.ts +32 -0
  45. package/dist/esm/core/bastion-http.js +43 -0
  46. package/dist/esm/core/errors.d.ts +11 -0
  47. package/dist/esm/core/errors.js +18 -0
  48. package/dist/esm/core/index.d.ts +5 -0
  49. package/dist/esm/core/index.js +5 -0
  50. package/dist/esm/core/jwks-verifier.d.ts +35 -0
  51. package/dist/esm/core/jwks-verifier.js +114 -0
  52. package/dist/esm/core/service-token-provider.d.ts +21 -0
  53. package/dist/esm/core/service-token-provider.js +43 -0
  54. package/dist/esm/core/types.d.ts +48 -0
  55. package/dist/esm/core/types.js +8 -0
  56. package/dist/esm/nest/bastion-audit.service.d.ts +29 -0
  57. package/dist/esm/nest/bastion-audit.service.js +94 -0
  58. package/dist/esm/nest/bastion-jwks.service.d.ts +8 -0
  59. package/dist/esm/nest/bastion-jwks.service.js +46 -0
  60. package/dist/esm/nest/bastion.module.d.ts +20 -0
  61. package/dist/esm/nest/bastion.module.js +57 -0
  62. package/dist/esm/nest/bastion.service.d.ts +18 -0
  63. package/dist/esm/nest/bastion.service.js +54 -0
  64. package/dist/esm/nest/decorators/audit.decorator.d.ts +15 -0
  65. package/dist/esm/nest/decorators/audit.decorator.js +4 -0
  66. package/dist/esm/nest/decorators/current-user.decorator.d.ts +4 -0
  67. package/dist/esm/nest/decorators/current-user.decorator.js +5 -0
  68. package/dist/esm/nest/decorators/public.decorator.d.ts +3 -0
  69. package/dist/esm/nest/decorators/public.decorator.js +4 -0
  70. package/dist/esm/nest/decorators/require-scope.decorator.d.ts +3 -0
  71. package/dist/esm/nest/decorators/require-scope.decorator.js +4 -0
  72. package/dist/esm/nest/guards/bastion-user.guard.d.ts +20 -0
  73. package/dist/esm/nest/guards/bastion-user.guard.js +84 -0
  74. package/dist/esm/nest/guards/service-client-jwt.guard.d.ts +17 -0
  75. package/dist/esm/nest/guards/service-client-jwt.guard.js +63 -0
  76. package/dist/esm/nest/index.d.ts +14 -0
  77. package/dist/esm/nest/index.js +14 -0
  78. package/dist/esm/nest/interceptors/audit.interceptor.d.ts +12 -0
  79. package/dist/esm/nest/interceptors/audit.interceptor.js +48 -0
  80. package/dist/esm/nest/nest-logger.d.ts +2 -0
  81. package/dist/esm/nest/nest-logger.js +9 -0
  82. package/dist/esm/nest/options.d.ts +24 -0
  83. package/dist/esm/nest/options.js +1 -0
  84. package/dist/esm/nest/tenant-scope.d.ts +4 -0
  85. package/dist/esm/nest/tenant-scope.js +10 -0
  86. package/dist/esm/package.json +1 -0
  87. package/package.json +80 -0
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var BastionJwksService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.BastionJwksService = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const jwks_verifier_js_1 = require("../core/jwks-verifier.js");
19
+ const errors_js_1 = require("../core/errors.js");
20
+ const options_js_1 = require("./options.js");
21
+ const nest_logger_js_1 = require("./nest-logger.js");
22
+ /** Verifies inbound Bastion tokens against the cached JWKS. Throws `UnauthorizedException` on any failure. */
23
+ let BastionJwksService = BastionJwksService_1 = class BastionJwksService {
24
+ verifier;
25
+ constructor(options) {
26
+ this.verifier = new jwks_verifier_js_1.JwksVerifier({
27
+ baseUrl: options.baseUrl,
28
+ ttlMs: options.jwksTtlMs,
29
+ issuer: options.issuer,
30
+ logger: (0, nest_logger_js_1.nestLogger)(BastionJwksService_1.name),
31
+ });
32
+ }
33
+ async verify(token) {
34
+ try {
35
+ return await this.verifier.verify(token);
36
+ }
37
+ catch (err) {
38
+ if (err instanceof errors_js_1.BastionTokenError)
39
+ throw new common_1.UnauthorizedException(err.message);
40
+ throw err;
41
+ }
42
+ }
43
+ };
44
+ exports.BastionJwksService = BastionJwksService;
45
+ exports.BastionJwksService = BastionJwksService = BastionJwksService_1 = __decorate([
46
+ (0, common_1.Injectable)(),
47
+ __param(0, (0, common_1.Inject)(options_js_1.BASTION_OPTIONS)),
48
+ __metadata("design:paramtypes", [Object])
49
+ ], BastionJwksService);
@@ -0,0 +1,20 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import { type BastionModuleAsyncOptions, type BastionModuleOptions } from './options.js';
3
+ /**
4
+ * ```ts
5
+ * BastionModule.forRootAsync({
6
+ * inject: [ConfigService],
7
+ * useFactory: (config: ConfigService) => ({
8
+ * baseUrl: config.getOrThrow('BASTION_URL'),
9
+ * serviceSlug: config.getOrThrow('BASTION_APP_SLUG'),
10
+ * apiKey: config.get('BASTION_CLIENT_API_KEY'),
11
+ * }),
12
+ * })
13
+ * ```
14
+ * Global: import once in `AppModule`, inject anywhere.
15
+ */
16
+ export declare class BastionModule {
17
+ static forRoot(options: BastionModuleOptions): DynamicModule;
18
+ static forRootAsync(options: BastionModuleAsyncOptions): DynamicModule;
19
+ private static build;
20
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var BastionModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.BastionModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const options_js_1 = require("./options.js");
13
+ const bastion_jwks_service_js_1 = require("./bastion-jwks.service.js");
14
+ const bastion_service_js_1 = require("./bastion.service.js");
15
+ const bastion_audit_service_js_1 = require("./bastion-audit.service.js");
16
+ const service_client_jwt_guard_js_1 = require("./guards/service-client-jwt.guard.js");
17
+ const bastion_user_guard_js_1 = require("./guards/bastion-user.guard.js");
18
+ const audit_interceptor_js_1 = require("./interceptors/audit.interceptor.js");
19
+ const PROVIDERS = [
20
+ bastion_jwks_service_js_1.BastionJwksService,
21
+ bastion_service_js_1.BastionService,
22
+ bastion_audit_service_js_1.BastionAuditService,
23
+ service_client_jwt_guard_js_1.ServiceClientJwtGuard,
24
+ bastion_user_guard_js_1.BastionUserGuard,
25
+ audit_interceptor_js_1.AuditInterceptor,
26
+ ];
27
+ /**
28
+ * ```ts
29
+ * BastionModule.forRootAsync({
30
+ * inject: [ConfigService],
31
+ * useFactory: (config: ConfigService) => ({
32
+ * baseUrl: config.getOrThrow('BASTION_URL'),
33
+ * serviceSlug: config.getOrThrow('BASTION_APP_SLUG'),
34
+ * apiKey: config.get('BASTION_CLIENT_API_KEY'),
35
+ * }),
36
+ * })
37
+ * ```
38
+ * Global: import once in `AppModule`, inject anywhere.
39
+ */
40
+ let BastionModule = BastionModule_1 = class BastionModule {
41
+ static forRoot(options) {
42
+ return this.build({ provide: options_js_1.BASTION_OPTIONS, useValue: options });
43
+ }
44
+ static forRootAsync(options) {
45
+ return this.build({ provide: options_js_1.BASTION_OPTIONS, useFactory: options.useFactory, inject: options.inject }, options.imports);
46
+ }
47
+ static build(optionsProvider, imports = []) {
48
+ return {
49
+ module: BastionModule_1,
50
+ imports,
51
+ providers: [optionsProvider, ...PROVIDERS],
52
+ exports: [options_js_1.BASTION_OPTIONS, ...PROVIDERS],
53
+ };
54
+ }
55
+ };
56
+ exports.BastionModule = BastionModule;
57
+ exports.BastionModule = BastionModule = BastionModule_1 = __decorate([
58
+ (0, common_1.Global)(),
59
+ (0, common_1.Module)({})
60
+ ], BastionModule);
@@ -0,0 +1,18 @@
1
+ import { BastionHttp } from '../core/bastion-http.js';
2
+ import type { AuditEventInput, TokenResponse } from '../core/types.js';
3
+ import { type BastionModuleOptions } from './options.js';
4
+ /** Outbound HTTP to Bastion. A non-2xx answer surfaces as `HttpException` with Bastion's status. */
5
+ export declare class BastionService {
6
+ private readonly options;
7
+ readonly http: BastionHttp;
8
+ constructor(options: BastionModuleOptions);
9
+ call<T>(method: string, path: string, body?: unknown, init?: {
10
+ token?: string;
11
+ headers?: Record<string, string>;
12
+ }): Promise<T>;
13
+ clientAuth(): Promise<TokenResponse>;
14
+ writeAuditEvent(token: string, data: AuditEventInput): Promise<{
15
+ id: string;
16
+ createdAt: string;
17
+ }>;
18
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.BastionService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const bastion_http_js_1 = require("../core/bastion-http.js");
18
+ const errors_js_1 = require("../core/errors.js");
19
+ const options_js_1 = require("./options.js");
20
+ /** Outbound HTTP to Bastion. A non-2xx answer surfaces as `HttpException` with Bastion's status. */
21
+ let BastionService = class BastionService {
22
+ options;
23
+ http;
24
+ constructor(options) {
25
+ this.options = options;
26
+ this.http = new bastion_http_js_1.BastionHttp({ baseUrl: options.baseUrl });
27
+ }
28
+ async call(method, path, body, init) {
29
+ try {
30
+ return await this.http.call(method, path, body, init);
31
+ }
32
+ catch (err) {
33
+ if (err instanceof errors_js_1.BastionHttpError)
34
+ throw new common_1.HttpException(err.message, err.status);
35
+ throw err;
36
+ }
37
+ }
38
+ clientAuth() {
39
+ if (!this.options.apiKey) {
40
+ throw new Error('BastionModule: apiKey is required for clientAuth()');
41
+ }
42
+ return this.call('POST', '/auth/client', {
43
+ apiKey: this.options.apiKey,
44
+ serviceSlug: this.options.serviceSlug,
45
+ ...(this.options.tenantSlug ? { tenantSlug: this.options.tenantSlug } : {}),
46
+ });
47
+ }
48
+ writeAuditEvent(token, data) {
49
+ return this.call('POST', '/events', data, { token });
50
+ }
51
+ };
52
+ exports.BastionService = BastionService;
53
+ exports.BastionService = BastionService = __decorate([
54
+ (0, common_1.Injectable)(),
55
+ __param(0, (0, common_1.Inject)(options_js_1.BASTION_OPTIONS)),
56
+ __metadata("design:paramtypes", [Object])
57
+ ], BastionService);
@@ -0,0 +1,15 @@
1
+ export declare const AUDIT_EVENT_KEY = "bastion:auditEvent";
2
+ export interface AuditRequest {
3
+ params: Record<string, string>;
4
+ body: unknown;
5
+ }
6
+ export interface AuditOptions {
7
+ /** Builds the metadata from the handler's result and the request. Errors here are logged, never propagated. */
8
+ metadata?: (result: unknown, req: AuditRequest) => Record<string, unknown>;
9
+ }
10
+ export interface AuditDescriptor {
11
+ event: string;
12
+ options: AuditOptions;
13
+ }
14
+ /** Writes `event` (attributed to the admin user on the request) after the handler succeeds. Needs `AuditInterceptor`. */
15
+ export declare const Audit: (event: string, options?: AuditOptions) => import("@nestjs/common").CustomDecorator<string>;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Audit = exports.AUDIT_EVENT_KEY = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ exports.AUDIT_EVENT_KEY = 'bastion:auditEvent';
6
+ /** Writes `event` (attributed to the admin user on the request) after the handler succeeds. Needs `AuditInterceptor`. */
7
+ const Audit = (event, options = {}) => (0, common_1.SetMetadata)(exports.AUDIT_EVENT_KEY, { event, options });
8
+ exports.Audit = Audit;
@@ -0,0 +1,4 @@
1
+ /** The machine token payload set by `ServiceClientJwtGuard`. */
2
+ export declare const CurrentClient: (...dataOrPipes: unknown[]) => ParameterDecorator;
3
+ /** The user payload set by `BastionUserGuard`. */
4
+ export declare const CurrentAdminUser: (...dataOrPipes: unknown[]) => ParameterDecorator;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CurrentAdminUser = exports.CurrentClient = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ /** The machine token payload set by `ServiceClientJwtGuard`. */
6
+ exports.CurrentClient = (0, common_1.createParamDecorator)((_, ctx) => ctx.switchToHttp().getRequest().user);
7
+ /** The user payload set by `BastionUserGuard`. */
8
+ exports.CurrentAdminUser = (0, common_1.createParamDecorator)((_, ctx) => ctx.switchToHttp().getRequest().adminUser);
@@ -0,0 +1,3 @@
1
+ export declare const IS_PUBLIC_KEY = "isPublic";
2
+ /** Exempts a route (or a whole controller) from `ServiceClientJwtGuard`. */
3
+ export declare const Public: () => import("@nestjs/common").CustomDecorator<string>;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Public = exports.IS_PUBLIC_KEY = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ exports.IS_PUBLIC_KEY = 'isPublic';
6
+ /** Exempts a route (or a whole controller) from `ServiceClientJwtGuard`. */
7
+ const Public = () => (0, common_1.SetMetadata)(exports.IS_PUBLIC_KEY, true);
8
+ exports.Public = Public;
@@ -0,0 +1,3 @@
1
+ export declare const REQUIRED_SCOPES_KEY = "bastion:requiredScopes";
2
+ /** Scopes the machine token must carry to reach this route (all of them). Checked by `ServiceClientJwtGuard`. */
3
+ export declare const RequireScope: (...scopes: string[]) => import("@nestjs/common").CustomDecorator<string>;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RequireScope = exports.REQUIRED_SCOPES_KEY = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ exports.REQUIRED_SCOPES_KEY = 'bastion:requiredScopes';
6
+ /** Scopes the machine token must carry to reach this route (all of them). Checked by `ServiceClientJwtGuard`. */
7
+ const RequireScope = (...scopes) => (0, common_1.SetMetadata)(exports.REQUIRED_SCOPES_KEY, scopes);
8
+ exports.RequireScope = RequireScope;
@@ -0,0 +1,20 @@
1
+ import { CanActivate, ExecutionContext } from '@nestjs/common';
2
+ import { BastionJwksService } from '../bastion-jwks.service.js';
3
+ import { BastionAuditService } from '../bastion-audit.service.js';
4
+ import { type BastionModuleOptions } from '../options.js';
5
+ /**
6
+ * Admin-surface guard: accepts a *user* token from one of `acceptedAppSlugs`
7
+ * (the console, typically) with one of `acceptedRoles`. Sets `req.adminUser`.
8
+ * A denial is audited as `admin.access_denied`, rate-limited per (app, reason)
9
+ * so a misconfigured console cannot flood the log.
10
+ */
11
+ export declare class BastionUserGuard implements CanActivate {
12
+ private readonly jwks;
13
+ private readonly audit;
14
+ private readonly acceptedAppSlugs;
15
+ private readonly acceptedRoles;
16
+ private readonly reportedAt;
17
+ constructor(jwks: BastionJwksService, audit: BastionAuditService, options: BastionModuleOptions);
18
+ canActivate(ctx: ExecutionContext): Promise<boolean>;
19
+ private reportAccessDenied;
20
+ }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.BastionUserGuard = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const types_js_1 = require("../../core/types.js");
18
+ const bastion_jwks_service_js_1 = require("../bastion-jwks.service.js");
19
+ const bastion_audit_service_js_1 = require("../bastion-audit.service.js");
20
+ const options_js_1 = require("../options.js");
21
+ const ACCESS_DENIED_COOLDOWN_MS = 5 * 60 * 1000;
22
+ const ACCESS_DENIED_MAX_KEYS = 50;
23
+ const DEFAULT_ACCEPTED_APP_SLUGS = ['meridian'];
24
+ const DEFAULT_ACCEPTED_ROLES = ['ADMIN', 'OWNER', 'SUPER_ADMIN'];
25
+ /**
26
+ * Admin-surface guard: accepts a *user* token from one of `acceptedAppSlugs`
27
+ * (the console, typically) with one of `acceptedRoles`. Sets `req.adminUser`.
28
+ * A denial is audited as `admin.access_denied`, rate-limited per (app, reason)
29
+ * so a misconfigured console cannot flood the log.
30
+ */
31
+ let BastionUserGuard = class BastionUserGuard {
32
+ jwks;
33
+ audit;
34
+ acceptedAppSlugs;
35
+ acceptedRoles;
36
+ reportedAt = new Map();
37
+ constructor(jwks, audit, options) {
38
+ this.jwks = jwks;
39
+ this.audit = audit;
40
+ this.acceptedAppSlugs = options.acceptedAppSlugs ?? DEFAULT_ACCEPTED_APP_SLUGS;
41
+ this.acceptedRoles = options.acceptedRoles ?? DEFAULT_ACCEPTED_ROLES;
42
+ }
43
+ async canActivate(ctx) {
44
+ const req = ctx.switchToHttp().getRequest();
45
+ const auth = req.headers['authorization'];
46
+ if (!auth?.startsWith('Bearer '))
47
+ throw new common_1.UnauthorizedException('Missing token');
48
+ const payload = await this.jwks.verify(auth.slice(7));
49
+ if ((0, types_js_1.isServiceClientToken)(payload))
50
+ throw new common_1.UnauthorizedException('Service client token not allowed');
51
+ if (!this.acceptedAppSlugs.includes(payload.appSlug)) {
52
+ this.reportAccessDenied('app_not_allowed', payload, req);
53
+ throw new common_1.ForbiddenException('App not allowed');
54
+ }
55
+ if (!this.acceptedRoles.includes(payload.role ?? '')) {
56
+ this.reportAccessDenied('role_insufficient', payload, req);
57
+ throw new common_1.ForbiddenException('Insufficient role');
58
+ }
59
+ req.adminUser = payload;
60
+ return true;
61
+ }
62
+ reportAccessDenied(reason, user, req) {
63
+ const key = `${user.appSlug}:${reason}`;
64
+ const now = Date.now();
65
+ const last = this.reportedAt.get(key);
66
+ if (last !== undefined && now - last < ACCESS_DENIED_COOLDOWN_MS)
67
+ return;
68
+ if (this.reportedAt.size >= ACCESS_DENIED_MAX_KEYS)
69
+ this.reportedAt.clear();
70
+ this.reportedAt.set(key, now);
71
+ void this.audit.write('admin.access_denied', {
72
+ metadata: {
73
+ reason,
74
+ appSlug: user.appSlug,
75
+ role: user.role ?? 'none',
76
+ path: req.originalUrl ?? req.url ?? 'unknown',
77
+ },
78
+ });
79
+ }
80
+ };
81
+ exports.BastionUserGuard = BastionUserGuard;
82
+ exports.BastionUserGuard = BastionUserGuard = __decorate([
83
+ (0, common_1.Injectable)(),
84
+ __param(2, (0, common_1.Inject)(options_js_1.BASTION_OPTIONS)),
85
+ __metadata("design:paramtypes", [bastion_jwks_service_js_1.BastionJwksService,
86
+ bastion_audit_service_js_1.BastionAuditService, Object])
87
+ ], BastionUserGuard);
@@ -0,0 +1,17 @@
1
+ import { CanActivate, ExecutionContext } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { BastionJwksService } from '../bastion-jwks.service.js';
4
+ import { type BastionModuleOptions } from '../options.js';
5
+ /**
6
+ * Machine-to-machine guard, meant as the global `APP_GUARD`: every route needs
7
+ * a `service_client` token issued for this service (`serviceSlug` from the
8
+ * module options, never hard-coded) unless it carries `@Public()`.
9
+ * `@RequireScope()` adds a per-route scope check. The payload lands on `req.user`.
10
+ */
11
+ export declare class ServiceClientJwtGuard implements CanActivate {
12
+ private readonly jwks;
13
+ private readonly reflector;
14
+ private readonly options;
15
+ constructor(jwks: BastionJwksService, reflector: Reflector, options: BastionModuleOptions);
16
+ canActivate(ctx: ExecutionContext): Promise<boolean>;
17
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ServiceClientJwtGuard = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const core_1 = require("@nestjs/core");
18
+ const types_js_1 = require("../../core/types.js");
19
+ const bastion_jwks_service_js_1 = require("../bastion-jwks.service.js");
20
+ const options_js_1 = require("../options.js");
21
+ const public_decorator_js_1 = require("../decorators/public.decorator.js");
22
+ const require_scope_decorator_js_1 = require("../decorators/require-scope.decorator.js");
23
+ /**
24
+ * Machine-to-machine guard, meant as the global `APP_GUARD`: every route needs
25
+ * a `service_client` token issued for this service (`serviceSlug` from the
26
+ * module options, never hard-coded) unless it carries `@Public()`.
27
+ * `@RequireScope()` adds a per-route scope check. The payload lands on `req.user`.
28
+ */
29
+ let ServiceClientJwtGuard = class ServiceClientJwtGuard {
30
+ jwks;
31
+ reflector;
32
+ options;
33
+ constructor(jwks, reflector, options) {
34
+ this.jwks = jwks;
35
+ this.reflector = reflector;
36
+ this.options = options;
37
+ }
38
+ async canActivate(ctx) {
39
+ const targets = [ctx.getHandler(), ctx.getClass()];
40
+ if (this.reflector.getAllAndOverride(public_decorator_js_1.IS_PUBLIC_KEY, targets))
41
+ return true;
42
+ const req = ctx.switchToHttp().getRequest();
43
+ const auth = req.headers['authorization'];
44
+ if (!auth?.startsWith('Bearer '))
45
+ throw new common_1.UnauthorizedException('Missing token');
46
+ const payload = await this.jwks.verify(auth.slice(7));
47
+ if (!(0, types_js_1.isServiceClientToken)(payload))
48
+ throw new common_1.UnauthorizedException('Not a service client token');
49
+ if (payload.serviceSlug !== this.options.serviceSlug) {
50
+ throw new common_1.ForbiddenException('Token not issued for this service');
51
+ }
52
+ const required = this.reflector.getAllAndOverride(require_scope_decorator_js_1.REQUIRED_SCOPES_KEY, targets) ?? [];
53
+ const missing = required.filter((s) => !payload.scopes?.includes(s));
54
+ if (missing.length)
55
+ throw new common_1.ForbiddenException(`Missing scope: ${missing.join(', ')}`);
56
+ req.user = payload;
57
+ return true;
58
+ }
59
+ };
60
+ exports.ServiceClientJwtGuard = ServiceClientJwtGuard;
61
+ exports.ServiceClientJwtGuard = ServiceClientJwtGuard = __decorate([
62
+ (0, common_1.Injectable)(),
63
+ __param(2, (0, common_1.Inject)(options_js_1.BASTION_OPTIONS)),
64
+ __metadata("design:paramtypes", [bastion_jwks_service_js_1.BastionJwksService,
65
+ core_1.Reflector, Object])
66
+ ], ServiceClientJwtGuard);
@@ -0,0 +1,14 @@
1
+ export * from '../core/index.js';
2
+ export { BastionModule } from './bastion.module.js';
3
+ export { BASTION_OPTIONS, type BastionModuleOptions, type BastionModuleAsyncOptions } from './options.js';
4
+ export { BastionJwksService } from './bastion-jwks.service.js';
5
+ export { BastionService } from './bastion.service.js';
6
+ export { BastionAuditService } from './bastion-audit.service.js';
7
+ export { ServiceClientJwtGuard } from './guards/service-client-jwt.guard.js';
8
+ export { BastionUserGuard } from './guards/bastion-user.guard.js';
9
+ export { AuditInterceptor } from './interceptors/audit.interceptor.js';
10
+ export { Public, IS_PUBLIC_KEY } from './decorators/public.decorator.js';
11
+ export { RequireScope, REQUIRED_SCOPES_KEY } from './decorators/require-scope.decorator.js';
12
+ export { Audit, AUDIT_EVENT_KEY, type AuditOptions, type AuditRequest, type AuditDescriptor } from './decorators/audit.decorator.js';
13
+ export { CurrentClient, CurrentAdminUser } from './decorators/current-user.decorator.js';
14
+ export { resolveTenantScope, CROSS_TENANT_ROLE } from './tenant-scope.js';
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.CROSS_TENANT_ROLE = exports.resolveTenantScope = exports.CurrentAdminUser = exports.CurrentClient = exports.AUDIT_EVENT_KEY = exports.Audit = exports.REQUIRED_SCOPES_KEY = exports.RequireScope = exports.IS_PUBLIC_KEY = exports.Public = exports.AuditInterceptor = exports.BastionUserGuard = exports.ServiceClientJwtGuard = exports.BastionAuditService = exports.BastionService = exports.BastionJwksService = exports.BASTION_OPTIONS = exports.BastionModule = void 0;
18
+ __exportStar(require("../core/index.js"), exports);
19
+ var bastion_module_js_1 = require("./bastion.module.js");
20
+ Object.defineProperty(exports, "BastionModule", { enumerable: true, get: function () { return bastion_module_js_1.BastionModule; } });
21
+ var options_js_1 = require("./options.js");
22
+ Object.defineProperty(exports, "BASTION_OPTIONS", { enumerable: true, get: function () { return options_js_1.BASTION_OPTIONS; } });
23
+ var bastion_jwks_service_js_1 = require("./bastion-jwks.service.js");
24
+ Object.defineProperty(exports, "BastionJwksService", { enumerable: true, get: function () { return bastion_jwks_service_js_1.BastionJwksService; } });
25
+ var bastion_service_js_1 = require("./bastion.service.js");
26
+ Object.defineProperty(exports, "BastionService", { enumerable: true, get: function () { return bastion_service_js_1.BastionService; } });
27
+ var bastion_audit_service_js_1 = require("./bastion-audit.service.js");
28
+ Object.defineProperty(exports, "BastionAuditService", { enumerable: true, get: function () { return bastion_audit_service_js_1.BastionAuditService; } });
29
+ var service_client_jwt_guard_js_1 = require("./guards/service-client-jwt.guard.js");
30
+ Object.defineProperty(exports, "ServiceClientJwtGuard", { enumerable: true, get: function () { return service_client_jwt_guard_js_1.ServiceClientJwtGuard; } });
31
+ var bastion_user_guard_js_1 = require("./guards/bastion-user.guard.js");
32
+ Object.defineProperty(exports, "BastionUserGuard", { enumerable: true, get: function () { return bastion_user_guard_js_1.BastionUserGuard; } });
33
+ var audit_interceptor_js_1 = require("./interceptors/audit.interceptor.js");
34
+ Object.defineProperty(exports, "AuditInterceptor", { enumerable: true, get: function () { return audit_interceptor_js_1.AuditInterceptor; } });
35
+ var public_decorator_js_1 = require("./decorators/public.decorator.js");
36
+ Object.defineProperty(exports, "Public", { enumerable: true, get: function () { return public_decorator_js_1.Public; } });
37
+ Object.defineProperty(exports, "IS_PUBLIC_KEY", { enumerable: true, get: function () { return public_decorator_js_1.IS_PUBLIC_KEY; } });
38
+ var require_scope_decorator_js_1 = require("./decorators/require-scope.decorator.js");
39
+ Object.defineProperty(exports, "RequireScope", { enumerable: true, get: function () { return require_scope_decorator_js_1.RequireScope; } });
40
+ Object.defineProperty(exports, "REQUIRED_SCOPES_KEY", { enumerable: true, get: function () { return require_scope_decorator_js_1.REQUIRED_SCOPES_KEY; } });
41
+ var audit_decorator_js_1 = require("./decorators/audit.decorator.js");
42
+ Object.defineProperty(exports, "Audit", { enumerable: true, get: function () { return audit_decorator_js_1.Audit; } });
43
+ Object.defineProperty(exports, "AUDIT_EVENT_KEY", { enumerable: true, get: function () { return audit_decorator_js_1.AUDIT_EVENT_KEY; } });
44
+ var current_user_decorator_js_1 = require("./decorators/current-user.decorator.js");
45
+ Object.defineProperty(exports, "CurrentClient", { enumerable: true, get: function () { return current_user_decorator_js_1.CurrentClient; } });
46
+ Object.defineProperty(exports, "CurrentAdminUser", { enumerable: true, get: function () { return current_user_decorator_js_1.CurrentAdminUser; } });
47
+ var tenant_scope_js_1 = require("./tenant-scope.js");
48
+ Object.defineProperty(exports, "resolveTenantScope", { enumerable: true, get: function () { return tenant_scope_js_1.resolveTenantScope; } });
49
+ Object.defineProperty(exports, "CROSS_TENANT_ROLE", { enumerable: true, get: function () { return tenant_scope_js_1.CROSS_TENANT_ROLE; } });
@@ -0,0 +1,12 @@
1
+ import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { Observable } from 'rxjs';
4
+ import { BastionAuditService } from '../bastion-audit.service.js';
5
+ /** Pairs with `@Audit()`: after a handler succeeds, writes the event attributed to `req.adminUser`. */
6
+ export declare class AuditInterceptor implements NestInterceptor {
7
+ private readonly reflector;
8
+ private readonly audit;
9
+ private readonly logger;
10
+ constructor(reflector: Reflector, audit: BastionAuditService);
11
+ intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown>;
12
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var AuditInterceptor_1;
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.AuditInterceptor = void 0;
14
+ const common_1 = require("@nestjs/common");
15
+ const core_1 = require("@nestjs/core");
16
+ const rxjs_1 = require("rxjs");
17
+ const bastion_audit_service_js_1 = require("../bastion-audit.service.js");
18
+ const audit_decorator_js_1 = require("../decorators/audit.decorator.js");
19
+ /** Pairs with `@Audit()`: after a handler succeeds, writes the event attributed to `req.adminUser`. */
20
+ let AuditInterceptor = AuditInterceptor_1 = class AuditInterceptor {
21
+ reflector;
22
+ audit;
23
+ logger = new common_1.Logger(AuditInterceptor_1.name);
24
+ constructor(reflector, audit) {
25
+ this.reflector = reflector;
26
+ this.audit = audit;
27
+ }
28
+ intercept(ctx, next) {
29
+ const descriptor = this.reflector.get(audit_decorator_js_1.AUDIT_EVENT_KEY, ctx.getHandler());
30
+ if (!descriptor)
31
+ return next.handle();
32
+ const req = ctx.switchToHttp().getRequest();
33
+ return next.handle().pipe((0, rxjs_1.tap)((result) => {
34
+ let metadata;
35
+ try {
36
+ metadata = descriptor.options.metadata?.(result, req) ?? {};
37
+ }
38
+ catch (err) {
39
+ this.logger.warn(`audit metadata builder failed event=${descriptor.event}: ${err.message}`);
40
+ return;
41
+ }
42
+ void this.audit.writeAsAdmin(descriptor.event, req.adminUser, metadata);
43
+ }));
44
+ }
45
+ };
46
+ exports.AuditInterceptor = AuditInterceptor;
47
+ exports.AuditInterceptor = AuditInterceptor = AuditInterceptor_1 = __decorate([
48
+ (0, common_1.Injectable)(),
49
+ __metadata("design:paramtypes", [core_1.Reflector,
50
+ bastion_audit_service_js_1.BastionAuditService])
51
+ ], AuditInterceptor);
@@ -0,0 +1,2 @@
1
+ import type { BastionLogger } from '../core/types.js';
2
+ export declare function nestLogger(context: string): BastionLogger;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nestLogger = nestLogger;
4
+ const common_1 = require("@nestjs/common");
5
+ function nestLogger(context) {
6
+ const logger = new common_1.Logger(context);
7
+ return {
8
+ warn: (m) => logger.warn(m),
9
+ error: (m) => logger.error(m),
10
+ log: (m) => logger.log(m),
11
+ };
12
+ }