@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 +4 -0
- package/dist/client.js +1 -1
- package/dist/health/contracts.d.ts +6 -0
- package/dist/health/indicators.d.ts +38 -7
- package/dist/index.d.ts +3 -2
- package/dist/index.js +62 -13
- package/dist/server/authorize.d.ts +18 -0
- package/package.json +2 -2
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
|
|
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
|
|
11
|
-
private readonly db;
|
|
15
|
+
export declare class DatabaseIndicator extends RoundTripIndicator {
|
|
12
16
|
readonly name = "database";
|
|
13
|
-
|
|
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
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
constructor(redis) {
|
|
3228
|
+
class RoundTripIndicator extends HealthIndicator {
|
|
3229
|
+
probe;
|
|
3230
|
+
constructor(probe) {
|
|
3218
3231
|
super();
|
|
3219
|
-
this.
|
|
3232
|
+
this.probe = probe;
|
|
3220
3233
|
}
|
|
3221
3234
|
async check() {
|
|
3222
3235
|
const started = performance.now();
|
|
3223
|
-
await this.
|
|
3236
|
+
await this.probe.ping();
|
|
3224
3237
|
return { state: "up", detail: `${ms(started)} ms` };
|
|
3225
3238
|
}
|
|
3226
3239
|
}
|
|
3227
|
-
Object.defineProperty(
|
|
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
|
|
3230
|
-
db;
|
|
3254
|
+
class DatabaseIndicator extends RoundTripIndicator {
|
|
3231
3255
|
name = "database";
|
|
3232
|
-
|
|
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.
|
|
3276
|
+
this.storage = storage;
|
|
3277
|
+
this.options = options;
|
|
3235
3278
|
}
|
|
3236
3279
|
async check() {
|
|
3237
3280
|
const started = performance.now();
|
|
3238
|
-
await this.
|
|
3281
|
+
await this.storage.exists(this.options.key);
|
|
3239
3282
|
return { state: "up", detail: `${ms(started)} ms` };
|
|
3240
3283
|
}
|
|
3241
3284
|
}
|
|
3242
|
-
Object.defineProperty(
|
|
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.
|
|
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.
|
|
79
|
+
"@dunx/core": "^3.9.0",
|
|
80
80
|
"@types/bun": ">=1.4.1"
|
|
81
81
|
},
|
|
82
82
|
"peerDependenciesMeta": {
|