@heyatom/bastion-client 0.1.2 → 0.3.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 (60) hide show
  1. package/README.md +40 -0
  2. package/dist/cjs/core/api-client.d.ts +98 -0
  3. package/dist/cjs/core/api-client.js +226 -0
  4. package/dist/cjs/core/api-types.d.ts +138 -0
  5. package/dist/cjs/core/api-types.js +10 -0
  6. package/dist/cjs/core/bastion-http.d.ts +13 -8
  7. package/dist/cjs/core/bastion-http.js +29 -17
  8. package/dist/cjs/core/errors.d.ts +24 -3
  9. package/dist/cjs/core/errors.js +45 -3
  10. package/dist/cjs/core/index.d.ts +4 -2
  11. package/dist/cjs/core/index.js +4 -1
  12. package/dist/cjs/core/jwks-verifier.js +2 -1
  13. package/dist/cjs/core/service-token-provider.d.ts +20 -12
  14. package/dist/cjs/core/service-token-provider.js +55 -27
  15. package/dist/cjs/core/types.d.ts +8 -0
  16. package/dist/cjs/core/types.js +20 -0
  17. package/dist/cjs/nest/bastion-api-client.provider.d.ts +13 -0
  18. package/dist/cjs/nest/bastion-api-client.provider.js +43 -0
  19. package/dist/cjs/nest/bastion-http-exception.d.ts +19 -0
  20. package/dist/cjs/nest/bastion-http-exception.js +39 -0
  21. package/dist/cjs/nest/bastion-jwks.service.d.ts +8 -1
  22. package/dist/cjs/nest/bastion-jwks.service.js +19 -0
  23. package/dist/cjs/nest/bastion.module.js +2 -0
  24. package/dist/cjs/nest/bastion.service.d.ts +5 -7
  25. package/dist/cjs/nest/bastion.service.js +5 -13
  26. package/dist/cjs/nest/index.d.ts +2 -0
  27. package/dist/cjs/nest/index.js +6 -1
  28. package/dist/cjs/nest/options.d.ts +2 -0
  29. package/dist/cjs/webhooks/index.d.ts +54 -0
  30. package/dist/cjs/webhooks/index.js +72 -0
  31. package/dist/esm/core/api-client.d.ts +98 -0
  32. package/dist/esm/core/api-client.js +222 -0
  33. package/dist/esm/core/api-types.d.ts +138 -0
  34. package/dist/esm/core/api-types.js +7 -0
  35. package/dist/esm/core/bastion-http.d.ts +13 -8
  36. package/dist/esm/core/bastion-http.js +30 -18
  37. package/dist/esm/core/errors.d.ts +24 -3
  38. package/dist/esm/core/errors.js +43 -2
  39. package/dist/esm/core/index.d.ts +4 -2
  40. package/dist/esm/core/index.js +2 -0
  41. package/dist/esm/core/jwks-verifier.js +2 -1
  42. package/dist/esm/core/service-token-provider.d.ts +20 -12
  43. package/dist/esm/core/service-token-provider.js +55 -27
  44. package/dist/esm/core/types.d.ts +8 -0
  45. package/dist/esm/core/types.js +19 -0
  46. package/dist/esm/nest/bastion-api-client.provider.d.ts +13 -0
  47. package/dist/esm/nest/bastion-api-client.provider.js +40 -0
  48. package/dist/esm/nest/bastion-http-exception.d.ts +19 -0
  49. package/dist/esm/nest/bastion-http-exception.js +34 -0
  50. package/dist/esm/nest/bastion-jwks.service.d.ts +8 -1
  51. package/dist/esm/nest/bastion-jwks.service.js +19 -0
  52. package/dist/esm/nest/bastion.module.js +2 -0
  53. package/dist/esm/nest/bastion.service.d.ts +5 -7
  54. package/dist/esm/nest/bastion.service.js +6 -14
  55. package/dist/esm/nest/index.d.ts +2 -0
  56. package/dist/esm/nest/index.js +2 -0
  57. package/dist/esm/nest/options.d.ts +2 -0
  58. package/dist/esm/webhooks/index.d.ts +54 -0
  59. package/dist/esm/webhooks/index.js +68 -0
  60. package/package.json +14 -1
@@ -1,11 +1,32 @@
1
- export type TokenErrorCode = 'malformed' | 'missing_kid' | 'unknown_kid' | 'invalid';
1
+ export type TokenErrorCode = 'malformed' | 'missing_kid' | 'unknown_kid' | 'invalid' | 'not_user_token' | 'wrong_app';
2
2
  /** Verification failure. `code` is safe to log; the message is safe to return to the caller. */
