@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,37 +1,49 @@
1
- import { BastionHttpError } from './errors.js';
1
+ import { BastionErrorCode, BastionHttpError } from './errors.js';
2
2
  /**
3
3
  * Outbound calls to Bastion. Nothing here verifies inbound tokens — that is
4
- * `JwksVerifier`. `call` is public so a service can reach any other route
5
- * (e.g. `POST /auth/login` from a BFF) with the same error handling; pass the
6
- * browser's IP as `X-Real-IP` on user-facing calls so Bastion's rate-limit
7
- * and audit rows see the real client, not your container.
4
+ * `JwksVerifier`. `call` is public so a service can reach any route with the
5
+ * same error handling: a non-2xx answer becomes `BastionHttpError` with the
6
+ * status and a stable `code`; Bastion unreachable or timed out is a 503
7
+ * `UNAVAILABLE`. Pass the browser's IP as `X-Real-IP` on user-facing calls.
8
8
  */
9
9
  export class BastionHttp {
10
10
  baseUrl;
11
+ timeoutMs;
11
12
  fetchImpl;
12
13
  constructor(options) {
13
14
  this.baseUrl = options.baseUrl.replace(/\/$/, '');
14
- this.fetchImpl = options.fetch ?? fetch;
15
+ this.timeoutMs = options.timeoutMs ?? 8_000;
16
+ // Late-bound on purpose: a consumer (or its tests) may replace global fetch after construction.
17
+ this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
15
18
  }
16
19
  async call(method, path, body, init = {}) {
17
- const headers = {
18
- 'Content-Type': 'application/json',
19
- ...init.headers,
20
- };
20
+ const headers = { ...init.headers };
21
+ if (body !== undefined)
22
+ headers['Content-Type'] = 'application/json';
21
23
  if (init.token)
22
24
  headers['Authorization'] = `Bearer ${init.token}`;
23
- const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
24
- method,
25
- headers,
26
- body: body === undefined ? undefined : JSON.stringify(body),
27
- });
25
+ let res;
26
+ try {
27
+ res = await this.fetchImpl(`${this.baseUrl}${path}`, {
28
+ method,
29
+ headers,
30
+ body: body === undefined ? undefined : JSON.stringify(body),
31
+ signal: AbortSignal.timeout(init.timeoutMs ?? this.timeoutMs),
32
+ });
33
+ }
34
+ catch (err) {
35
+ throw new BastionHttpError(503, `Bastion unreachable: ${err.message}`, BastionErrorCode.UNAVAILABLE);
36
+ }
28
37
  if (!res.ok) {
29
- const err = (await res.json().catch(() => ({})));
30
- throw new BastionHttpError(res.status, err.message ?? 'Bastion error');
38
+ const parsed = (await res.json().catch(() => ({})));
39
+ const message = Array.isArray(parsed.message)
40
+ ? parsed.message.join(', ')
41
+ : (parsed.message ?? `Bastion error ${res.status}`);
42
+ throw new BastionHttpError(res.status, message);
31
43
  }
32
44
  if (res.status === 204)
33
45
  return undefined;
34
- return (await res.json());
46
+ return (await res.json().catch(() => undefined));
35
47
  }
36
48
  clientAuth(input) {
37
49
  return this.call('POST', '/auth/client', input);
@@ -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
  }
@@ -7,12 +7,53 @@ export class BastionTokenError extends Error {
7
7
  this.name = 'BastionTokenError';
8
8
  }
9
9
  }
