@rdlabo/workers-hono-kit 0.6.13 → 0.6.14
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 +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/middleware/maintenance.d.ts +113 -0
- package/dist/middleware/maintenance.js +171 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,7 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
|
|
|
78
78
|
| `HTTP_ERROR_PHRASES` | `{ 400, 401, 403, 404 }` → standard `error` field phrases. |
|
|
79
79
|
| `createAuthMiddleware(options)` / `AuthMiddlewareOptions` | Factory for a Firebase-token auth middleware: reads the token header, verifies, resolves the DB user id, and stashes the result on the context. Omit `resolveUserId` for a token-only (login) guard. |
|
|
80
80
|
| `perfLog(options?)` / `PerfLogOptions` / `AnalyticsEngineDatasetLike` | Middleware that records one per-request latency data point (`t_app`, colo, cold/warm, route, status) and emits it to **Workers Logs** (`console.log`) and/or **Workers Analytics Engine** (`writeDataPoint`). Lets you measure low-traffic Workers without a live `wrangler tail`. |
|
|
81
|
+
| `createMaintenanceMiddleware(options)` / `createMaintenanceWaitHandler(options)` / `isMaintenanceEnabled(env)` / `MAINTENANCE_CODE` / `MAINTENANCE_WAIT_PATH` | Fleet maintenance short-circuit: when enabled (`MAINTENANCE=1`), every non-allowlisted request returns `503` + `{ statusCode, message, code: 'MAINTENANCE' }` **before** container/DB. Pair with `GET /public/maintenance/wait` SSE (`event: ping` / `event: ended`) so clients can auto-dismiss a lock UI. Mount after `cors`, before `containerMiddleware`. |
|
|
81
82
|
| `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createHttpErrorHandler`'s `onUnhandledError`. |
|
|
82
83
|
| `createSentryErrorReporter(sentry)` / `SentryExceptionReporterLike` | Build an `ErrorReporter` that forwards to Sentry with an optional `request_id` tag (no hard `@sentry/cloudflare` dependency). |
|
|
83
84
|
| `DeferExecutor` / `defaultDefer` / `createWaitUntilDefer(ctx)` | Fire-and-forget executor for Workers: `defaultDefer` swallows rejections (tests); `createWaitUntilDefer` registers work via `ctx.waitUntil`. |
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export { createAuthMiddleware } from './middleware/auth.js';
|
|
|
18
18
|
export type { AuthMiddlewareOptions } from './middleware/auth.js';
|
|
19
19
|
export { perfLog } from './middleware/perf-log.js';
|
|
20
20
|
export type { PerfLogOptions, AnalyticsEngineDatasetLike } from './middleware/perf-log.js';
|
|
21
|
+
export { createMaintenanceMiddleware, createMaintenanceWaitHandler, isMaintenanceEnabled, MAINTENANCE_BODY, MAINTENANCE_CODE, MAINTENANCE_WAIT_PATH, } from './middleware/maintenance.js';
|
|
22
|
+
export type { MaintenanceBody, MaintenanceMiddlewareOptions, MaintenanceWaitOptions, } from './middleware/maintenance.js';
|
|
21
23
|
export { createIsolateMemo } from './container/isolate-memo.js';
|
|
22
24
|
export type { IsolateMemo } from './container/isolate-memo.js';
|
|
23
25
|
export { createContainerRuntime } from './container/middleware.js';
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export { createSentryValidate } from './middleware/validation.js';
|
|
|
18
18
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
19
19
|
export { createAuthMiddleware } from './middleware/auth.js';
|
|
20
20
|
export { perfLog } from './middleware/perf-log.js';
|
|
21
|
+
export { createMaintenanceMiddleware, createMaintenanceWaitHandler, isMaintenanceEnabled, MAINTENANCE_BODY, MAINTENANCE_CODE, MAINTENANCE_WAIT_PATH, } from './middleware/maintenance.js';
|
|
21
22
|
export { createIsolateMemo } from './container/isolate-memo.js';
|
|
22
23
|
export { createContainerRuntime } from './container/middleware.js';
|
|
23
24
|
// http
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Context, Env, MiddlewareHandler } from 'hono';
|
|
2
|
+
/** Canonical API error `code` for fleet-wide maintenance short-circuit. */
|
|
3
|
+
export declare const MAINTENANCE_CODE: "MAINTENANCE";
|
|
4
|
+
/** Default allowlisted SSE path that stays open while the rest of the API returns 503. */
|
|
5
|
+
export declare const MAINTENANCE_WAIT_PATH = "/public/maintenance/wait";
|
|
6
|
+
/** JSON body returned for every blocked request during maintenance. */
|
|
7
|
+
export interface MaintenanceBody {
|
|
8
|
+
statusCode: 503;
|
|
9
|
+
message: string;
|
|
10
|
+
code: typeof MAINTENANCE_CODE;
|
|
11
|
+
}
|
|
12
|
+
/** Default 503 body (no phrase `error` field — would collide with `code` shape on the client). */
|
|
13
|
+
export declare const MAINTENANCE_BODY: MaintenanceBody;
|
|
14
|
+
/**
|
|
15
|
+
* True when the Workers binding / wrangler var `MAINTENANCE` is the string `'1'`.
|
|
16
|
+
*
|
|
17
|
+
* @param env - Bindings object that may carry `MAINTENANCE`
|
|
18
|
+
*/
|
|
19
|
+
export declare function isMaintenanceEnabled(env: {
|
|
20
|
+
MAINTENANCE?: string;
|
|
21
|
+
} | null | undefined): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Options for {@link createMaintenanceMiddleware}.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam E - The Hono `Env` of the application.
|
|
26
|
+
*/
|
|
27
|
+
export interface MaintenanceMiddlewareOptions<E extends Env = Env> {
|
|
28
|
+
/**
|
|
29
|
+
* Whether maintenance mode is currently on for this request.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* Typical wiring: `(c) => isMaintenanceEnabled(c.env)`. Injected so tests and non-env sources
|
|
33
|
+
* (future KV) can supply their own predicate without forking the middleware.
|
|
34
|
+
*/
|
|
35
|
+
isEnabled: (c: Context<E>) => boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Paths that stay reachable during maintenance (default: {@link MAINTENANCE_WAIT_PATH}).
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* Compared against `pathname` with and without a trailing slash. {@link MAINTENANCE_WAIT_PATH}
|
|
41
|
+
* is handled inside this middleware (SSE) so it never reaches container/DB. Other allowlisted
|
|
42
|
+
* paths call `next()`.
|
|
43
|
+
*/
|
|
44
|
+
allowPaths?: readonly string[];
|
|
45
|
+
/** Override the default {@link MAINTENANCE_BODY.message}. */
|
|
46
|
+
message?: string;
|
|
47
|
+
/** Optional `Retry-After` header (seconds). */
|
|
48
|
+
retryAfterSeconds?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Ping interval for the in-middleware wait SSE (ms). Defaults to `5_000`.
|
|
51
|
+
*
|
|
52
|
+
* @see {@link MaintenanceWaitOptions.pingIntervalMs}
|
|
53
|
+
*/
|
|
54
|
+
pingIntervalMs?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Options for {@link createMaintenanceWaitHandler}.
|
|
58
|
+
*
|
|
59
|
+
* @typeParam E - The Hono `Env` of the application.
|
|
60
|
+
*/
|
|
61
|
+
export interface MaintenanceWaitOptions<E extends Env = Env> {
|
|
62
|
+
/**
|
|
63
|
+
* Whether maintenance mode is still on. Re-evaluated on each ping tick.
|
|
64
|
+
*
|
|
65
|
+
* @remarks
|
|
66
|
+
* Wrangler `vars` do not change inside a long-lived isolate; after a deploy that clears
|
|
67
|
+
* `MAINTENANCE`, new connections (and clients that reconnect) see `false` and get `ended`.
|
|
68
|
+
*/
|
|
69
|
+
isEnabled: (c: Context<E>) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Interval between SSE `ping` events and `isEnabled` re-checks (ms). Defaults to `5_000`.
|
|
72
|
+
*/
|
|
73
|
+
pingIntervalMs?: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Short-circuit middleware: when enabled, every non-allowlisted request returns
|
|
77
|
+
* `503` + `{ statusCode, message, code: 'MAINTENANCE' }` without running downstream
|
|
78
|
+
* (so container / Hyperdrive / secrets stay cold).
|
|
79
|
+
*
|
|
80
|
+
* @remarks
|
|
81
|
+
* Mount **after** `cors` / `finalizeResponse` and **before** `containerMiddleware`.
|
|
82
|
+
* {@link MAINTENANCE_WAIT_PATH} is served **inside this middleware** (both when
|
|
83
|
+
* maintenance is on and off) so the wait SSE never reaches container / DB. Other
|
|
84
|
+
* `allowPaths` still call `next()`.
|
|
85
|
+
*
|
|
86
|
+
* @typeParam E - The Hono `Env` of the application.
|
|
87
|
+
* @param options - Enable predicate, allowlist, and optional body/header overrides.
|
|
88
|
+
* @returns A {@link MiddlewareHandler} that either returns 503 / SSE or calls `next()`.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* app.use('*', createMaintenanceMiddleware({
|
|
93
|
+
* isEnabled: (c) => isMaintenanceEnabled(c.env),
|
|
94
|
+
* }));
|
|
95
|
+
* // Optional: also register createMaintenanceWaitHandler as a route — redundant when
|
|
96
|
+
* // this middleware is mounted, because MAINTENANCE_WAIT_PATH is handled here.
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare function createMaintenanceMiddleware<E extends Env = Env>(options: MaintenanceMiddlewareOptions<E>): MiddlewareHandler<E>;
|
|
100
|
+
/**
|
|
101
|
+
* SSE handler for {@link MAINTENANCE_WAIT_PATH}.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* - Already off → emit `event: ended` once and close.
|
|
105
|
+
* - Still on → emit `event: ping` on an interval; when `isEnabled` becomes false, emit
|
|
106
|
+
* `event: ended` and close. Clients should close the EventSource on `ended` and dismiss UI.
|
|
107
|
+
*
|
|
108
|
+
* @typeParam E - The Hono `Env` of the application.
|
|
109
|
+
* @param options - Enable predicate and ping interval.
|
|
110
|
+
* @returns A handler `(c) => Response` suitable for `app.get(MAINTENANCE_WAIT_PATH, …)`
|
|
111
|
+
* or for embedding inside {@link createMaintenanceMiddleware}.
|
|
112
|
+
*/
|
|
113
|
+
export declare function createMaintenanceWaitHandler<E extends Env = Env>(options: MaintenanceWaitOptions<E>): (c: Context<E>) => Response;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/** Canonical API error `code` for fleet-wide maintenance short-circuit. */
|
|
2
|
+
export const MAINTENANCE_CODE = 'MAINTENANCE';
|
|
3
|
+
/** Default allowlisted SSE path that stays open while the rest of the API returns 503. */
|
|
4
|
+
export const MAINTENANCE_WAIT_PATH = '/public/maintenance/wait';
|
|
5
|
+
/** Default 503 body (no phrase `error` field — would collide with `code` shape on the client). */
|
|
6
|
+
export const MAINTENANCE_BODY = {
|
|
7
|
+
statusCode: 503,
|
|
8
|
+
message: 'Service temporarily unavailable',
|
|
9
|
+
code: MAINTENANCE_CODE,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* True when the Workers binding / wrangler var `MAINTENANCE` is the string `'1'`.
|
|
13
|
+
*
|
|
14
|
+
* @param env - Bindings object that may carry `MAINTENANCE`
|
|
15
|
+
*/
|
|
16
|
+
export function isMaintenanceEnabled(env) {
|
|
17
|
+
return env?.MAINTENANCE === '1';
|
|
18
|
+
}
|
|
19
|
+
const SSE_HEADERS = {
|
|
20
|
+
'Content-Type': 'text/event-stream',
|
|
21
|
+
'Cache-Control': 'no-cache',
|
|
22
|
+
Connection: 'keep-alive',
|
|
23
|
+
'X-Accel-Buffering': 'no',
|
|
24
|
+
};
|
|
25
|
+
function normalizePath(pathname) {
|
|
26
|
+
if (pathname.length > 1 && pathname.endsWith('/')) {
|
|
27
|
+
return pathname.slice(0, -1);
|
|
28
|
+
}
|
|
29
|
+
return pathname;
|
|
30
|
+
}
|
|
31
|
+
function isAllowlisted(pathname, allowPaths) {
|
|
32
|
+
const normalized = normalizePath(pathname);
|
|
33
|
+
return allowPaths.has(normalized) || allowPaths.has(pathname);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Short-circuit middleware: when enabled, every non-allowlisted request returns
|
|
37
|
+
* `503` + `{ statusCode, message, code: 'MAINTENANCE' }` without running downstream
|
|
38
|
+
* (so container / Hyperdrive / secrets stay cold).
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* Mount **after** `cors` / `finalizeResponse` and **before** `containerMiddleware`.
|
|
42
|
+
* {@link MAINTENANCE_WAIT_PATH} is served **inside this middleware** (both when
|
|
43
|
+
* maintenance is on and off) so the wait SSE never reaches container / DB. Other
|
|
44
|
+
* `allowPaths` still call `next()`.
|
|
45
|
+
*
|
|
46
|
+
* @typeParam E - The Hono `Env` of the application.
|
|
47
|
+
* @param options - Enable predicate, allowlist, and optional body/header overrides.
|
|
48
|
+
* @returns A {@link MiddlewareHandler} that either returns 503 / SSE or calls `next()`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* app.use('*', createMaintenanceMiddleware({
|
|
53
|
+
* isEnabled: (c) => isMaintenanceEnabled(c.env),
|
|
54
|
+
* }));
|
|
55
|
+
* // Optional: also register createMaintenanceWaitHandler as a route — redundant when
|
|
56
|
+
* // this middleware is mounted, because MAINTENANCE_WAIT_PATH is handled here.
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function createMaintenanceMiddleware(options) {
|
|
60
|
+
const allowPaths = new Set((options.allowPaths ?? [MAINTENANCE_WAIT_PATH]).map((p) => normalizePath(p)));
|
|
61
|
+
const message = options.message ?? MAINTENANCE_BODY.message;
|
|
62
|
+
const waitHandler = createMaintenanceWaitHandler({
|
|
63
|
+
isEnabled: options.isEnabled,
|
|
64
|
+
pingIntervalMs: options.pingIntervalMs,
|
|
65
|
+
});
|
|
66
|
+
const waitPath = normalizePath(MAINTENANCE_WAIT_PATH);
|
|
67
|
+
return async (c, next) => {
|
|
68
|
+
const pathname = normalizePath(new URL(c.req.url).pathname);
|
|
69
|
+
// Wait SSE must never hit container/DB — handle it here whether maintenance is on or off.
|
|
70
|
+
if (pathname === waitPath) {
|
|
71
|
+
return waitHandler(c);
|
|
72
|
+
}
|
|
73
|
+
if (!options.isEnabled(c)) {
|
|
74
|
+
return next();
|
|
75
|
+
}
|
|
76
|
+
if (isAllowlisted(pathname, allowPaths)) {
|
|
77
|
+
return next();
|
|
78
|
+
}
|
|
79
|
+
if (options.retryAfterSeconds != null) {
|
|
80
|
+
c.header('Retry-After', String(options.retryAfterSeconds));
|
|
81
|
+
}
|
|
82
|
+
const body = {
|
|
83
|
+
statusCode: 503,
|
|
84
|
+
message,
|
|
85
|
+
code: MAINTENANCE_CODE,
|
|
86
|
+
};
|
|
87
|
+
return c.json(body, 503);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* SSE handler for {@link MAINTENANCE_WAIT_PATH}.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* - Already off → emit `event: ended` once and close.
|
|
95
|
+
* - Still on → emit `event: ping` on an interval; when `isEnabled` becomes false, emit
|
|
96
|
+
* `event: ended` and close. Clients should close the EventSource on `ended` and dismiss UI.
|
|
97
|
+
*
|
|
98
|
+
* @typeParam E - The Hono `Env` of the application.
|
|
99
|
+
* @param options - Enable predicate and ping interval.
|
|
100
|
+
* @returns A handler `(c) => Response` suitable for `app.get(MAINTENANCE_WAIT_PATH, …)`
|
|
101
|
+
* or for embedding inside {@link createMaintenanceMiddleware}.
|
|
102
|
+
*/
|
|
103
|
+
export function createMaintenanceWaitHandler(options) {
|
|
104
|
+
const pingIntervalMs = options.pingIntervalMs ?? 5_000;
|
|
105
|
+
const encoder = new TextEncoder();
|
|
106
|
+
return (c) => {
|
|
107
|
+
const clientSignal = c.req.raw.signal;
|
|
108
|
+
if (!options.isEnabled(c)) {
|
|
109
|
+
const stream = new ReadableStream({
|
|
110
|
+
start(controller) {
|
|
111
|
+
controller.enqueue(encoder.encode('event: ended\ndata: ended\n\n'));
|
|
112
|
+
controller.close();
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
return new Response(stream, { headers: SSE_HEADERS });
|
|
116
|
+
}
|
|
117
|
+
let heartbeat;
|
|
118
|
+
const stream = new ReadableStream({
|
|
119
|
+
start(controller) {
|
|
120
|
+
let closed = false;
|
|
121
|
+
const close = () => {
|
|
122
|
+
if (closed) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
closed = true;
|
|
126
|
+
if (heartbeat) {
|
|
127
|
+
clearInterval(heartbeat);
|
|
128
|
+
heartbeat = undefined;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
controller.close();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* already closed */
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const enqueue = (chunk) => {
|
|
138
|
+
try {
|
|
139
|
+
controller.enqueue(encoder.encode(chunk));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
close();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const end = () => {
|
|
146
|
+
enqueue('event: ended\ndata: ended\n\n');
|
|
147
|
+
close();
|
|
148
|
+
};
|
|
149
|
+
if (clientSignal.aborted) {
|
|
150
|
+
close();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
clientSignal.addEventListener('abort', close, { once: true });
|
|
154
|
+
heartbeat = setInterval(() => {
|
|
155
|
+
if (!options.isEnabled(c)) {
|
|
156
|
+
end();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
enqueue('event: ping\ndata: ping\n\n');
|
|
160
|
+
}, pingIntervalMs);
|
|
161
|
+
},
|
|
162
|
+
cancel() {
|
|
163
|
+
if (heartbeat) {
|
|
164
|
+
clearInterval(heartbeat);
|
|
165
|
+
heartbeat = undefined;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
return new Response(stream, { headers: SSE_HEADERS });
|
|
170
|
+
};
|
|
171
|
+
}
|