@zerotal/core 1.6.3 → 1.7.2
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 +176 -0
- package/api-surface.md +3609 -0
- package/package.json +3 -1
- package/src/application/Application.ts +174 -11
- package/src/command/builtin/DoctorCommand.ts +53 -6
- package/src/command/builtin/RouteTypesCommand.ts +1 -0
- package/src/dev/DevDeck.ts +144 -20
- package/src/dev/DevOrchestrator.ts +1 -1
- package/src/doctor/HeaderProbe.ts +164 -0
- package/src/events/Emitter.ts +24 -0
- package/src/events/FrameworkEvents.ts +42 -0
- package/src/helpers/index.ts +43 -28
- package/src/index.ts +2 -0
- package/src/middleware/BaseMiddleware.ts +12 -1
- package/src/middleware/SecureHeadersMiddleware.ts +54 -23
- package/src/provider/StorageProvider.ts +4 -1
- package/src/router/RouteHandler.ts +5 -0
- package/src/router/Router.ts +62 -3
- package/src/router/routeTypes.ts +52 -11
- package/src/router/routes.ts +115 -0
- package/src/security/index.ts +6 -0
- package/src/security/redactGraph.ts +107 -0
- package/src/support/deepMerge.ts +42 -2
- package/src/support/env.ts +48 -8
package/src/support/deepMerge.ts
CHANGED
|
@@ -44,6 +44,46 @@
|
|
|
44
44
|
/** Keys that must never be copied across — they can pollute `Object.prototype`. */
|
|
45
45
|
const _UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Values {@link deepMerge} treats as atomic: replaced wholesale, never recursed
|
|
49
|
+
* into. Kept in step with `_isPlainObject` below — a type that recursed into an
|
|
50
|
+
* array would ask callers for `{ 0?: string }` where the function wants
|
|
51
|
+
* `string[]`.
|
|
52
|
+
*/
|
|
53
|
+
type _Atomic =
|
|
54
|
+
| readonly unknown[]
|
|
55
|
+
| Date
|
|
56
|
+
| RegExp
|
|
57
|
+
| Map<unknown, unknown>
|
|
58
|
+
| Set<unknown>
|
|
59
|
+
| ((...args: never[]) => unknown);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Every key optional, all the way down — the shape {@link deepMerge} actually
|
|
63
|
+
* accepts.
|
|
64
|
+
*
|
|
65
|
+
* `Partial<T>` only makes the *top* level optional, so
|
|
66
|
+
* `{ drivers: { anthropic: { apiKey } } }` — the single most common thing anyone
|
|
67
|
+
* writes in a config file — was a type error against a shape whose `anthropic`
|
|
68
|
+
* has other keys, even though the merge handles it perfectly. `@zerotal/ai` hit
|
|
69
|
+
* this first and defined its own copy; this is that type, promoted so nothing
|
|
70
|
+
* has to define it again.
|
|
71
|
+
*
|
|
72
|
+
* The explicit `| undefined` is deliberate under `exactOptionalPropertyTypes`:
|
|
73
|
+
* `deepMerge` documents that an `undefined` override is skipped rather than
|
|
74
|
+
* blanking a default, so passing one explicitly has a defined meaning and
|
|
75
|
+
* should type-check.
|
|
76
|
+
*/
|
|
77
|
+
export type DeepPartial<T> = {
|
|
78
|
+
[K in keyof T]?:
|
|
79
|
+
| (NonNullable<T[K]> extends _Atomic
|
|
80
|
+
? T[K]
|
|
81
|
+
: NonNullable<T[K]> extends object
|
|
82
|
+
? DeepPartial<NonNullable<T[K]>>
|
|
83
|
+
: T[K])
|
|
84
|
+
| undefined;
|
|
85
|
+
};
|
|
86
|
+
|
|
47
87
|
/**
|
|
48
88
|
* A *plain* object: a `{}`-style record whose prototype is `Object.prototype` or
|
|
49
89
|
* `null`. Class instances, arrays, Dates, Maps, etc. are intentionally excluded so
|
|
@@ -96,7 +136,7 @@ function _clone<T>(value: T): T {
|
|
|
96
136
|
* deepMerge({ tags: ["a", "b"] }, { tags: ["c"] });
|
|
97
137
|
* // → { tags: ["c"] }
|
|
98
138
|
*/
|
|
99
|
-
export function deepMerge<T extends object>(base: T, override:
|
|
139
|
+
export function deepMerge<T extends object>(base: T, override: DeepPartial<T>): T {
|
|
100
140
|
const result = _clone(base) as T;
|
|
101
141
|
for (const key in override) {
|
|
102
142
|
if (!Object.prototype.hasOwnProperty.call(override, key)) continue;
|
|
@@ -107,7 +147,7 @@ export function deepMerge<T extends object>(base: T, override: Partial<T>): T {
|
|
|
107
147
|
if (_isPlainObject(overrideValue) && _isPlainObject(baseValue)) {
|
|
108
148
|
(result as Record<string, unknown>)[key] = deepMerge(
|
|
109
149
|
baseValue,
|
|
110
|
-
overrideValue as
|
|
150
|
+
overrideValue as DeepPartial<typeof baseValue>,
|
|
111
151
|
);
|
|
112
152
|
} else {
|
|
113
153
|
(result as Record<string, unknown>)[key] = _clone(overrideValue);
|
package/src/support/env.ts
CHANGED
|
@@ -53,6 +53,21 @@ export const DEV_WORKER_ENV_VAR = "ZT_DEV";
|
|
|
53
53
|
*/
|
|
54
54
|
export const DEPLOY_ENV_VAR = "ZT_APP_ENV";
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Environment variable holding the *runtime mode* — `web`, `worker`, `console`.
|
|
58
|
+
*
|
|
59
|
+
* Separate from `APP_ENV`, which holds the deployment name, because they answer
|
|
60
|
+
* different questions and one variable cannot hold both. It used to try: every
|
|
61
|
+
* boot overwrote `APP_ENV` with the mode, so `APP_ENV=production` read back as
|
|
62
|
+
* `"console"` inside a CLI command and a guard written `if (env("APP_ENV") ===
|
|
63
|
+
* "production") refuse()` was inert exactly where destructive commands live.
|
|
64
|
+
*
|
|
65
|
+
* Written by `setAppEnv()`; read through {@link runtimeMode}. Settable by hand to
|
|
66
|
+
* force a mode — `APP_TYPE=web bun zt.ts something` — which is what the dev
|
|
67
|
+
* orchestrator does for the server it supervises.
|
|
68
|
+
*/
|
|
69
|
+
export const RUNTIME_MODE_VAR = "APP_TYPE";
|
|
70
|
+
|
|
56
71
|
/**
|
|
57
72
|
* The values of `APP_ENV` that name a runtime *mode* rather than a deployment.
|
|
58
73
|
* `setAppEnv()` writes these; {@link deployEnv} recognises them to know whether
|
|
@@ -73,17 +88,18 @@ export const RUNTIME_MODES: ReadonlySet<string> = new Set([
|
|
|
73
88
|
* The deployment name this process was started with — `production`, `staging`,
|
|
74
89
|
* `local`, whatever the operator set — as opposed to the runtime *mode*.
|
|
75
90
|
*
|
|
76
|
-
* `APP_ENV`
|
|
77
|
-
* `setAppEnv()`
|
|
78
|
-
*
|
|
91
|
+
* `APP_ENV` used to carry both meanings, and the second destroyed the first:
|
|
92
|
+
* `setAppEnv()` overwrote it with `web` / `console` / `worker` before the app
|
|
93
|
+
* booted, so a gate asking `isProdLike(Bun.env["APP_ENV"])` after startup was
|
|
79
94
|
* asking whether `"web"` is production and always getting no. That was not
|
|
80
95
|
* theoretical — it silently disabled the weak-`APP_KEY` refusal and left the
|
|
81
|
-
* ORM's N+1 detector wrapping every query in production
|
|
96
|
+
* ORM's N+1 detector wrapping every query in production, and it later made
|
|
97
|
+
* `env("APP_ENV")` return `"console"` inside a seeder.
|
|
82
98
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
99
|
+
* The mode now lives in its own variable ({@link RUNTIME_MODE_VAR}) and `APP_ENV`
|
|
100
|
+
* is left alone, so this is usually just a read of it. The runtime-mode branch
|
|
101
|
+
* below stays for a process started by an older launcher, or one where somebody
|
|
102
|
+
* still exports `APP_ENV=web` by hand.
|
|
87
103
|
*
|
|
88
104
|
* @internal
|
|
89
105
|
*/
|
|
@@ -101,6 +117,30 @@ export function deployEnv(): string {
|
|
|
101
117
|
return Bun.env[DEPLOY_ENV_VAR] ?? current;
|
|
102
118
|
}
|
|
103
119
|
|
|
120
|
+
/**
|
|
121
|
+
* How this process is running — `web`, `worker`, or `console`.
|
|
122
|
+
*
|
|
123
|
+
* The other half of what `APP_ENV` used to mean. Providers are filtered on it
|
|
124
|
+
* (`static environments = ["console"]`), which is why getting it wrong is not a
|
|
125
|
+
* cosmetic problem: a provider is simply never asked to register, with no error
|
|
126
|
+
* and nothing missing from the logs.
|
|
127
|
+
*
|
|
128
|
+
* `fallback` is what an unset environment means, and it differs by caller:
|
|
129
|
+
* `setAppEnv()` treats a process that never declared itself as a script
|
|
130
|
+
* (`console`), while `Application.create()` has always treated one as a server
|
|
131
|
+
* (`web`) — an app constructed directly, in a test or a script, expects its
|
|
132
|
+
* web providers to register.
|
|
133
|
+
*/
|
|
134
|
+
export function runtimeMode(fallback = "console"): string {
|
|
135
|
+
const mode = (Bun.env[RUNTIME_MODE_VAR] ?? "").toLowerCase();
|
|
136
|
+
if (RUNTIME_MODES.has(mode)) return mode;
|
|
137
|
+
|
|
138
|
+
// A process started by an older launcher, which put the mode in `APP_ENV`.
|
|
139
|
+
// eslint-disable-next-line no-restricted-syntax -- reading the legacy location is the fallback's entire job
|
|
140
|
+
const legacy = (Bun.env["APP_ENV"] ?? "").toLowerCase();
|
|
141
|
+
return RUNTIME_MODES.has(legacy) ? legacy : fallback;
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
/**
|
|
105
145
|
* Whether *this process* may expose dev-only surfaces — the stack-trace error
|
|
106
146
|
* page, the trace inspector, an open monitor panel.
|