@dunx/http 3.8.2 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,6 +95,10 @@ from, and it may change in any release.
95
95
  otherwise silently keep one.
96
96
  - Handlers may return a `Response`, any JSON-serialisable value, or `undefined`
97
97
  for a 204.
98
+ - `Authorize` and `gate()` are the contract an ops surface gates itself with:
99
+ raw request in, 404 on refusal, and a returned `Response` sent as written for a
100
+ browser that needs a sign-in page. `@dunx/dashboard` and `@dunx/openapi` both
101
+ take one, so an app writes the policy once and hands it to both.
98
102
  - Schemas, parsers and the status resolve at boot into the same closure the
99
103
  middleware chain folds into. A request reads no metadata and does no lookup.
100
104
  - Every request adopts W3C Trace Context, so `traceId`, `spanId`, `parentSpanId`
package/dist/client.js CHANGED
@@ -41,7 +41,7 @@ class FetchTransportError extends AppError {
41
41
  this.aborted = aborted;
42
42
  }
43
43
  }
44
- Object.defineProperty(FetchTransportError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly response: { readonly method: string; readonly url: string }" }, { unresolved: "readonly aborted: boolean" }, ErrorOptions] });
44
+ Object.defineProperty(FetchTransportError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly response: { readonly method: string; readonly url: string }" }, { unresolved: "readonly aborted: boolean" }, typeof ErrorOptions === "undefined" ? { unresolved: "options?: ErrorOptions" } : ErrorOptions] });
45
45
  // src/client/options.ts
46
46
  class HttpClientOptions {
47
47
  baseUrl;
@@ -54,3 +54,9 @@ export declare abstract class PingProbe {
54
54
  export declare abstract class QueryProbe {
55
55
  abstract ping(): Promise<void>;
56
56
  }
57
+ /** Enough of an object store to answer "can I reach it", which `Storage` from
58
+ * `@dunx/infra/files` is on either backend. `exists` and not a read or a write:
59
+ * one `HEAD` against S3, one `stat` locally, and nothing left behind. */
60
+ export declare abstract class StorageProbe {
61
+ abstract exists(key: string): Promise<boolean>;
62
+ }
@@ -1,16 +1,47 @@
1
- import { HealthIndicator, type PingProbe, type ProbeResult, type QueryProbe } from './contracts.js';
1
+ import { HealthIndicator, type PingProbe, type ProbeResult, type QueryProbe, type StorageProbe } from './contracts.js';
2
+ /** Up on a completed round trip, down with the thrown message, detail the
3
+ * latency. Subclass it with a `name` for anything else answering a `ping()`. */
4
+ export declare abstract class RoundTripIndicator extends HealthIndicator {
5
+ private readonly probe;
6
+ constructor(probe: QueryProbe);
7
+ check(): Promise<ProbeResult>;
8
+ }
2
9
  /** Redis is up if it answers `PING`. */
3
- export declare class RedisIndicator extends HealthIndicator {
4
- private readonly redis;
10
+ export declare class RedisIndicator extends RoundTripIndicator {
5
11
  readonly name = "redis";
6
12
  constructor(redis: PingProbe);
7
- check(): Promise<ProbeResult>;
8
13
  }
9
14
  /** The database is up if a round trip completes. */
10
- export declare class DatabaseIndicator extends HealthIndicator {
11
- private readonly db;
15
+ export declare class DatabaseIndicator extends RoundTripIndicator {
12
16
  readonly name = "database";
13
- constructor(db: QueryProbe);
17
+ }
18
+ /**
19
+ * The AMQP broker, up while the connection is established and unblocked, and
20
+ * satisfied by `AmqpConnection` from `@dunx/infra/amqp`. The first probe on an
21
+ * unopened connection waits `readyTimeoutMs`, whose 5 s default outruns
22
+ * `timeoutMs` at 2 s and so reports `unknown`; the rest never wait.
23
+ */
24
+ export declare class AmqpIndicator extends RoundTripIndicator {
25
+ readonly name = "amqp";
26
+ }
27
+ export interface StorageProbeOptionsInit {
28
+ readonly key?: string;
29
+ }
30
+ export declare class StorageProbeOptions {
31
+ readonly key: string;
32
+ constructor(init?: StorageProbeOptionsInit);
33
+ }
34
+ /**
35
+ * The configured object store, which is what `DiskIndicator` stops measuring the
36
+ * moment an app moves to `S3Storage`. Whether the key exists is no part of the
37
+ * signal and the answer is discarded: a store that replies is up, one that throws
38
+ * is down with the message expired credentials and a missing bucket arrive as.
39
+ */
40
+ export declare class StorageIndicator extends HealthIndicator {
41
+ private readonly storage;
42
+ private readonly options;
43
+ readonly name = "storage";
44
+ constructor(storage: StorageProbe, options?: StorageProbeOptions);
14
45
  check(): Promise<ProbeResult>;
15
46
  }
16
47
  export interface MemoryOptionsInit {
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { Controller, Delete, Get, Patch, Post, Put, } from './route/decorators.j
2
2
  export type { HttpMethod, RoutePath } from './route/marker.js';
3
3
  export { ApiHidden, HIDDEN, meta, metaKey, metaOf, mergeMeta, Public, PUBLIC, Roles, ROLES, STREAMS, UNMATCHED, UseGuards, type MetaKey, type MetaRecord, } from './route/metadata.js';
4
4
  export type { InferOutput, Input, JsonSchema, ResponseMap, Returns, RouteInput, RouteSchemas, StandardSchemaIssue, StandardSchemaResult, StandardSchemaV1, } from './route/schema.js';
5
+ export { gate, type Authorize, type AuthorizeDecision, } from './server/authorize.js';
5
6
  export { ClientAddress } from './server/client-address.js';
6
7
  export type { RouteContext } from './server/context.js';
7
8
  export type { CorsOptions, CorsOrigin } from './server/cors.js';
@@ -40,9 +41,9 @@ export { PostgresRelayConnectionOptions, RelayConnectionOptions, WsRelayModule,
40
41
  export { RelayPublisher, type RelayPublisherInit, } from './ws/relay-publisher.js';
41
42
  export { DEFAULT_RELAY_CHANNEL, WsRelay, type PubSubRelay, type RelayOptions, } from './ws/relay.js';
42
43
  export type { Socket, SocketData, SocketErrorHandler, SocketOptions, } from './ws/socket.js';
43
- export { HealthIndicator, PingProbe, QueryProbe, type ProbeResult, type ProbeState, } from './health/contracts.js';
44
+ export { HealthIndicator, PingProbe, QueryProbe, StorageProbe, type ProbeResult, type ProbeState, } from './health/contracts.js';
44
45
  export { HealthController } from './health/controller.js';
45
- export { DatabaseIndicator, DiskIndicator, DiskOptions, MemoryIndicator, MemoryOptions, RedisIndicator, type DiskOptionsInit, type MemoryOptionsInit, } from './health/indicators.js';
46
+ export { AmqpIndicator, DatabaseIndicator, DiskIndicator, DiskOptions, MemoryIndicator, MemoryOptions, RedisIndicator, RoundTripIndicator, StorageIndicator, StorageProbeOptions, type DiskOptionsInit, type MemoryOptionsInit, type StorageProbeOptionsInit, } from './health/indicators.js';
46
47
  export { HealthModule } from './health/module.js';
47
48
  export { HEALTH_REPORT_SCHEMA } from './health/report-schema.js';
48
49
  export { Readiness, ReadinessOptions } from './health/readiness.js';
package/dist/index.js CHANGED
@@ -62,6 +62,17 @@ var Post = verb("POST");
62
62
  var Put = verb("PUT");
63
63
  var Patch = verb("PATCH");
64
64
  var Delete = verb("DELETE");
65
+ // src/server/authorize.ts
66
+ var gate = async (authorize, req) => {
67
+ if (authorize === undefined)
68
+ return;
69
+ const decision = await authorize(req);
70
+ if (decision === true)
71
+ return;
72
+ if (decision instanceof Response)
73
+ return decision;
74
+ return Response.json({ error: "NOT_FOUND", status: 404 }, { status: 404 });
75
+ };
65
76
  // src/server/client-address.ts
66
77
  import { AppError } from "@dunx/core";
67
78
  var trustedHops = (setting) => {
@@ -3010,6 +3021,9 @@ class PingProbe {
3010
3021
 
3011
3022
  class QueryProbe {
3012
3023
  }
3024
+
3025
+ class StorageProbe {
3026
+ }
3013
3027
  // src/health/controller.ts
3014
3028
  import { inject } from "@dunx/core";
3015
3029
 
@@ -3211,35 +3225,64 @@ let _HiddenHealthController = HiddenHealthController;
3211
3225
  import { statfs } from "fs/promises";
3212
3226
  var ms = (started) => Math.round(performance.now() - started);
3213
3227
 
3214
- class RedisIndicator extends HealthIndicator {
3215
- redis;
3216
- name = "redis";
3217
- constructor(redis) {
3228
+ class RoundTripIndicator extends HealthIndicator {
3229
+ probe;
3230
+ constructor(probe) {
3218
3231
  super();
3219
- this.redis = redis;
3232
+ this.probe = probe;
3220
3233
  }
3221
3234
  async check() {
3222
3235
  const started = performance.now();
3223
- await this.redis.ping();
3236
+ await this.probe.ping();
3224
3237
  return { state: "up", detail: `${ms(started)} ms` };
3225
3238
  }
3226
3239
  }
3227
- Object.defineProperty(RedisIndicator, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "private readonly redis: PingProbe", typeOnly: "PingProbe" }] });
3240
+ Object.defineProperty(RoundTripIndicator, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "private readonly probe: QueryProbe", typeOnly: "QueryProbe" }] });
3241
+
3242
+ class RedisIndicator extends RoundTripIndicator {
3243
+ name = "redis";
3244
+ constructor(redis) {
3245
+ super({
3246
+ ping: async () => {
3247
+ await redis.ping();
3248
+ }
3249
+ });
3250
+ }
3251
+ }
3252
+ Object.defineProperty(RedisIndicator, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "redis: PingProbe", typeOnly: "PingProbe" }] });
3228
3253
 
3229
- class DatabaseIndicator extends HealthIndicator {
3230
- db;
3254
+ class DatabaseIndicator extends RoundTripIndicator {
3231
3255
  name = "database";
3232
- constructor(db) {
3256
+ }
3257
+
3258
+ class AmqpIndicator extends RoundTripIndicator {
3259
+ name = "amqp";
3260
+ }
3261
+
3262
+ class StorageProbeOptions {
3263
+ key;
3264
+ constructor(init = {}) {
3265
+ this.key = init.key ?? ".dunx-health";
3266
+ }
3267
+ }
3268
+ Object.defineProperty(StorageProbeOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: StorageProbeOptionsInit = {}", optional: true }] });
3269
+
3270
+ class StorageIndicator extends HealthIndicator {
3271
+ storage;
3272
+ options;
3273
+ name = "storage";
3274
+ constructor(storage, options = new StorageProbeOptions) {
3233
3275
  super();
3234
- this.db = db;
3276
+ this.storage = storage;
3277
+ this.options = options;
3235
3278
  }
3236
3279
  async check() {
3237
3280
  const started = performance.now();
3238
- await this.db.ping();
3281
+ await this.storage.exists(this.options.key);
3239
3282
  return { state: "up", detail: `${ms(started)} ms` };
3240
3283
  }
3241
3284
  }
3242
- Object.defineProperty(DatabaseIndicator, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "private readonly db: QueryProbe", typeOnly: "QueryProbe" }] });
3285
+ Object.defineProperty(StorageIndicator, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "private readonly storage: StorageProbe", typeOnly: "StorageProbe" }, StorageProbeOptions] });
3243
3286
 