10
- /** A non-2xx answer from Bastion's HTTP API. */
10
+ /**
11
+ * Stable classification of a Bastion error answer, so an app can branch on a
12
+ * code instead of on Bastion's message text. The age-gate codes are Bastion's
13
+ * own (it returns them verbatim); the rest are derived from status + message
14
+ * until Bastion carries a `code` field on every error.
15
+ */
16
+ export const BastionErrorCode = {
17
+ EMAIL_NOT_VERIFIED: 'EMAIL_NOT_VERIFIED',
18
+ INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
19
+ NO_APP_ACCESS: 'NO_APP_ACCESS',
20
+ ACCOUNT_INACTIVE: 'ACCOUNT_INACTIVE',
21
+ CONFLICT: 'CONFLICT',
22
+ BIRTH_DATE_REQUIRED: 'BIRTH_DATE_REQUIRED',
23
+ BIRTH_DATE_INVALID: 'BIRTH_DATE_INVALID',
24
+ UNDER_MINIMUM_AGE: 'UNDER_MINIMUM_AGE',
25
+ UNAVAILABLE: 'UNAVAILABLE',
26
+ UPSTREAM: 'UPSTREAM',
27
+ };
28
+ export function classifyBastionError(status, message) {
29
+ if (message === BastionErrorCode.BIRTH_DATE_REQUIRED ||
30
+ message === BastionErrorCode.BIRTH_DATE_INVALID ||
31
+ message === BastionErrorCode.UNDER_MINIMUM_AGE) {
32
+ return message;
33
+ }
34
+ if (status === 403 && /email not verified/i.test(message))
35
+ return BastionErrorCode.EMAIL_NOT_VERIFIED;
36
+ if (status === 401) {
37
+ if (/no access to this app/i.test(message))
38
+ return BastionErrorCode.NO_APP_ACCESS;
39
+ if (/inactive|locked/i.test(message))
40
+ return BastionErrorCode.ACCOUNT_INACTIVE;
41
+ return BastionErrorCode.INVALID_CREDENTIALS;
42
+ }
43
+ if (status === 409)
44
+ return BastionErrorCode.CONFLICT;
45
+ if (status === 503)
46
+ return BastionErrorCode.UNAVAILABLE;
47
+ return BastionErrorCode.UPSTREAM;
48
+ }
49
+ /** A non-2xx answer from Bastion's HTTP API, or Bastion unreachable (`status` 503, code `UNAVAILABLE`). */
11
50
  export class BastionHttpError extends Error {
12
51
  status;
13
- constructor(status, message) {
52
+ code;
53
+ constructor(status, message, code) {
14
54
  super(message);
15
55
  this.status = status;
16
56
  this.name = 'BastionHttpError';
57
+ this.code = code ?? classifyBastionError(status, message);
17
58
  }
18
59
  }
@@ -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';
@@ -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 } from './jwks-verifier.js';
4
5
  export { BastionHttp } from './bastion-http.js';
5
6
  export { ServiceTokenProvider } from './service-token-provider.js';
7
+ export { BastionApiClient } from './api-client.js';
@@ -30,7 +30,8 @@ export class JwksVerifier {
30
30
  this.ttlMs = options.ttlMs ?? 300_000;
31
31
  this.issuer = options.issuer === undefined ? 'bastion' : options.issuer;
32
32
  this.logger = options.logger ?? silentLogger;
33
- this.fetchImpl = options.fetch ?? fetch;
33
+ // Late-bound on purpose: a consumer (or its tests) may replace global fetch after construction.
34
+ this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
34
35
  }
35
36
  async verify(token) {
36
37
  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
  }
@@ -2,42 +2,70 @@ import { decodeJwt } from 'jose';
2
2
  import { silentLogger } from './types.js';
3
3
  const REFRESH_MARGIN_MS = 5 * 60 * 1000;
4
4
  const FALLBACK_TTL_MS = 60 * 60 * 1000;
