@zerotal/core 1.13.1 → 1.13.3

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/CHANGELOG.md CHANGED
@@ -8,6 +8,70 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.13.3] — 2026-08-31
12
+
13
+ ### Added
14
+
15
+ - **`Heartbeat` — which background processes are actually running.** An app could say what
16
+ it _registered_ and nothing could say whether any of it ever _ran_.
17
+
18
+ The failure this exists for was reported from production: an app shipped with no worker
19
+ process, and every scheduled task silently did not execute for weeks. No hold was
20
+ released, no reminder was sent, and nothing logged, because from the web process's point
21
+ of view nothing was wrong. They found it by going looking.
22
+
23
+ `@zerotal/core/heartbeat` — `beat()`, `start()`, `lastSeen()`, and `workerLivenessCheck()`
24
+ for building the doctor check.
25
+
26
+ The beat lives in the **cache**, because the reader is a different process from the
27
+ writer and often a different machine, and the app has already chosen where its shared
28
+ state goes. `sqlite` (the default) is shared across processes on one box, `redis` across
29
+ machines, `memory` with nobody — which is why `lastSeen()` returns a distinct
30
+ `"unknown"` rather than `"never"`. A check that reported a missing worker on every app
31
+ using the memory driver would be wrong more often than right, and a check that is
32
+ usually wrong is one people learn to skip.
33
+
34
+ ### Added
35
+
36
+ - **A site gate — maintenance, and private preview.** From a proposal by the Trekly team,
37
+ who were running a hand-edited `basic_auth` block in a reverse proxy, deliberately kept
38
+ out of version control so it could not be deployed and forgotten into a live shop.
39
+
40
+ Two states that look alike and are not. **Maintenance** means the site is down —
41
+ everyone refused, staff included, because the usual reason a site is down is that its
42
+ database is being changed underneath it. **Private preview** means the site is up and
43
+ working, for the people invited to it, for weeks.
44
+
45
+ `zt down`, `zt preview`, `zt up`, `zt gate:status`, and a `Gate` API at
46
+ `@zerotal/core/gate` so an app can build its own switch. See
47
+ [the guide](https://zerotal.dev/docs/site-gate).
48
+
49
+ The details that are the reason this belongs in the framework rather than in each app:
50
+
51
+ - **Maintenance is always `503` with `Retry-After`, and is not configurable.** A
52
+ maintenance page at `200` tells a search engine the apology is your homepage.
53
+ - **A preview token is stripped from the URL on first use**, via a redirect to a cookie.
54
+ Left in the address bar it travels into `Referer`, analytics and screenshots.
55
+ - **Webhook paths must be declared** in `gate.allow`. A payment provider posting a
56
+ settlement into a maintenance window otherwise gets a 503, and that is a retry, a
57
+ dropped callback, or a payment your books never learn about.
58
+ - **The state is a file, and the token is stored hashed.** A flag in the database is
59
+ unreadable exactly when the database is what you are working on; a token in a file is
60
+ a credential in something every backup copies.
61
+ - **It covers `Router.raw()` routes.** Found while testing: this framework's own docs
62
+ site serves every `/docs/*` page from a raw route, so an early build gated the front
63
+ page — which is what a person checks — and left all the content public.
64
+ - **`zt doctor` reports a gate left on**, and a preview whose `until` has passed.
65
+ - **The state file is gitignored** by the scaffold, which is the whole point.
66
+
67
+ ### Fixed
68
+
69
+ - **`secureHeaders: false` no longer empties the kernel.** It set the kernel middleware to
70
+ `[]`, which was the same thing as removing the headers until the gate joined that layer —
71
+ at which point opting out of security headers would silently have taken the site gate
72
+ with it. One feature's opt-out disabling another's is the failure the gate is otherwise
73
+ about.
74
+
11
75
  ## [1.13.0] — 2026-08-31
12
76
 
13
77
  ### Changed — **BREAKING**
package/api-surface.md CHANGED
@@ -1448,6 +1448,62 @@ class DoctorCommand = {
1448
1448
  write: (msg: string) => void
1449
1449
  }
1450
1450
 
1451
+ class DownCommand = {
1452
+ new (): DownCommand
1453
+ static args: never[]
1454
+ static commandName: string
1455
+ static description: string
1456
+ static flags: ({ name: string; type: 'number'; description: string; default: number;} | { name: string; type: 'string'; description: string; default?: never;})[]
1457
+ static needsApp: boolean
1458
+ _readLine: () => Promise<string>
1459
+ _writer: OutputWriter
1460
+ app: unknown
1461
+ args: Record<string, string>
1462
+ ask: (question: string, defaultValue?: string) => Promise<string>
1463
+ choice: (question: string, options: string[]) => Promise<string>
1464
+ confirm: (question: string, defaultValue?: boolean) => Promise<boolean>
1465
+ dim: (msg: string) => void
1466
+ error: (msg: string) => void
1467
+ flags: Record<string, string | number | boolean>
1468
+ info: (msg: string) => void
1469
+ line: (msg: string) => void
1470
+ newLine: () => void
1471
+ run: () => Promise<void>
1472
+ secret: (question: string) => Promise<string>
1473
+ section: (title: string) => void
1474
+ table: (rows: [string, string][], indent?: number) => void
1475
+ warn: (msg: string) => void
1476
+ write: (msg: string) => void
1477
+ }
1478
+
1479
+ class GateStatusCommand = {
1480
+ new (): GateStatusCommand
1481
+ static args: never[]
1482
+ static commandName: string
1483
+ static description: string
1484
+ static flags: { name: string; type: 'boolean'; description: string; default: boolean;}[]
1485
+ static needsApp: boolean
1486
+ _readLine: () => Promise<string>
1487
+ _writer: OutputWriter
1488
+ app: unknown
1489
+ args: Record<string, string>
1490
+ ask: (question: string, defaultValue?: string) => Promise<string>
1491
+ choice: (question: string, options: string[]) => Promise<string>
1492
+ confirm: (question: string, defaultValue?: boolean) => Promise<boolean>
1493
+ dim: (msg: string) => void
1494
+ error: (msg: string) => void
1495
+ flags: Record<string, string | number | boolean>
1496
+ info: (msg: string) => void
1497
+ line: (msg: string) => void
1498
+ newLine: () => void
1499
+ run: () => Promise<void>
1500
+ secret: (question: string) => Promise<string>
1501
+ section: (title: string) => void
1502
+ table: (rows: [string, string][], indent?: number) => void
1503
+ warn: (msg: string) => void
1504
+ write: (msg: string) => void
1505
+ }
1506
+
1451
1507
  class KeyGenerateCommand = {
1452
1508
  new (): KeyGenerateCommand
1453
1509
  static args: never[]
@@ -1896,6 +1952,34 @@ class MakeTestCommand = {
1896
1952
  write: (msg: string) => void
1897
1953
  }
1898
1954
 
1955
+ class PreviewCommand = {
1956
+ new (): PreviewCommand
1957
+ static args: never[]
1958
+ static commandName: string
1959
+ static description: string
1960
+ static flags: { name: string; type: 'string'; description: string;}[]
1961
+ static needsApp: boolean
1962
+ _readLine: () => Promise<string>
1963
+ _writer: OutputWriter
1964
+ app: unknown
1965
+ args: Record<string, string>
1966
+ ask: (question: string, defaultValue?: string) => Promise<string>
1967
+ choice: (question: string, options: string[]) => Promise<string>
1968
+ confirm: (question: string, defaultValue?: boolean) => Promise<boolean>
1969
+ dim: (msg: string) => void
1970
+ error: (msg: string) => void
1971
+ flags: Record<string, string | number | boolean>
1972
+ info: (msg: string) => void
1973
+ line: (msg: string) => void
1974
+ newLine: () => void
1975
+ run: () => Promise<void>
1976
+ secret: (question: string) => Promise<string>
1977
+ section: (title: string) => void
1978
+ table: (rows: [string, string][], indent?: number) => void
1979
+ warn: (msg: string) => void
1980
+ write: (msg: string) => void
1981
+ }
1982
+
1899
1983
  class ReloadCommand = {
1900
1984
  new (): ReloadCommand
1901
1985
  static args: never[]
@@ -2123,6 +2207,34 @@ class TestCommand = {
2123
2207
  write: (msg: string) => void
2124
2208
  }
2125
2209
 
2210
+ class UpCommand = {
2211
+ new (): UpCommand
2212
+ static args: never[]
2213
+ static commandName: string
2214
+ static description: string
2215
+ static flags: never[]
2216
+ static needsApp: boolean
2217
+ _readLine: () => Promise<string>
2218
+ _writer: OutputWriter
2219
+ app: unknown
2220
+ args: Record<string, string>
2221
+ ask: (question: string, defaultValue?: string) => Promise<string>
2222
+ choice: (question: string, options: string[]) => Promise<string>
2223
+ confirm: (question: string, defaultValue?: boolean) => Promise<boolean>
2224
+ dim: (msg: string) => void
2225
+ error: (msg: string) => void
2226
+ flags: Record<string, string | number | boolean>
2227
+ info: (msg: string) => void
2228
+ line: (msg: string) => void
2229
+ newLine: () => void
2230
+ run: () => Promise<void>
2231
+ secret: (question: string) => Promise<string>
2232
+ section: (title: string) => void
2233
+ table: (rows: [string, string][], indent?: number) => void
2234
+ warn: (msg: string) => void
2235
+ write: (msg: string) => void
2236
+ }
2237
+
2126
2238
  class UpgradeCommand = {
2127
2239
  new (): UpgradeCommand
2128
2240
  static args: ArgDef[]
@@ -2625,6 +2737,59 @@ interface ArtisanResult = {
2625
2737
  output: string
2626
2738
  }
2627
2739
 
2740
+ ## ./gate `(./src/gate/index.ts)`
2741
+
2742
+ class GateMiddleware = {
2743
+ new (): GateMiddleware
2744
+ static with: <T extends new (...args: any[]) => BaseMiddleware<any>, Opts = T extends new (...args: any[]) => BaseMiddleware<infer U> ? U : object>(this: T, options: DeepPartial<NoInfer<Opts>>) => new () => InstanceType<T>
2745
+ afterResponse?: (ctx: HttpContext) => Promise<void>
2746
+ handle: (ctx: HttpContext, next: NextFn) => Promise<Response | void>
2747
+ onError?: (ctx: HttpContext, error: Error) => Promise<void>
2748
+ }
2749
+
2750
+ const Gate = { maintenance(options?: MaintenanceOptions): Promise<void>; preview(options: PreviewOptions): Promise<void>; open(): Promise<void>; status(): GateStatus;}
2751
+
2752
+ const GATE_COOKIE = 'zerotal_preview'
2753
+
2754
+ const GATE_FILE = string
2755
+
2756
+ const GATE_QUERY = 'preview'
2757
+
2758
+ function gateExpired = (state: GateState, now?: Date) => boolean
2759
+
2760
+ function readGate = (root?: string) => GateState | null
2761
+
2762
+ interface GateState = {
2763
+ by?: string | undefined
2764
+ mode: GateMode
2765
+ retryAfter?: number | undefined
2766
+ since: string
2767
+ tokenHash?: string | undefined
2768
+ until?: string | undefined
2769
+ }
2770
+
2771
+ interface GateStatus = {
2772
+ by?: string | undefined
2773
+ expired?: boolean | undefined
2774
+ mode: 'open' | GateMode
2775
+ retryAfter?: number | undefined
2776
+ since?: string | undefined
2777
+ until?: string | undefined
2778
+ }
2779
+
2780
+ interface MaintenanceOptions = {
2781
+ by?: string | undefined
2782
+ retryAfter?: number | undefined
2783
+ }
2784
+
2785
+ interface PreviewOptions = {
2786
+ by?: string | undefined
2787
+ token: string
2788
+ until?: string | undefined
2789
+ }
2790
+
2791
+ type GateMode = 'maintenance' | 'preview'
2792
+
2628
2793
  ## ./health `(./src/health/index.ts)`
2629
2794
 
2630
2795
  const Health = HealthRegistry
@@ -2669,6 +2834,22 @@ type HealthCheckFn = () => Promise<HealthResult | void> | HealthResult | void
2669
2834
 
2670
2835
  type HealthStatus = 'ok' | 'degraded' | 'down'
2671
2836
 
2837
+ ## ./heartbeat `(./src/heartbeat/index.ts)`
2838
+
2839
+ const Heartbeat = { beat(name: string, extra?: Omit<Beat, 'at'>): Promise<void>; start(name: string, extra?: Omit<Beat, 'at'>, everyMs?: number): () => void; lastSeen(name: string): Promise<BeatLookup>;}
2840
+
2841
+ function describeBeat = (lookup: BeatLookup) => string
2842
+
2843
+ function workerLivenessCheck = (options: WorkerLivenessOptions) => DoctorCheck
2844
+
2845
+ interface Beat = {
2846
+ at: string
2847
+ detail?: string | undefined
2848
+ pid?: number | undefined
2849
+ }
2850
+
2851
+ type BeatLookup = { status: 'seen'; beat: Beat; ageSeconds: number } | { status: 'never' } | { status: 'unknown'; reason: string }
2852
+
2672
2853
  ## ./helpers `(./src/helpers/index.ts)`
2673
2854
 
2674
2855
  const Str = { camelCase(value: string): string; snakeCase(value: string): string; slugify(value: string): string; titleCase(value: string): string; pascalCase(value: string): string; capitalize(value: string): string; lcfirst(value: string): string; truncate(value: string, maxLength: number, suffix?: string): string; isAlphanumeric(value: string): boolean; padLeft(value: string, length: number, char?: string): string; kebab(value: string): string; squish(value: string): string; finish(value: string, cap: string): string; start(value: string, prefix: string): string; after(value: string, needle: string): string; before(value: string, needle: string): string; afterLast(value: string, needle: string): string; beforeLast(value: string, needle: string): string; contains(value: string, substring: string): boolean; random(length?: number): string; replaceFirst(value: string, search: string, replace: string): string; replaceLast(value: string, search: string, replace: string): string; reverse(value: string): string; words(value: string, maxWords: number, suffix?: string): string; macro(name: string, fn: (...args: unknown[]) => unknown): void;}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/core",
3
- "version": "1.13.1",
3
+ "version": "1.13.3",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -17,6 +17,8 @@
17
17
  "./commands": "./src/command/builtin/index.ts",
18
18
  "./helpers": "./src/helpers/index.ts",
19
19
  "./runtime": "./src/support/runtime.ts",
20
+ "./gate": "./src/gate/index.ts",
21
+ "./heartbeat": "./src/heartbeat/index.ts",
20
22
  "./shared": "./src/shared/index.ts",
21
23
  "./facades": "./src/facade/facades/index.ts",
22
24
  "./carbon": "./src/datetime/index.ts",
@@ -32,6 +32,7 @@ import {
32
32
  SecureHeadersMiddleware,
33
33
  staticSecurityHeaders,
34
34
  } from "../middleware/SecureHeadersMiddleware.ts";
35
+ import { GateMiddleware } from "../gate/GateMiddleware.ts";
35
36
  import { isAllowedOrigin, allowedOriginsFrom } from "../http/originGuard.ts";
36
37
  import { rescueSync } from "../helpers/index.ts";
37
38
  import {
@@ -525,7 +526,15 @@ export class Application {
525
526
 
526
527
  const app = new Application();
527
528
  app._env = resolvedEnv;
528
- if (options.secureHeaders === false) app._kernelMiddleware = [];
529
+ // Remove only the headers, not the whole kernel. Emptying it used to be the
530
+ // same thing; once the gate joined the kernel it stopped being, and
531
+ // `secureHeaders: false` would have silently taken a site's maintenance mode
532
+ // with it — an opt-out for one feature disabling an unrelated one.
533
+ if (options.secureHeaders === false) {
534
+ app._kernelMiddleware = app._kernelMiddleware.filter(
535
+ (pipe) => pipe !== SecureHeadersMiddleware,
536
+ );
537
+ }
529
538
  if (options.providers) for (const provider of options.providers) app._addProvider(provider);
530
539
  if (options.config) {
531
540
  app._configMap =
@@ -899,7 +908,11 @@ export class Application {
899
908
  * documenting it and hoping — shipped every scaffold with no clickjacking protection and
900
909
  * no MIME-sniffing protection. Cleared by `Application.create({ secureHeaders: false })`.
901
910
  */
902
- private _kernelMiddleware: PipeClass[] = [SecureHeadersMiddleware];
911
+ // GateMiddleware first, and before SecureHeaders: a closed site should answer
912
+ // from the gate without anything else running, including the session and
913
+ // anything that reaches the database — which in maintenance mode may be the
914
+ // thing that is unavailable.
915
+ private _kernelMiddleware: PipeClass[] = [GateMiddleware, SecureHeadersMiddleware];
903
916
 
904
917
  /** Resolved middleware pipeline: kernel → provider auto → explicit .use() */
905
918
  private get _pipeline(): PipeClass[] {
@@ -344,6 +344,10 @@ export class CommandRunner {
344
344
  ReloadCommand,
345
345
  StatusCommand,
346
346
  VersionCommand,
347
+ DownCommand,
348
+ PreviewCommand,
349
+ UpCommand,
350
+ GateStatusCommand,
347
351
  MakeControllerCommand,
348
352
  MakeMiddlewareCommand,
349
353
  MakeCommandCommand,
@@ -386,6 +390,15 @@ export class CommandRunner {
386
390
  // "which version is this" must not depend on which environment is asking.
387
391
  this.register(VersionCommand);
388
392
 
393
+ // The gate is changed at the moments things are going wrong — before a
394
+ // migration, during an incident — so these are registered in every
395
+ // environment and need no application. A maintenance command that only
396
+ // works when the app boots is one you cannot reach when you need it.
397
+ this.register(DownCommand);
398
+ this.register(PreviewCommand);
399
+ this.register(UpCommand);
400
+ this.register(GateStatusCommand);
401
+
389
402
  // route:list and doctor are inspection commands — always available regardless of mode.
390
403
  this.register(RouteListCommand);
391
404
  this.register(DoctorCommand);
@@ -0,0 +1,171 @@
1
+ import { Command } from "../Command.ts";
2
+ import { Gate } from "../../gate/Gate.ts";
3
+
4
+ /**
5
+ * `bun zt down` — take the site down for maintenance.
6
+ *
7
+ * Every request is answered `503` with `Retry-After`, staff included, because
8
+ * the usual reason a site is down is that its database is being changed
9
+ * underneath it and letting one person in is letting them into that.
10
+ *
11
+ * @example
12
+ * ```bash
13
+ * bun zt down --retry=120
14
+ * bun zt up
15
+ * ```
16
+ *
17
+ * @category Serving
18
+ */
19
+ export class DownCommand extends Command {
20
+ static override commandName = "down";
21
+ static override description = "Take the site down for maintenance (503 + Retry-After)";
22
+ static override needsApp = false;
23
+ static override args = [];
24
+ static override flags = [
25
+ {
26
+ name: "retry",
27
+ type: "number" as const,
28
+ description: "Seconds to advertise in Retry-After",
29
+ default: 60,
30
+ },
31
+ { name: "by", type: "string" as const, description: "Who is taking it down" },
32
+ ];
33
+
34
+ async run(): Promise<void> {
35
+ const retryAfter = this.flags["retry"] as number;
36
+ const by = this.flags["by"] as string | undefined;
37
+
38
+ await Gate.maintenance({ retryAfter, ...(by ? { by } : {}) });
39
+
40
+ this.info(`Site is down. Answering 503 with Retry-After: ${retryAfter}.`);
41
+ this.dim("Health checks and static assets stay reachable. `bun zt up` to restore.");
42
+ this.dim("Webhook paths do NOT — declare them in config/gate.ts `allow` first.");
43
+ }
44
+ }
45
+
46
+ /**
47
+ * `bun zt preview` — put the site behind a private preview.
48
+ *
49
+ * The site works normally for anyone holding the token; the public gets a
50
+ * holding page. Unlike `down`, this is not an outage — invited people can
51
+ * transact.
52
+ *
53
+ * @example
54
+ * ```bash
55
+ * bun zt preview # generates a token and prints the link
56
+ * bun zt preview --until=2026-09-30
57
+ * ```
58
+ *
59
+ * @category Serving
60
+ */
61
+ export class PreviewCommand extends Command {
62
+ static override commandName = "preview";
63
+ static override description = "Put the site behind a private preview link";
64
+ static override needsApp = false;
65
+ static override args = [];
66
+ static override flags = [
67
+ {
68
+ name: "token",
69
+ type: "string" as const,
70
+ description: "Token to use. Generated when omitted; pass a new one to rotate",
71
+ },
72
+ {
73
+ name: "until",
74
+ type: "string" as const,
75
+ description: "ISO date the preview lifts, e.g. 2026-09-30",
76
+ },
77
+ { name: "by", type: "string" as const, description: "Who is raising it" },
78
+ { name: "url", type: "string" as const, description: "Base URL to print the link against" },
79
+ ];
80
+
81
+ async run(): Promise<void> {
82
+ // Generated rather than asked for by default. A token typed by a person is
83
+ // the one thing standing between the public and an unlaunched site, and
84
+ // nothing rate-limits guesses at it.
85
+ const token =
86
+ (this.flags["token"] as string | undefined) ?? crypto.randomUUID().replace(/-/g, "");
87
+ const until = this.flags["until"] as string | undefined;
88
+ const by = this.flags["by"] as string | undefined;
89
+ const base = (this.flags["url"] as string | undefined) ?? "https://your-site";
90
+
91
+ const rotating = Gate.status().mode === "preview";
92
+ await Gate.preview({ token, ...(until ? { until } : {}), ...(by ? { by } : {}) });
93
+
94
+ this.info("Site is in private preview.");
95
+ this.newLine();
96
+ this.line(` ${base}/?preview=${token}`);
97
+ this.newLine();
98
+ this.dim("Keep that link — the token is stored as a hash and cannot be read back.");
99
+ if (rotating) this.warn("Rotated: every previously issued preview cookie has stopped working.");
100
+ if (until) this.dim(`Lifts itself after ${until}.`);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * `bun zt up` — open the site to the public again.
106
+ *
107
+ * @category Serving
108
+ */
109
+ export class UpCommand extends Command {
110
+ static override commandName = "up";
111
+ static override description = "Open the site — clears maintenance or preview";
112
+ static override needsApp = false;
113
+ static override args = [];
114
+ static override flags = [];
115
+
116
+ async run(): Promise<void> {
117
+ const before = Gate.status();
118
+ await Gate.open();
119
+
120
+ if (before.mode === "open") {
121
+ this.info("Site was already open. Nothing to do.");
122
+ return;
123
+ }
124
+ this.info(`Site is open. Was in ${before.mode} since ${before.since ?? "unknown"}.`);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * `bun zt gate:status` — what the gate is doing.
130
+ *
131
+ * @category Diagnostics
132
+ */
133
+ export class GateStatusCommand extends Command {
134
+ static override commandName = "gate:status";
135
+ static override description = "Show whether the site is open, in maintenance, or in preview";
136
+ static override needsApp = false;
137
+ static override args = [];
138
+ static override flags = [
139
+ { name: "json", type: "boolean" as const, description: "Print as JSON", default: false },
140
+ ];
141
+
142
+ async run(): Promise<void> {
143
+ const status = Gate.status();
144
+
145
+ if (this.flags["json"] === true) {
146
+ // `write`, not `line`: the other helpers colour, and a colour code is not
147
+ // JSON.
148
+ this.write(`${JSON.stringify(status, null, 2)}\n`);
149
+ return;
150
+ }
151
+
152
+ if (status.mode === "open") {
153
+ this.info("Site is open.");
154
+ return;
155
+ }
156
+
157
+ this.table([
158
+ ["Mode", status.mode],
159
+ ["Since", status.since ?? "—"],
160
+ ["Until", status.until ?? "—"],
161
+ ["By", status.by ?? "—"],
162
+ ...(status.mode === "maintenance"
163
+ ? ([["Retry-After", String(status.retryAfter ?? 60)]] as [string, string][])
164
+ : []),
165
+ ]);
166
+
167
+ if (status.expired) {
168
+ this.warn("This preview's window has passed — it is no longer gating anything.");
169
+ }
170
+ }
171
+ }
@@ -34,6 +34,7 @@ export { KeyGenerateCommand } from "./KeyGenerateCommand.ts";
34
34
  export { ReloadCommand } from "./ReloadCommand.ts";
35
35
  export { StatusCommand } from "./StatusCommand.ts";
36
36
  export { VersionCommand } from "./VersionCommand.ts";
37
+ export { DownCommand, PreviewCommand, UpCommand, GateStatusCommand } from "./GateCommands.ts";
37
38
  export { MakeControllerCommand } from "./MakeControllerCommand.ts";
38
39
  export { MakeMiddlewareCommand } from "./MakeMiddlewareCommand.ts";
39
40
  export { MakeCommandCommand } from "./MakeCommandCommand.ts";
@@ -12,6 +12,7 @@
12
12
  * scheduler's static-config check is the model); the built-ins cover core.
13
13
  */
14
14
  import { readdirSync } from "node:fs";
15
+ import { siteGateCheck } from "../gate/doctor.ts";
15
16
  import type { Application } from "../application/Application.ts";
16
17
  import { appKeyStrengthWarning } from "../support/appKey.ts";
17
18
  import { unroutedRoutesWarning } from "../support/unroutedRoutes.ts";
@@ -409,6 +410,7 @@ const throttleIdentityCheck: DoctorCheck = {
409
410
  */
410
411
  export const builtinDoctorChecks: DoctorCheck[] = [
411
412
  appKeyCheck,
413
+ siteGateCheck,
412
414
  allowedOriginsCheck,
413
415
  corsWildcardCheck,
414
416
  secureHeadersCheck,