3244
3287
  class MemoryOptions {
3245
3288
  maxRssBytes;
@@ -3393,6 +3436,7 @@ __runInitializers(_init, 1, HealthModule);
3393
3436
  __decoratorMetadata(_init, HealthModule);
3394
3437
  let _HealthModule = HealthModule;
3395
3438
  export {
3439
+ AmqpIndicator,
3396
3440
  ApiHidden2 as ApiHidden,
3397
3441
  ClientAddress,
3398
3442
  Compression,
@@ -3452,6 +3496,7 @@ export {
3452
3496
  RequestLoggingMiddleware,
3453
3497
  RequestMetrics,
3454
3498
  Roles2 as Roles,
3499
+ RoundTripIndicator,
3455
3500
  SKIP_THROTTLE,
3456
3501
  STREAMS2 as STREAMS,
3457
3502
  SkipThrottle,
@@ -3462,6 +3507,9 @@ export {
3462
3507
  StaticFiles2 as StaticFiles,
3463
3508
  StaticModule,
3464
3509
  StaticOptions2 as StaticOptions,
3510
+ StorageIndicator,
3511
+ StorageProbe,
3512
+ StorageProbeOptions,
3465
3513
  THROTTLE,
3466
3514
  TRACEPARENT_HEADER2 as TRACEPARENT_HEADER,
3467
3515
  TRACERESPONSE_HEADER2 as TRACERESPONSE_HEADER,
@@ -3480,6 +3528,7 @@ export {
3480
3528
  WsRelayModule,
3481
3529
  defaultErrorMapper,
3482
3530
  errorMapper,
3531
+ gate,
3483
3532
  mergeMeta2 as mergeMeta,
3484
3533
  meta2 as meta,
3485
3534
  metaKey2 as metaKey,
@@ -0,0 +1,18 @@
1
+ import type { BunRequest } from 'bun';
2
+ /**
3
+ * What an {@link Authorize} answers. `true` passes and `false` refuses with a
4
+ * 404; a `Response` refuses with itself, which is what a browser needs.
5
+ */
6
+ export type AuthorizeDecision = boolean | Response;
7
+ /**
8
+ * Decides whether a request may see an ops surface at all - the dashboard page,
9
+ * the OpenAPI explorer. Both sit outside the app's session guard, so this gets
10
+ * the raw `BunRequest` and asks the auth library. Refusal is 404, not 403.
11
+ */
12
+ export type Authorize = (req: BunRequest) => AuthorizeDecision | Promise<AuthorizeDecision>;
13
+ /**
14
+ * Runs the gate: `undefined` to carry on, or the response to answer with. Only
15
+ * `true` admits and only a `Response` replaces the refusal, so an `authorize`
16
+ * that falls off the end of a branch closes rather than opens.
17
+ */
18
+ export declare const gate: (authorize: Authorize | undefined, req: BunRequest) => Promise<Response | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/http",
3
- "version": "3.8.2",
3
+ "version": "3.9.0",
4
4
  "description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
5
5
  "keywords": [
6
6
  "bun",
@@ -76,7 +76,7 @@
76
76
  "peerDependencies": {
77
77
  "@bufbuild/protobuf": "^2.15.0",
78
78
  "@connectrpc/connect": "^2.2.0",
79
- "@dunx/core": "^3.8.2",
79
+ "@dunx/core": "^3.9.0",
80
80
  "@types/bun": ">=1.4.1"
81
81
  },
82
82
  "peerDependenciesMeta": {