5
- /**
6
- * Holds the service-client token for outbound calls and renews it ahead of
7
- * `exp`. Single-flight: a burst of first calls shares one `POST /auth/client`
8
- * instead of issuing one per caller — the previous per-service copies had no
9
- * such guard.
10
- */
11
5
  export class ServiceTokenProvider {
12
6
  http;
13
- credentials;
14
7
  logger;
15
- token = null;
16
- expiresAt = 0;
17
- inFlight = null;
18
- /** Tenant the token was issued for — the tenant `POST /events` rows land in. */
19
- tenantId = null;
8
+ resolveCredential;
9
+ defaultSlug;
10
+ cache = new Map();
11
+ inFlight = new Map();
20
12
  constructor(http, credentials, logger = silentLogger) {
21
13
  this.http = http;
22
- this.credentials = credentials;
23
14
  this.logger = logger;
24
- }
25
- getToken() {
26
- if (this.token && Date.now() < this.expiresAt - REFRESH_MARGIN_MS) {
27
- return Promise.resolve(this.token);
15
+ if ('resolve' in credentials && typeof credentials.resolve === 'function') {
16
+ const resolver = credentials;
17
+ this.resolveCredential = resolver.resolve;
18
+ this.defaultSlug = resolver.defaultSlug;
19
+ }
20
+ else if ('apiKey' in credentials && typeof credentials.apiKey === 'string') {
21
+ const single = credentials;
22
+ this.resolveCredential = (slug) => (slug === single.serviceSlug ? single : null);
23
+ this.defaultSlug = single.serviceSlug;
24
+ }
25
+ else {
26
+ const record = credentials;
27
+ const [first] = Object.keys(record);
28
+ if (!first)
29
+ throw new Error('ServiceTokenProvider: no credentials configured');
30
+ this.resolveCredential = (slug) => record[slug];
31
+ this.defaultSlug = first;
28
32
  }
29
- this.inFlight ??= this.refresh().finally(() => {
30
- this.inFlight = null;
31
- });
32
- return this.inFlight;
33
33
  }
34
- async refresh() {
35
- const { accessToken } = await this.http.clientAuth(this.credentials);
34
+ /** Tenant of the default credential's token, once minted — the tenant `POST /events` rows land in. */
35
+ get tenantId() {
36
+ return this.cache.get(this.defaultSlug)?.tenantId ?? null;
37
+ }
38
+ getToken(serviceSlug = this.defaultSlug) {
39
+ const cached = this.cache.get(serviceSlug);
40
+ if (cached && Date.now() < cached.expiresAt - REFRESH_MARGIN_MS)
41
+ return Promise.resolve(cached.token);
42
+ const running = this.inFlight.get(serviceSlug);
43
+ if (running)
44
+ return running;
45
+ const pending = this.refresh(serviceSlug)
46
+ .catch((err) => {
47
+ if (cached && Date.now() < cached.expiresAt) {
48
+ this.logger.warn(`bastion: token mint for ${serviceSlug} failed, serving stale cache`);
49
+ return cached.token;
50
+ }
51
+ throw err;
52
+ })
53
+ .finally(() => this.inFlight.delete(serviceSlug));
54
+ this.inFlight.set(serviceSlug, pending);
55
+ return pending;
56
+ }
57
+ async refresh(serviceSlug) {
58
+ const credential = this.resolveCredential(serviceSlug);
59
+ if (!credential?.apiKey)
60
+ throw new Error(`Bastion service client for "${serviceSlug}" not configured`);
61
+ const { accessToken } = await this.http.clientAuth(credential);
36
62
  const { exp, tenantId } = decodeJwt(accessToken);
37
- this.token = accessToken;
38
- this.expiresAt = exp ? exp * 1000 : Date.now() + FALLBACK_TTL_MS;
39
- this.tenantId = tenantId ?? null;
40
- this.logger.log('service client token refreshed');
63
+ this.cache.set(serviceSlug, {
64
+ token: accessToken,
65
+ expiresAt: exp ? exp * 1000 : Date.now() + FALLBACK_TTL_MS,
66
+ tenantId: tenantId ?? null,
67
+ });
68
+ this.logger.log(`service client token refreshed for ${serviceSlug}`);
41
69
  return accessToken;
42
70
  }
43
71
  }
@@ -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,3 +1,22 @@
1
+ import { BastionTokenError } from './errors.js';
2
+ /**
3
+ * For an app backend that accepts its *users'* tokens: the payload must be a
4
+ * user token (machine tokens are rejected whatever their scopes), minted for
5
+ * this app — Bastion sets `aud` and `appSlug` to the app slug at issuance, and
6
+ * one signing key serves the whole fleet, so a valid signature alone proves
7
+ * nothing about which app the session belongs to.
8
+ */
9
+ export function assertUserTokenFor(payload, appSlug) {
10
+ if (payload.type !== undefined) {
11
+ throw new BastionTokenError('not_user_token', 'Service token not allowed');
12
+ }
13
+ if (payload.aud !== appSlug || payload.appSlug !== appSlug) {
14
+ throw new BastionTokenError('wrong_app', 'Invalid app context');
15
+ }
16
+ if (!payload.sub)
17
+ throw new BastionTokenError('wrong_app', 'Token without subject');
18
+ return payload;
19
+ }
1
20
  export function isServiceClientToken(payload) {
2
21
  return payload.type === 'service_client';
3
22
  }
@@ -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,40 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable } from '@nestjs/common';
14
+ import { BastionApiClient } from '../core/api-client.js';
15
+ import { BastionService } from './bastion.service.js';
16
+ import { BASTION_OPTIONS } from './options.js';
17
+ import { nestLogger } from './nest-logger.js';
18
+ /**
19
+ * `BastionApiClient` bound to this app (`serviceSlug` as `appSlug`,
20
+ * `tenantSlug`), sharing `BastionService.http`. Inject it in an app backend
21
+ * that proxies its users' login/refresh/account flows; a pure service
22
+ * (Herald, Beacon) never needs it. Failures are `BastionHttpException`s:
23
+ * an `HttpException` with Bastion's status plus `code`/`upstreamMessage`.
24
+ */
25
+ let BastionApiClientProvider = class BastionApiClientProvider extends BastionApiClient {
26
+ constructor(bastion, options) {
27
+ super({
28
+ http: bastion.http,
29
+ appSlug: options.serviceSlug,
30
+ tenantSlug: options.tenantSlug,
31
+ logger: nestLogger(BastionApiClient.name),
32
+ });
33
+ }
34
+ };
35
+ BastionApiClientProvider = __decorate([
36
+ Injectable(),
37
+ __param(1, Inject(BASTION_OPTIONS)),
38
+ __metadata("design:paramtypes", [BastionService, Object])
39
+ ], BastionApiClientProvider);
40
+ export { 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,34 @@
1
+ import { HttpException } from '@nestjs/common';
2
+ import { BastionHttp } from '../core/bastion-http.js';
3
+ import { BastionHttpError } 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 class BastionHttpException extends HttpException {
11
+ code;
12
+ upstreamMessage;
13
+ constructor(code, status, upstreamMessage) {
14
+ super({ code, message: upstreamMessage }, status);
15
+ this.code = code;
16
+ this.upstreamMessage = upstreamMessage;
17
+ }
18
+ static from(err) {
19
+ return new BastionHttpException(err.code, err.status, err.message);
20
+ }
21
+ }
22
+ /** `BastionHttp` whose failures are `BastionHttpException`s — what `BastionService.http` is. */
23
+ export class NestBastionHttp extends BastionHttp {
24
+ async call(method, path, body, init) {
25
+ try {
26
+ return await super.call(method, path, body, init);
27
+ }
28
+ catch (err) {
29
+ if (err instanceof BastionHttpError)
30
+ throw BastionHttpException.from(err);
31
+ throw err;
32
+ }
33
+ }
34
+ }
@@ -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
  }
@@ -14,12 +14,15 @@ var BastionJwksService_1;
14
14
  import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
15
15
  import { JwksVerifier } from '../core/jwks-verifier.js';
16
16
  import { BastionTokenError } from '../core/errors.js';
17
+ import { assertUserTokenFor } from '../core/types.js';
17
18
  import { BASTION_OPTIONS } from './options.js';
18
19
  import { nestLogger } from './nest-logger.js';
19
20
  /** Verifies inbound Bastion tokens against the cached JWKS. Throws `UnauthorizedException` on any failure. */
20
21
  let BastionJwksService = BastionJwksService_1 = class BastionJwksService {
21
22
  verifier;
23
+ appSlug;
22
24
  constructor(options) {
25
+ this.appSlug = options.serviceSlug;
23
26
  this.verifier = new JwksVerifier({
24
27
  baseUrl: options.baseUrl,
25
28
  ttlMs: options.jwksTtlMs,
@@ -37,6 +40,22 @@ let BastionJwksService = BastionJwksService_1 = class BastionJwksService {
37
40
  throw err;
38
41
  }
39
42
  }
43
+ /**
44
+ * A *user* session token issued for this app (`options.serviceSlug` is the
45
+ * app's own slug). What an app backend puts behind its own auth guard —
46
+ * cookie extraction, suspension, age gate stay the app's business.
47
+ */
48
+ async verifyUserToken(token) {
49
+ const payload = await this.verify(token);
50
+ try {
51
+ return assertUserTokenFor(payload, this.appSlug);
52
+ }
53
+ catch (err) {
54
+ if (err instanceof BastionTokenError)
55
+ throw new UnauthorizedException(err.message);
56
+ throw err;
57
+ }
58
+ }
40
59
  };
41
60
  BastionJwksService = BastionJwksService_1 = __decorate([
42
61
  Injectable(),
@@ -13,6 +13,7 @@ import { BastionAuditService } from './bastion-audit.service.js';
13
13
  import { ServiceClientJwtGuard } from './guards/service-client-jwt.guard.js';
14
14
  import { BastionUserGuard } from './guards/bastion-user.guard.js';
15
15
  import { AuditInterceptor } from './interceptors/audit.interceptor.js';
16
+ import { BastionApiClientProvider } from './bastion-api-client.provider.js';
16
17
  const PROVIDERS = [
17
18
  BastionJwksService,
18
19
  BastionService,
@@ -20,6 +21,7 @@ const PROVIDERS = [
20
21
  ServiceClientJwtGuard,
21
22
  BastionUserGuard,
22
23
  AuditInterceptor,
24
+ BastionApiClientProvider,
23
25
  ];
24
26
  /**
25
27
  * ```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;
@@ -10,27 +10,19 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  var __param = (this && this.__param) || function (paramIndex, decorator) {
11
11
  return function (target, key) { decorator(target, key, paramIndex); }
12
12
  };
13
- import { HttpException, Inject, Injectable } from '@nestjs/common';
14
- import { BastionHttp } from '../core/bastion-http.js';
15
- import { BastionHttpError } from '../core/errors.js';
13
+ import { Inject, Injectable } from '@nestjs/common';
14
+ import { NestBastionHttp } from './bastion-http-exception.js';
16
15
  import { BASTION_OPTIONS } from './options.js';
17
- /** Outbound HTTP to Bastion. A non-2xx answer surfaces as `HttpException` with Bastion's status. */
16
+ /** Outbound HTTP to Bastion. A failure surfaces as `BastionHttpException` (an `HttpException` with Bastion's status, a stable `code` and the upstream message). */
18
17
  let BastionService = class BastionService {
19
18
  options;
20
19
  http;
21
20
  constructor(options) {
22
21
  this.options = options;
23
- this.http = new BastionHttp({ baseUrl: options.baseUrl });
22
+ this.http = new NestBastionHttp({ baseUrl: options.baseUrl, timeoutMs: options.timeoutMs });
24
23
  }
25
- async call(method, path, body, init) {
26
- try {
27
- return await this.http.call(method, path, body, init);
28
- }
29
- catch (err) {
30
- if (err instanceof BastionHttpError)
31
- throw new HttpException(err.message, err.status);
32
- throw err;
33
- }
24
+ call(method, path, body, init) {
25
+ return this.http.call(method, path, body, init);
34
26
  }
35
27
  clientAuth() {
36
28
  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';
@@ -3,6 +3,8 @@ export { BastionModule } from './bastion.module.js';
3
3
  export { BASTION_OPTIONS } 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';