3
3
  export declare class BastionTokenError extends Error {
4
4
  readonly code: TokenErrorCode;
5
5
  constructor(code: TokenErrorCode, message: string);
6
6
  }
7
- /** A non-2xx answer from Bastion's HTTP API. */
7
+ /**
8
+ * Stable classification of a Bastion error answer, so an app can branch on a
9
+ * code instead of on Bastion's message text. The age-gate codes are Bastion's
10
+ * own (it returns them verbatim); the rest are derived from status + message
11
+ * until Bastion carries a `code` field on every error.
12
+ */
13
+ export declare const BastionErrorCode: {
14
+ readonly EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED";
15
+ readonly INVALID_CREDENTIALS: "INVALID_CREDENTIALS";
16
+ readonly NO_APP_ACCESS: "NO_APP_ACCESS";
17
+ readonly ACCOUNT_INACTIVE: "ACCOUNT_INACTIVE";
18
+ readonly CONFLICT: "CONFLICT";
19
+ readonly BIRTH_DATE_REQUIRED: "BIRTH_DATE_REQUIRED";
20
+ readonly BIRTH_DATE_INVALID: "BIRTH_DATE_INVALID";
21
+ readonly UNDER_MINIMUM_AGE: "UNDER_MINIMUM_AGE";
22
+ readonly UNAVAILABLE: "UNAVAILABLE";
23
+ readonly UPSTREAM: "UPSTREAM";
24
+ };
25
+ export type BastionErrorCode = (typeof BastionErrorCode)[keyof typeof BastionErrorCode];
26
+ export declare function classifyBastionError(status: number, message: string): BastionErrorCode;
27
+ /** A non-2xx answer from Bastion's HTTP API, or Bastion unreachable (`status` 503, code `UNAVAILABLE`). */
8
28
  export declare class BastionHttpError extends Error {
9
29
  readonly status: number;
10
- constructor(status: number, message: string);
30
+ readonly code: BastionErrorCode;
31
+ constructor(status: number, message: string, code?: BastionErrorCode);
11
32
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BastionHttpError = exports.BastionTokenError = void 0;
3
+ exports.BastionHttpError = exports.BastionErrorCode = exports.BastionTokenError = void 0;
4
+ exports.classifyBastionError = classifyBastionError;
4
5
  /** Verification failure. `code` is safe to log; the message is safe to return to the caller. */
5
6
  class BastionTokenError extends Error {
6
7
  code;
@@ -11,13 +12,54 @@ class BastionTokenError extends Error {
11
12
  }
12
13
  }
13
14
  exports.BastionTokenError = BastionTokenError;
14
- /** A non-2xx answer from Bastion's HTTP API. */
15
+ /**
16
+ * Stable classification of a Bastion error answer, so an app can branch on a
17
+ * code instead of on Bastion's message text. The age-gate codes are Bastion's
18
+ * own (it returns them verbatim); the rest are derived from status + message
19
+ * until Bastion carries a `code` field on every error.
20
+ */
21
+ exports.BastionErrorCode = {
22
+ EMAIL_NOT_VERIFIED: 'EMAIL_NOT_VERIFIED',
23
+ INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
24
+ NO_APP_ACCESS: 'NO_APP_ACCESS',
25
+ ACCOUNT_INACTIVE: 'ACCOUNT_INACTIVE',
26
+ CONFLICT: 'CONFLICT',
27
+ BIRTH_DATE_REQUIRED: 'BIRTH_DATE_REQUIRED',
28
+ BIRTH_DATE_INVALID: 'BIRTH_DATE_INVALID',
29
+ UNDER_MINIMUM_AGE: 'UNDER_MINIMUM_AGE',
30
+ UNAVAILABLE: 'UNAVAILABLE',
31
+ UPSTREAM: 'UPSTREAM',
32
+ };
33
+ function classifyBastionError(status, message) {
34
+ if (message === exports.BastionErrorCode.BIRTH_DATE_REQUIRED ||
35
+ message === exports.BastionErrorCode.BIRTH_DATE_INVALID ||
36
+ message === exports.BastionErrorCode.UNDER_MINIMUM_AGE) {
37
+ return message;
38
+ }
39
+ if (status === 403 && /email not verified/i.test(message))
40
+ return exports.BastionErrorCode.EMAIL_NOT_VERIFIED;
41
+ if (status === 401) {
42
+ if (/no access to this app/i.test(message))
43
+ return exports.BastionErrorCode.NO_APP_ACCESS;
44
+ if (/inactive|locked/i.test(message))
45
+ return exports.BastionErrorCode.ACCOUNT_INACTIVE;
46
+ return exports.BastionErrorCode.INVALID_CREDENTIALS;
47
+ }
48
+ if (status === 409)
49
+ return exports.BastionErrorCode.CONFLICT;
50
+ if (status === 503)
51
+ return exports.BastionErrorCode.UNAVAILABLE;
52
+ return exports.BastionErrorCode.UPSTREAM;
53
+ }
54
+ /** A non-2xx answer from Bastion's HTTP API, or Bastion unreachable (`status` 503, code `UNAVAILABLE`). */
15
55
  class BastionHttpError extends Error {
16
56
  status;
17
- constructor(status, message) {
57
+ code;
58
+ constructor(status, message, code) {
18
59
  super(message);
19
60
  this.status = status;
20
61
  this.name = 'BastionHttpError';
62
+ this.code = code ?? classifyBastionError(status, message);
21
63
  }
22
64
  }
23
65
  exports.BastionHttpError = BastionHttpError;
@@ -1,5 +1,7 @@
1
1
  export * from './types.js';
2
+ export * from './api-types.js';
2
3
  export * from './errors.js';
3
4
  export { JwksVerifier, type JwksVerifierOptions } from './jwks-verifier.js';
4
- export { BastionHttp, type BastionHttpOptions, type ClientAuthInput } from './bastion-http.js';
5
- export { ServiceTokenProvider } from './service-token-provider.js';
5
+ export { BastionHttp, type BastionHttpOptions, type BastionRequestInit, type ClientAuthInput } from './bastion-http.js';
6
+ export { ServiceTokenProvider, type CredentialResolver, type ServiceTokenCredentials } from './service-token-provider.js';
7
+ export { BastionApiClient, type BastionApiClientOptions } from './api-client.js';
@@ -14,8 +14,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.ServiceTokenProvider = exports.BastionHttp = exports.JwksVerifier = void 0;
17
+ exports.BastionApiClient = exports.ServiceTokenProvider = exports.BastionHttp = exports.JwksVerifier = void 0;
18
18
  __exportStar(require("./types.js"), exports);
19
+ __exportStar(require("./api-types.js"), exports);
19
20
  __exportStar(require("./errors.js"), exports);
20
21
  var jwks_verifier_js_1 = require("./jwks-verifier.js");
21
22
  Object.defineProperty(exports, "JwksVerifier", { enumerable: true, get: function () { return jwks_verifier_js_1.JwksVerifier; } });
@@ -23,3 +24,5 @@ var bastion_http_js_1 = require("./bastion-http.js");
23
24
  Object.defineProperty(exports, "BastionHttp", { enumerable: true, get: function () { return bastion_http_js_1.BastionHttp; } });
24
25
  var service_token_provider_js_1 = require("./service-token-provider.js");
25
26
  Object.defineProperty(exports, "ServiceTokenProvider", { enumerable: true, get: function () { return service_token_provider_js_1.ServiceTokenProvider; } });
27
+ var api_client_js_1 = require("./api-client.js");
28
+ Object.defineProperty(exports, "BastionApiClient", { enumerable: true, get: function () { return api_client_js_1.BastionApiClient; } });
@@ -33,7 +33,8 @@ class JwksVerifier {
33
33
  this.ttlMs = options.ttlMs ?? 300_000;
34
34
  this.issuer = options.issuer === undefined ? 'bastion' : options.issuer;
35
35
  this.logger = options.logger ?? types_js_1.silentLogger;
36
- this.fetchImpl = options.fetch ?? fetch;
36
+ // Late-bound on purpose: a consumer (or its tests) may replace global fetch after construction.
37
+ this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
37
38
  }
38
39
  async verify(token) {
39
40
  let kid;
@@ -1,21 +1,29 @@
1
1
  import type { BastionHttp, ClientAuthInput } from './bastion-http.js';
2
2
  import { type BastionLogger } from './types.js';
3
3
  /**
4
- * Holds the service-client token for outbound calls and renews it ahead of
5
- * `exp`. Single-flight: a burst of first calls shares one `POST /auth/client`
6
- * instead of issuing one per caller — the previous per-service copies had no
7
- * such guard.
4
+ * Holds machine tokens for outbound calls and renews each ahead of `exp`.
5
+ * One credential (the common case) or several keyed by `serviceSlug`, for an
6
+ * app that calls more than one service with its own client for each. Per
7
+ * slug: single-flight (a burst of first calls shares one `POST /auth/client`)
8
+ * and stale-cache fallback (a mint that fails while the old token is still
9
+ * inside its `exp` keeps serving it).
8
10
  */
11
+ /** Credentials looked up per `serviceSlug` on demand — for an app whose targets are configured lazily. */
12
+ export interface CredentialResolver {
13
+ resolve: (serviceSlug: string) => ClientAuthInput | null | undefined;
14
+ defaultSlug: string;
15
+ }
16
+ export type ServiceTokenCredentials = ClientAuthInput | Record<string, ClientAuthInput> | CredentialResolver;
9
17
  export declare class ServiceTokenProvider {
10
18
  private readonly http;
11
- private readonly credentials;
12
19
  private readonly logger;
13
- private token;
14
- private expiresAt;
15
- private inFlight;
16
- /** Tenant the token was issued for — the tenant `POST /events` rows land in. */
17
- tenantId: string | null;
18
- constructor(http: BastionHttp, credentials: ClientAuthInput, logger?: BastionLogger);
19
- getToken(): Promise<string>;
20
+ private readonly resolveCredential;
21
+ private readonly defaultSlug;
22
+ private readonly cache;
23
+ private readonly inFlight;
24
+ constructor(http: BastionHttp, credentials: ServiceTokenCredentials, logger?: BastionLogger);
25
+ /** Tenant of the default credential's token, once minted — the tenant `POST /events` rows land in. */
26
+ get tenantId(): string | null;
27
+ getToken(serviceSlug?: string): Promise<string>;
20
28
  private refresh;
21
29
  }
@@ -5,42 +5,70 @@ const jose_1 = require("jose");
5
5
  const types_js_1 = require("./types.js");
6
6
  const REFRESH_MARGIN_MS = 5 * 60 * 1000;
7
7
  const FALLBACK_TTL_MS = 60 * 60 * 1000;
8
- /**
9
- * Holds the service-client token for outbound calls and renews it ahead of
10
- * `exp`. Single-flight: a burst of first calls shares one `POST /auth/client`
11
- * instead of issuing one per caller — the previous per-service copies had no
12
- * such guard.
13
- */
14
8
  class ServiceTokenProvider {
15
9
  http;
16
- credentials;
17
10
  logger;
18
- token = null;
19
- expiresAt = 0;
20
- inFlight = null;
21
- /** Tenant the token was issued for — the tenant `POST /events` rows land in. */
22
- tenantId = null;
11
+ resolveCredential;
12
+ defaultSlug;
13
+ cache = new Map();
14
+ inFlight = new Map();
23
15
  constructor(http, credentials, logger = types_js_1.silentLogger) {
24
16
  this.http = http;
25
- this.credentials = credentials;
26
17
  this.logger = logger;
27
- }
28
- getToken() {
29
- if (this.token && Date.now() < this.expiresAt - REFRESH_MARGIN_MS) {
30
- return Promise.resolve(this.token);
18
+ if ('resolve' in credentials && typeof credentials.resolve === 'function') {
19
+ const resolver = credentials;
20
+ this.resolveCredential = resolver.resolve;
21
+ this.defaultSlug = resolver.defaultSlug;
22
+ }
23
+ else if ('apiKey' in credentials && typeof credentials.apiKey === 'string') {
24
+ const single = credentials;
25
+ this.resolveCredential = (slug) => (slug === single.serviceSlug ? single : null);
26
+ this.defaultSlug = single.serviceSlug;
27
+ }
28
+ else {
29
+ const record = credentials;
30
+ const [first] = Object.keys(record);
31
+ if (!first)
32
+ throw new Error('ServiceTokenProvider: no credentials configured');
33
+ this.resolveCredential = (slug) => record[slug];
34
+ this.defaultSlug = first;
31
35
  }
32
- this.inFlight ??= this.refresh().finally(() => {
33
- this.inFlight = null;
34
- });
35
- return this.inFlight;
36
36
  }
37
- async refresh() {
38
- const { accessToken } = await this.http.clientAuth(this.credentials);
37
+ /** Tenant of the default credential's token, once minted — the tenant `POST /events` rows land in. */
38
+ get tenantId() {
39
+ return this.cache.get(this.defaultSlug)?.tenantId ?? null;
40
+ }
41
+ getToken(serviceSlug = this.defaultSlug) {
42
+ const cached = this.cache.get(serviceSlug);
43
+ if (cached && Date.now() < cached.expiresAt - REFRESH_MARGIN_MS)
44
+ return Promise.resolve(cached.token);
45
+ const running = this.inFlight.get(serviceSlug);
46
+ if (running)
47
+ return running;
48
+ const pending = this.refresh(serviceSlug)
49
+ .catch((err) => {
50
+ if (cached && Date.now() < cached.expiresAt) {
51
+ this.logger.warn(`bastion: token mint for ${serviceSlug} failed, serving stale cache`);
52
+ return cached.token;
53
+ }
54
+ throw err;
55
+ })
56
+ .finally(() => this.inFlight.delete(serviceSlug));
57
+ this.inFlight.set(serviceSlug, pending);
58
+ return pending;
59
+ }
60
+ async refresh(serviceSlug) {
61
+ const credential = this.resolveCredential(serviceSlug);
62
+ if (!credential?.apiKey)
63
+ throw new Error(`Bastion service client for "${serviceSlug}" not configured`);
64
+ const { accessToken } = await this.http.clientAuth(credential);
39
65
  const { exp, tenantId } = (0, jose_1.decodeJwt)(accessToken);
40
- this.token = accessToken;
41
- this.expiresAt = exp ? exp * 1000 : Date.now() + FALLBACK_TTL_MS;
42
- this.tenantId = tenantId ?? null;
43
- this.logger.log('service client token refreshed');
66
+ this.cache.set(serviceSlug, {
67
+ token: accessToken,
68
+ expiresAt: exp ? exp * 1000 : Date.now() + FALLBACK_TTL_MS,
69
+ tenantId: tenantId ?? null,
70
+ });
71
+ this.logger.log(`service client token refreshed for ${serviceSlug}`);
44
72
  return accessToken;
45
73
  }
46
74
  }
@@ -29,6 +29,14 @@ export interface ServiceClientJwtPayload extends CommonClaims {
29
29
  scopes: string[];
30
30
  }
31
31
  export type BastionJwtPayload = UserJwtPayload | ServiceClientJwtPayload;
32
+ /**
33
+ * For an app backend that accepts its *users'* tokens: the payload must be a
34
+ * user token (machine tokens are rejected whatever their scopes), minted for
35
+ * this app — Bastion sets `aud` and `appSlug` to the app slug at issuance, and
36
+ * one signing key serves the whole fleet, so a valid signature alone proves
37
+ * nothing about which app the session belongs to.
38
+ */
39
+ export declare function assertUserTokenFor(payload: BastionJwtPayload, appSlug: string): UserJwtPayload;
32
40
  export declare function isServiceClientToken(payload: BastionJwtPayload): payload is ServiceClientJwtPayload;
33
41
  export interface TokenResponse {
34
42
  accessToken: string;
@@ -1,7 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.silentLogger = void 0;
4
+ exports.assertUserTokenFor = assertUserTokenFor;
4
5
  exports.isServiceClientToken = isServiceClientToken;
6
+ const errors_js_1 = require("./errors.js");
7
+ /**
8
+ * For an app backend that accepts its *users'* tokens: the payload must be a
9
+ * user token (machine tokens are rejected whatever their scopes), minted for
10
+ * this app — Bastion sets `aud` and `appSlug` to the app slug at issuance, and
11
+ * one signing key serves the whole fleet, so a valid signature alone proves
12
+ * nothing about which app the session belongs to.
13
+ */
14
+ function assertUserTokenFor(payload, appSlug) {
15
+ if (payload.type !== undefined) {
16
+ throw new errors_js_1.BastionTokenError('not_user_token', 'Service token not allowed');
17
+ }
18
+ if (payload.aud !== appSlug || payload.appSlug !== appSlug) {
19
+ throw new errors_js_1.BastionTokenError('wrong_app', 'Invalid app context');
20
+ }
21
+ if (!payload.sub)
22
+ throw new errors_js_1.BastionTokenError('wrong_app', 'Token without subject');
23
+ return payload;
24
+ }
5
25
  function isServiceClientToken(payload) {
6
26
  return payload.type === 'service_client';
7
27
  }
@@ -0,0 +1,13 @@
1
+ import { BastionApiClient } from '../core/api-client.js';
2
+ import { BastionService } from './bastion.service.js';
3
+ import { type BastionModuleOptions } from './options.js';
4
+ /**
5
+ * `BastionApiClient` bound to this app (`serviceSlug` as `appSlug`,
6
+ * `tenantSlug`), sharing `BastionService.http`. Inject it in an app backend
7
+ * that proxies its users' login/refresh/account flows; a pure service
8
+ * (Herald, Beacon) never needs it. Failures are `BastionHttpException`s:
9
+ * an `HttpException` with Bastion's status plus `code`/`upstreamMessage`.
10
+ */
11
+ export declare class BastionApiClientProvider extends BastionApiClient {
12
+ constructor(bastion: BastionService, options: BastionModuleOptions);
13
+ }
@@ -0,0 +1,43 @@
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.BastionApiClientProvider = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const api_client_js_1 = require("../core/api-client.js");
18
+ const bastion_service_js_1 = require("./bastion.service.js");
19
+ const options_js_1 = require("./options.js");
20
+ const nest_logger_js_1 = require("./nest-logger.js");
21
+ /**
22
+ * `BastionApiClient` bound to this app (`serviceSlug` as `appSlug`,
23
+ * `tenantSlug`), sharing `BastionService.http`. Inject it in an app backend
24
+ * that proxies its users' login/refresh/account flows; a pure service
25
+ * (Herald, Beacon) never needs it. Failures are `BastionHttpException`s:
26
+ * an `HttpException` with Bastion's status plus `code`/`upstreamMessage`.
27
+ */
28
+ let BastionApiClientProvider = class BastionApiClientProvider extends api_client_js_1.BastionApiClient {
29
+ constructor(bastion, options) {
30
+ super({
31
+ http: bastion.http,
32
+ appSlug: options.serviceSlug,
33
+ tenantSlug: options.tenantSlug,
34
+ logger: (0, nest_logger_js_1.nestLogger)(api_client_js_1.BastionApiClient.name),
35
+ });
36
+ }
37
+ };
38
+ exports.BastionApiClientProvider = BastionApiClientProvider;
39
+ exports.BastionApiClientProvider = BastionApiClientProvider = __decorate([
40
+ (0, common_1.Injectable)(),
41
+ __param(1, (0, common_1.Inject)(options_js_1.BASTION_OPTIONS)),
42
+ __metadata("design:paramtypes", [bastion_service_js_1.BastionService, Object])
43
+ ], BastionApiClientProvider);
@@ -0,0 +1,19 @@
1
+ import { HttpException } from '@nestjs/common';
2
+ import { BastionHttp, type BastionRequestInit } from '../core/bastion-http.js';
3
+ import { BastionHttpError, type BastionErrorCode } from '../core/errors.js';
4
+ /**
5
+ * The Nest face of a Bastion error: an `HttpException` carrying Bastion's
6
+ * status, so the global filter answers with it unchanged, plus the stable
7
+ * `code` and the upstream message for the handlers that branch on them.
8
+ * `getResponse()` is `{ code, message }`.
9
+ */
10
+ export declare class BastionHttpException extends HttpException {
11
+ readonly code: BastionErrorCode;
12
+ readonly upstreamMessage: string;
13
+ constructor(code: BastionErrorCode, status: number, upstreamMessage: string);
14
+ static from(err: BastionHttpError): BastionHttpException;
15
+ }
16
+ /** `BastionHttp` whose failures are `BastionHttpException`s — what `BastionService.http` is. */
17
+ export declare class NestBastionHttp extends BastionHttp {
18
+ call<T>(method: string, path: string, body?: unknown, init?: BastionRequestInit): Promise<T>;
19
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NestBastionHttp = exports.BastionHttpException = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ const bastion_http_js_1 = require("../core/bastion-http.js");
6
+ const errors_js_1 = require("../core/errors.js");
7
+ /**
8
+ * The Nest face of a Bastion error: an `HttpException` carrying Bastion's
9
+ * status, so the global filter answers with it unchanged, plus the stable
10
+ * `code` and the upstream message for the handlers that branch on them.
11
+ * `getResponse()` is `{ code, message }`.
12
+ */
13
+ class BastionHttpException extends common_1.HttpException {
14
+ code;
15
+ upstreamMessage;
16
+ constructor(code, status, upstreamMessage) {
17
+ super({ code, message: upstreamMessage }, status);
18
+ this.code = code;
19
+ this.upstreamMessage = upstreamMessage;
20
+ }
21
+ static from(err) {
22
+ return new BastionHttpException(err.code, err.status, err.message);
23
+ }
24
+ }
25
+ exports.BastionHttpException = BastionHttpException;
26
+ /** `BastionHttp` whose failures are `BastionHttpException`s — what `BastionService.http` is. */
27
+ class NestBastionHttp extends bastion_http_js_1.BastionHttp {
28
+ async call(method, path, body, init) {
29
+ try {
30
+ return await super.call(method, path, body, init);
31
+ }
32
+ catch (err) {
33
+ if (err instanceof errors_js_1.BastionHttpError)
34
+ throw BastionHttpException.from(err);
35
+ throw err;
36
+ }
37
+ }
38
+ }
39
+ exports.NestBastionHttp = NestBastionHttp;
@@ -1,8 +1,15 @@
1
- import type { BastionJwtPayload } from '../core/types.js';
1
+ import { type BastionJwtPayload, type UserJwtPayload } from '../core/types.js';
2
2
  import { type BastionModuleOptions } from './options.js';
3
3
  /** Verifies inbound Bastion tokens against the cached JWKS. Throws `UnauthorizedException` on any failure. */
4
4
  export declare class BastionJwksService {
5
5
  private readonly verifier;
6
+ private readonly appSlug;
6
7
  constructor(options: BastionModuleOptions);
7
8
  verify(token: string): Promise<BastionJwtPayload>;
9
+ /**
10
+ * A *user* session token issued for this app (`options.serviceSlug` is the
11
+ * app's own slug). What an app backend puts behind its own auth guard —
12
+ * cookie extraction, suspension, age gate stay the app's business.
13
+ */
14
+ verifyUserToken(token: string): Promise<UserJwtPayload>;
8
15
  }
@@ -17,12 +17,15 @@ exports.BastionJwksService = void 0;
17
17
  const common_1 = require("@nestjs/common");
18
18
  const jwks_verifier_js_1 = require("../core/jwks-verifier.js");
19
19
  const errors_js_1 = require("../core/errors.js");
20
+ const types_js_1 = require("../core/types.js");
20
21
  const options_js_1 = require("./options.js");
21
22
  const nest_logger_js_1 = require("./nest-logger.js");
22
23
  /** Verifies inbound Bastion tokens against the cached JWKS. Throws `UnauthorizedException` on any failure. */
23
24
  let BastionJwksService = BastionJwksService_1 = class BastionJwksService {
24
25
  verifier;
26
+ appSlug;
25
27
  constructor(options) {
28
+ this.appSlug = options.serviceSlug;
26
29
  this.verifier = new jwks_verifier_js_1.JwksVerifier({
27
30
  baseUrl: options.baseUrl,
28
31
  ttlMs: options.jwksTtlMs,
@@ -40,6 +43,22 @@ let BastionJwksService = BastionJwksService_1 = class BastionJwksService {
40
43
  throw err;
41
44
  }
42
45
  }
46
+ /**
47
+ * A *user* session token issued for this app (`options.serviceSlug` is the
48
+ * app's own slug). What an app backend puts behind its own auth guard —
49
+ * cookie extraction, suspension, age gate stay the app's business.
50
+ */
51
+ async verifyUserToken(token) {
52
+ const payload = await this.verify(token);
53
+ try {
54
+ return (0, types_js_1.assertUserTokenFor)(payload, this.appSlug);
55
+ }
56
+ catch (err) {
57
+ if (err instanceof errors_js_1.BastionTokenError)
58
+ throw new common_1.UnauthorizedException(err.message);
59
+ throw err;
60
+ }
61
+ }
43
62
  };
44
63
  exports.BastionJwksService = BastionJwksService;
45
64
  exports.BastionJwksService = BastionJwksService = BastionJwksService_1 = __decorate([
@@ -16,6 +16,7 @@ const bastion_audit_service_js_1 = require("./bastion-audit.service.js");
16
16
  const service_client_jwt_guard_js_1 = require("./guards/service-client-jwt.guard.js");
17
17
  const bastion_user_guard_js_1 = require("./guards/bastion-user.guard.js");
18
18
  const audit_interceptor_js_1 = require("./interceptors/audit.interceptor.js");
19
+ const bastion_api_client_provider_js_1 = require("./bastion-api-client.provider.js");
19
20
  const PROVIDERS = [
20
21
  bastion_jwks_service_js_1.BastionJwksService,
21
22
  bastion_service_js_1.BastionService,
@@ -23,6 +24,7 @@ const PROVIDERS = [
23
24
  service_client_jwt_guard_js_1.ServiceClientJwtGuard,
24
25
  bastion_user_guard_js_1.BastionUserGuard,
25
26
  audit_interceptor_js_1.AuditInterceptor,
27
+ bastion_api_client_provider_js_1.BastionApiClientProvider,
26
28
  ];
27
29
  /**
28
30
  * ```ts
@@ -1,15 +1,13 @@
1
- import { BastionHttp } from '../core/bastion-http.js';
1
+ import { type BastionRequestInit } from '../core/bastion-http.js';
2
+ import { NestBastionHttp } from './bastion-http-exception.js';
2
3
  import type { AuditEventInput, TokenResponse } from '../core/types.js';
3
4
  import { type BastionModuleOptions } from './options.js';
4
- /** Outbound HTTP to Bastion. A non-2xx answer surfaces as `HttpException` with Bastion's status. */
5
+ /** Outbound HTTP to Bastion. A failure surfaces as `BastionHttpException` (an `HttpException` with Bastion's status, a stable `code` and the upstream message). */
5
6
  export declare class BastionService {
6
7
  private readonly options;
7
- readonly http: BastionHttp;
8
+ readonly http: NestBastionHttp;
8
9
  constructor(options: BastionModuleOptions);
9
- call<T>(method: string, path: string, body?: unknown, init?: {
10
- token?: string;
11
- headers?: Record<string, string>;
12
- }): Promise<T>;
10
+ call<T>(method: string, path: string, body?: unknown, init?: BastionRequestInit): Promise<T>;
13
11
  clientAuth(): Promise<TokenResponse>;
14
12
  writeAuditEvent(token: string, data: AuditEventInput): Promise<{
15
13
  id: string;
@@ -14,26 +14,18 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.BastionService = void 0;
16
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");
17
+ const bastion_http_exception_js_1 = require("./bastion-http-exception.js");
19
18
  const options_js_1 = require("./options.js");
20
- /** Outbound HTTP to Bastion. A non-2xx answer surfaces as `HttpException` with Bastion's status. */
19
+ /** Outbound HTTP to Bastion. A failure surfaces as `BastionHttpException` (an `HttpException` with Bastion's status, a stable `code` and the upstream message). */
21
20
  let BastionService = class BastionService {
22
21
  options;
23
22
  http;
24
23
  constructor(options) {
25
24
  this.options = options;
26
- this.http = new bastion_http_js_1.BastionHttp({ baseUrl: options.baseUrl });
25
+ this.http = new bastion_http_exception_js_1.NestBastionHttp({ baseUrl: options.baseUrl, timeoutMs: options.timeoutMs });
27
26
  }
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
- }
27
+ call(method, path, body, init) {
28
+ return this.http.call(method, path, body, init);
37
29
  }
38
30
  clientAuth() {
39
31
  if (!this.options.apiKey) {
@@ -3,6 +3,8 @@ export { BastionModule } from './bastion.module.js';
3
3
  export { BASTION_OPTIONS, type BastionModuleOptions, type BastionModuleAsyncOptions } from './options.js';
4
4
  export { BastionJwksService } from './bastion-jwks.service.js';
5
5
  export { BastionService } from './bastion.service.js';
6
+ export { BastionApiClientProvider } from './bastion-api-client.provider.js';
7
+ export { BastionHttpException, NestBastionHttp } from './bastion-http-exception.js';
6
8
  export { BastionAuditService } from './bastion-audit.service.js';
7
9
  export { ServiceClientJwtGuard } from './guards/service-client-jwt.guard.js';
8
10
  export { BastionUserGuard } from './guards/bastion-user.guard.js';
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
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;
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.NestBastionHttp = exports.BastionHttpException = exports.BastionApiClientProvider = exports.BastionService = exports.BastionJwksService = exports.BASTION_OPTIONS = exports.BastionModule = void 0;
18
18
  __exportStar(require("../core/index.js"), exports);
19
19
  var bastion_module_js_1 = require("./bastion.module.js");
20
20
  Object.defineProperty(exports, "BastionModule", { enumerable: true, get: function () { return bastion_module_js_1.BastionModule; } });
@@ -24,6 +24,11 @@ var bastion_jwks_service_js_1 = require("./bastion-jwks.service.js");
24
24
  Object.defineProperty(exports, "BastionJwksService", { enumerable: true, get: function () { return bastion_jwks_service_js_1.BastionJwksService; } });
25
25
  var bastion_service_js_1 = require("./bastion.service.js");
26
26
  Object.defineProperty(exports, "BastionService", { enumerable: true, get: function () { return bastion_service_js_1.BastionService; } });
27
+ var bastion_api_client_provider_js_1 = require("./bastion-api-client.provider.js");
28
+ Object.defineProperty(exports, "BastionApiClientProvider", { enumerable: true, get: function () { return bastion_api_client_provider_js_1.BastionApiClientProvider; } });
29
+ var bastion_http_exception_js_1 = require("./bastion-http-exception.js");
30
+ Object.defineProperty(exports, "BastionHttpException", { enumerable: true, get: function () { return bastion_http_exception_js_1.BastionHttpException; } });
31
+ Object.defineProperty(exports, "NestBastionHttp", { enumerable: true, get: function () { return bastion_http_exception_js_1.NestBastionHttp; } });
27
32
  var bastion_audit_service_js_1 = require("./bastion-audit.service.js");
28
33
  Object.defineProperty(exports, "BastionAuditService", { enumerable: true, get: function () { return bastion_audit_service_js_1.BastionAuditService; } });
29
34
  var service_client_jwt_guard_js_1 = require("./guards/service-client-jwt.guard.js");
@@ -9,6 +9,8 @@ export interface BastionModuleOptions {
9
9
  apiKey?: string;
10
10
  /** Required by Bastion when the app is `isGlobal` or registered in more than one tenant. */
11
11
  tenantSlug?: string;
12
+ /** Per-request timeout for outbound calls, default 8 000 ms. */
13
+ timeoutMs?: number;
12
14
  /** `BASTION_JWKS_TTL_MS`, default 300 000. */
13
15
  jwksTtlMs?: number;
14
16
  /** Expected `iss`. Default `'bastion'`; `null` disables the check. */