@zerotal/core 1.0.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/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +72 -0
- package/src/application/Application.ts +1671 -0
- package/src/application/BootDoctor.ts +108 -0
- package/src/application/DevErrorPage.ts +567 -0
- package/src/application/ExceptionHandler.ts +183 -0
- package/src/application/currentApp.ts +73 -0
- package/src/assets/assets.ts +79 -0
- package/src/assets/index.ts +16 -0
- package/src/auth/AuthenticatedUser.ts +18 -0
- package/src/build/PackageLinter.ts +146 -0
- package/src/build/PackageScaffold.ts +127 -0
- package/src/build/codemod.ts +64 -0
- package/src/build/index.ts +12 -0
- package/src/command/Command.ts +254 -0
- package/src/command/CommandRunner.ts +593 -0
- package/src/command/OutputWriter.ts +61 -0
- package/src/command/builtin/CompileCommand.ts +46 -0
- package/src/command/builtin/CssBuildCommand.ts +71 -0
- package/src/command/builtin/KeyGenerateCommand.ts +58 -0
- package/src/command/builtin/LintPackagesCommand.ts +72 -0
- package/src/command/builtin/MakeCommandCommand.ts +85 -0
- package/src/command/builtin/MakeControllerCommand.ts +95 -0
- package/src/command/builtin/MakeEventCommand.ts +85 -0
- package/src/command/builtin/MakeJobCommand.ts +53 -0
- package/src/command/builtin/MakeListenerCommand.ts +35 -0
- package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
- package/src/command/builtin/MakeNotificationCommand.ts +48 -0
- package/src/command/builtin/MakeObserverCommand.ts +78 -0
- package/src/command/builtin/MakePackageCommand.ts +45 -0
- package/src/command/builtin/MakePolicyCommand.ts +66 -0
- package/src/command/builtin/MakeProviderCommand.ts +75 -0
- package/src/command/builtin/MakeRequestCommand.ts +47 -0
- package/src/command/builtin/MakeResourceCommand.ts +61 -0
- package/src/command/builtin/MakeTestCommand.ts +120 -0
- package/src/command/builtin/ReloadCommand.ts +52 -0
- package/src/command/builtin/ReplCommand.ts +174 -0
- package/src/command/builtin/RouteListCommand.ts +188 -0
- package/src/command/builtin/ServeCommand.ts +321 -0
- package/src/command/builtin/StartCommand.ts +3 -0
- package/src/command/builtin/StatusCommand.ts +71 -0
- package/src/command/builtin/TestCommand.ts +172 -0
- package/src/command/builtin/WorkerCommand.ts +27 -0
- package/src/command/builtin/index.ts +53 -0
- package/src/command/scaffold/worker.ts.txt +12 -0
- package/src/command/scaffold/zerotal.ts.txt +26 -0
- package/src/command/startZerotal.ts +55 -0
- package/src/config/AppConfig.ts +253 -0
- package/src/config/ConfigLoader.ts +117 -0
- package/src/config/ConfigManager.ts +169 -0
- package/src/config/index.ts +46 -0
- package/src/config/registry.ts +59 -0
- package/src/config/validation.ts +117 -0
- package/src/container/Container.ts +606 -0
- package/src/container/ContextualBindingBuilder.ts +57 -0
- package/src/container/ScopedResolver.ts +117 -0
- package/src/container/index.ts +32 -0
- package/src/container/inject.ts +55 -0
- package/src/container/types.ts +71 -0
- package/src/context/RequestContext.ts +91 -0
- package/src/contracts/auth.ts +24 -0
- package/src/contracts/index.ts +23 -0
- package/src/contracts/session.ts +70 -0
- package/src/contracts/transaction.ts +26 -0
- package/src/conventions/ConventionLoader.ts +128 -0
- package/src/conventions/builtinConcerns.ts +131 -0
- package/src/crypt/Crypt.ts +141 -0
- package/src/crypt/URLSigner.ts +96 -0
- package/src/datetime/Carbon.ts +1396 -0
- package/src/datetime/CarbonInterval.ts +421 -0
- package/src/datetime/clock.ts +28 -0
- package/src/datetime/index.ts +23 -0
- package/src/datetime/temporal-shim.ts +1 -0
- package/src/dev/BuildOutput.ts +131 -0
- package/src/dev/CssPlugins.ts +184 -0
- package/src/dev/DevBuildHook.ts +74 -0
- package/src/dev/DevOrchestrator.ts +213 -0
- package/src/dev/DevReloadMiddleware.ts +101 -0
- package/src/dev/DevReloadServer.ts +85 -0
- package/src/dev/DevWsServer.ts +45 -0
- package/src/dev/index.ts +19 -0
- package/src/dev/reloadClient.ts +39 -0
- package/src/env/Def.ts +232 -0
- package/src/env/EnvSchema.ts +105 -0
- package/src/env/index.ts +34 -0
- package/src/env/t.ts +128 -0
- package/src/errors/ConfigError.ts +12 -0
- package/src/errors/ContainerErrors.ts +143 -0
- package/src/errors/HttpError.ts +127 -0
- package/src/errors/ValidationError.ts +19 -0
- package/src/errors/ZerotalError.ts +25 -0
- package/src/errors/index.ts +46 -0
- package/src/events/CallQueuedListener.ts +66 -0
- package/src/events/Emitter.ts +280 -0
- package/src/events/EventFake.ts +160 -0
- package/src/events/FrameworkEvents.ts +252 -0
- package/src/facade/Facade.ts +101 -0
- package/src/facade/facades/App.ts +155 -0
- package/src/facade/facades/Artisan.ts +63 -0
- package/src/facade/facades/Config.ts +21 -0
- package/src/facade/facades/Events.ts +19 -0
- package/src/facade/facades/index.ts +28 -0
- package/src/global.d.ts +9 -0
- package/src/hash/Hash.ts +60 -0
- package/src/health/Health.ts +221 -0
- package/src/health/index.ts +27 -0
- package/src/helpers/Collection.ts +435 -0
- package/src/helpers/config.ts +59 -0
- package/src/helpers/fluent.ts +52 -0
- package/src/helpers/html.ts +11 -0
- package/src/helpers/index.ts +266 -0
- package/src/helpers/make.ts +35 -0
- package/src/helpers/markdown.ts +73 -0
- package/src/helpers/pageElements.ts +27 -0
- package/src/helpers/request.ts +62 -0
- package/src/helpers/response.ts +411 -0
- package/src/helpers/str.ts +208 -0
- package/src/http/Http.ts +298 -0
- package/src/http/HttpClient.ts +289 -0
- package/src/http/Resource.ts +171 -0
- package/src/http/UploadedFile.ts +204 -0
- package/src/http/Uri.ts +490 -0
- package/src/http/index.ts +46 -0
- package/src/http/negotiate.ts +213 -0
- package/src/http/originGuard.ts +76 -0
- package/src/http/sniffContentType.ts +105 -0
- package/src/http/url.ts +204 -0
- package/src/http/withHeaders.ts +24 -0
- package/src/index.ts +250 -0
- package/src/lock/LockManager.ts +228 -0
- package/src/lock/config.ts +49 -0
- package/src/lock/drivers/LockDriver.ts +32 -0
- package/src/lock/drivers/MemoryLockDriver.ts +52 -0
- package/src/lock/drivers/RedisLockDriver.ts +58 -0
- package/src/lock/drivers/SqliteLockDriver.ts +85 -0
- package/src/lock/errors.ts +20 -0
- package/src/lock/facades/Lock.ts +114 -0
- package/src/lock/index.ts +53 -0
- package/src/logger/Log.ts +35 -0
- package/src/logger/LogManager.ts +430 -0
- package/src/logger/LoggerMiddleware.ts +125 -0
- package/src/logger/channels/ConsoleChannel.ts +139 -0
- package/src/logger/channels/DailyChannel.ts +74 -0
- package/src/logger/channels/NullChannel.ts +17 -0
- package/src/logger/channels/SingleChannel.ts +34 -0
- package/src/logger/channels/StackChannel.ts +29 -0
- package/src/logger/config.ts +90 -0
- package/src/logger/format.ts +96 -0
- package/src/logger/frameworkLog.ts +93 -0
- package/src/logger/index.ts +68 -0
- package/src/logger/renderTable.ts +111 -0
- package/src/logger/types.ts +212 -0
- package/src/macros/config.macro.ts +50 -0
- package/src/metrics/HttpMetrics.ts +114 -0
- package/src/metrics/index.ts +18 -0
- package/src/middleware/BaseMiddleware.ts +72 -0
- package/src/middleware/CorsMiddleware.ts +152 -0
- package/src/middleware/RateLimiter.ts +255 -0
- package/src/middleware/SecureHeadersMiddleware.ts +127 -0
- package/src/middleware/ThrottleMiddleware.ts +252 -0
- package/src/middleware/WebhookMiddleware.ts +204 -0
- package/src/pipeline/ContextRegistry.ts +42 -0
- package/src/pipeline/HttpContext.ts +865 -0
- package/src/pipeline/Pipeline.ts +150 -0
- package/src/pipeline/currentPage.ts +46 -0
- package/src/pipeline/types.ts +80 -0
- package/src/provider/LockProvider.ts +64 -0
- package/src/provider/LogProvider.ts +137 -0
- package/src/provider/ServiceProvider.ts +84 -0
- package/src/provider/StorageProvider.ts +45 -0
- package/src/router/FileRouter.ts +526 -0
- package/src/router/Route.ts +76 -0
- package/src/router/RouteHandler.ts +335 -0
- package/src/router/Router.ts +1247 -0
- package/src/router/domain.ts +65 -0
- package/src/security/index.ts +22 -0
- package/src/storage/FakeDisk.ts +233 -0
- package/src/storage/StorageFilesMiddleware.ts +150 -0
- package/src/storage/StorageManager.ts +173 -0
- package/src/storage/config.ts +47 -0
- package/src/storage/drivers/LocalDriver.ts +138 -0
- package/src/storage/drivers/S3Driver.ts +169 -0
- package/src/storage/errors.ts +135 -0
- package/src/storage/facades/Storage.ts +3 -0
- package/src/storage/global.d.ts +7 -0
- package/src/storage/index.ts +22 -0
- package/src/storage/root.ts +59 -0
- package/src/storage/types.ts +104 -0
- package/src/support/appKey.ts +38 -0
- package/src/support/cookie.ts +72 -0
- package/src/support/crypto.ts +52 -0
- package/src/support/deepMerge.ts +117 -0
- package/src/support/env.ts +71 -0
- package/src/support/network.ts +79 -0
- package/src/support/port.ts +197 -0
- package/src/support/str.ts +122 -0
- package/src/view/FileRouteResolver.ts +59 -0
- package/src/view/index.ts +144 -0
- package/src/view/jsx-runtime.ts +233 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The facade factory: wraps a named container binding in a Proxy so it can be
|
|
3
|
+
* used like a static object while still resolving lazily from the live
|
|
4
|
+
* container on every access.
|
|
5
|
+
*/
|
|
6
|
+
import type { Application } from "../application/Application.ts";
|
|
7
|
+
import { currentApp } from "../application/currentApp.ts";
|
|
8
|
+
import type { ContainerBindings } from "../container/types.ts";
|
|
9
|
+
import {
|
|
10
|
+
FacadeAccessedBeforeBootError,
|
|
11
|
+
FacadeBindingMissingError,
|
|
12
|
+
} from "../errors/ContainerErrors.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a typed facade over a named container binding.
|
|
16
|
+
*
|
|
17
|
+
* The returned object delegates every property access to the live instance
|
|
18
|
+
* resolved from the container **on each access** — lazy, never cached at
|
|
19
|
+
* module load time. This means:
|
|
20
|
+
*
|
|
21
|
+
* - Importing `Config` before `Application.boot()` is safe.
|
|
22
|
+
* - The binding is always the most recently registered singleton.
|
|
23
|
+
*
|
|
24
|
+
* Prerequisites:
|
|
25
|
+
* - `Application.create()` must have been called.
|
|
26
|
+
* - The singleton must have been pre-resolved (e.g. via `make()` in
|
|
27
|
+
* `onBooting()`) because `makeSync()` only works for resolved singletons.
|
|
28
|
+
*
|
|
29
|
+
* Methods are automatically bound to the live instance so `this` is correct
|
|
30
|
+
* inside them regardless of how the caller stores the reference.
|
|
31
|
+
*
|
|
32
|
+
* @param key The container binding name the facade resolves on each access.
|
|
33
|
+
* @returns A proxy typed as the bound instance; every access resolves live.
|
|
34
|
+
* @throws {FacadeAccessedBeforeBootError} If accessed before the application is
|
|
35
|
+
* created (e.g. at module scope on import).
|
|
36
|
+
* @throws {FacadeBindingMissingError} If the application exists but no binding is
|
|
37
|
+
* registered for `key` (a missing ServiceProvider).
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // packages/core/src/facade/facades/Config.ts
|
|
42
|
+
* export const Config = createFacade('config');
|
|
43
|
+
*
|
|
44
|
+
* // Anywhere in the application (after boot):
|
|
45
|
+
* Config.get('app.name');
|
|
46
|
+
* await Events.emit(new UserRegistered(id));
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Properties the language itself probes, which must never resolve the binding.
|
|
51
|
+
*
|
|
52
|
+
* Returning a facade from an `async` function makes the runtime read `.then` to
|
|
53
|
+
* decide whether it is a thenable. Answering that with a container lookup meant
|
|
54
|
+
* a facade could not be returned from an async function at all without throwing
|
|
55
|
+
* — from inside promise resolution, with a stack pointing at the proxy rather
|
|
56
|
+
* than at the caller. `Symbol.toPrimitive` and friends are the same trap for
|
|
57
|
+
* string coercion and inspection.
|
|
58
|
+
*/
|
|
59
|
+
const _PROBES: Array<string | symbol> = [
|
|
60
|
+
"then",
|
|
61
|
+
"catch",
|
|
62
|
+
"finally",
|
|
63
|
+
Symbol.toPrimitive,
|
|
64
|
+
Symbol.toStringTag,
|
|
65
|
+
Symbol.iterator,
|
|
66
|
+
Symbol.asyncIterator,
|
|
67
|
+
"inspect",
|
|
68
|
+
"constructor",
|
|
69
|
+
"nodejs.util.inspect.custom",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export function createFacade<K extends keyof ContainerBindings>(key: K): ContainerBindings[K] {
|
|
73
|
+
return new Proxy({} as ContainerBindings[K], {
|
|
74
|
+
get(_target, prop: string | symbol) {
|
|
75
|
+
// A probe is the runtime asking what this object *is*, not the application
|
|
76
|
+
// asking the binding to do something. Answer "nothing special" without
|
|
77
|
+
// touching the container.
|
|
78
|
+
if (_PROBES.includes(prop)) return undefined;
|
|
79
|
+
|
|
80
|
+
// Distinguish "app not created yet" (module-scope misuse) from "binding not
|
|
81
|
+
// registered" (a missing ServiceProvider) — they need very different fixes.
|
|
82
|
+
let app: Application;
|
|
83
|
+
try {
|
|
84
|
+
app = currentApp();
|
|
85
|
+
} catch {
|
|
86
|
+
throw new FacadeAccessedBeforeBootError(String(key));
|
|
87
|
+
}
|
|
88
|
+
let instance: ContainerBindings[K];
|
|
89
|
+
try {
|
|
90
|
+
instance = app.container.makeSync(key);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new FacadeBindingMissingError(String(key));
|
|
93
|
+
}
|
|
94
|
+
const value = (instance as unknown as Record<string | symbol, unknown>)[prop];
|
|
95
|
+
if (typeof value === "function") {
|
|
96
|
+
return (value as (...args: unknown[]) => unknown).bind(instance);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `App` facade — static-style access to the running {@link Application}
|
|
3
|
+
* kernel and its IoC container.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the live application singleton on every call, so it holds no
|
|
6
|
+
* module-level state and is safe to import anywhere. Every method throws if
|
|
7
|
+
* accessed before `Application.create()` (and, for resolution, `boot()`) has
|
|
8
|
+
* run, mirroring the behaviour of the other framework facades.
|
|
9
|
+
*
|
|
10
|
+
* Registration methods (`bind` / `singleton` / `scoped` / `value` / `alias` /
|
|
11
|
+
* `forget`) are **boot-time only**: once `boot()` completes the container is
|
|
12
|
+
* locked and they throw {@link ContainerLockedError}, because mutating the
|
|
13
|
+
* process-global container at request time would leak state across concurrent
|
|
14
|
+
* requests. Register during boot (bootstrap `app.bind()`, a `ServiceProvider`,
|
|
15
|
+
* or the `app/services` convention) and use a `scoped` binding for per-request
|
|
16
|
+
* state.
|
|
17
|
+
*/
|
|
18
|
+
import type { Application } from "../../application/Application.ts";
|
|
19
|
+
import { currentApp } from "../../application/currentApp.ts";
|
|
20
|
+
import type { Container } from "../../container/Container.ts";
|
|
21
|
+
import type { BindingToken, ContainerBindings, Factory } from "../../container/types.ts";
|
|
22
|
+
import { ContainerLockedError } from "../../errors/ContainerErrors.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Return the live container, throwing {@link ContainerLockedError} when the
|
|
26
|
+
* application has already booted. Used to gate every registration method.
|
|
27
|
+
*/
|
|
28
|
+
function _unlockedContainer(method: string): Container {
|
|
29
|
+
const app = currentApp();
|
|
30
|
+
if (app.booted) throw new ContainerLockedError(method);
|
|
31
|
+
return app.container;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Static facade for the application container.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* // Auto-wire a service class (resolves its @inject() graph):
|
|
40
|
+
* const users = await App.make(UsersService);
|
|
41
|
+
*
|
|
42
|
+
* // Resolve a named binding:
|
|
43
|
+
* const events = await App.make("events");
|
|
44
|
+
*
|
|
45
|
+
* // Register during boot (bootstrap/app.ts or a provider):
|
|
46
|
+
* App.singleton(Clock, () => new SystemClock());
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export const App = {
|
|
50
|
+
// ── Introspection ─────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/** The live IoC container of the running application. */
|
|
53
|
+
get container(): Container {
|
|
54
|
+
return currentApp().container;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/** The running {@link Application} instance. */
|
|
58
|
+
instance(): Application {
|
|
59
|
+
return currentApp();
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/** The runtime environment: "web" | "console" | "worker" | "test" | "repl". */
|
|
63
|
+
environment(): Application["environment"] {
|
|
64
|
+
return currentApp().environment;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/** Whether the configured `app.env` is a production environment. */
|
|
68
|
+
isProduction(): boolean {
|
|
69
|
+
return ["production", "prod"].includes(_appEnv());
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/** Whether the configured `app.env` is a local/development environment. */
|
|
73
|
+
isLocal(): boolean {
|
|
74
|
+
return ["local", "development", "dev"].includes(_appEnv());
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// ── Resolution ────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a binding — or auto-wire a class via its `@inject()`
|
|
81
|
+
* metadata — from the container asynchronously. This is the primary
|
|
82
|
+
* resolution method.
|
|
83
|
+
*/
|
|
84
|
+
make<T>(token: BindingToken<T>, consumer?: unknown): Promise<T> {
|
|
85
|
+
return currentApp().container.make(token, consumer);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a binding synchronously. Only works for value bindings and
|
|
90
|
+
* already-resolved singletons; throws otherwise. Prefer {@link App.make}.
|
|
91
|
+
*/
|
|
92
|
+
makeSync<T>(token: BindingToken<T>): T {
|
|
93
|
+
return currentApp().container.makeSync(token);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Construct a class by auto-wiring its dependencies, ignoring any registered
|
|
98
|
+
* binding for that token. Returns a brand-new instance every call.
|
|
99
|
+
*/
|
|
100
|
+
build<T>(ctor: new (...args: unknown[]) => T): Promise<T> {
|
|
101
|
+
return currentApp().container.build(ctor);
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a named binding without throwing — returns `undefined` when the
|
|
106
|
+
* token is not registered or cannot be resolved synchronously.
|
|
107
|
+
*/
|
|
108
|
+
tryMake<K extends keyof ContainerBindings>(token: K): ContainerBindings[K] | undefined {
|
|
109
|
+
return currentApp().container.tryMake(token);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/** Whether a token is registered in the container. */
|
|
113
|
+
bound(token: BindingToken): boolean {
|
|
114
|
+
const container = currentApp().container;
|
|
115
|
+
return container.registry.has(container._resolveAlias(token));
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// ── Registration (boot-time only) ─────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/** Register a transient binding — the factory runs on every resolution. */
|
|
121
|
+
bind<T>(token: BindingToken<T>, factory: Factory<T>): Container {
|
|
122
|
+
return _unlockedContainer("bind").bind(token, factory);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/** Register a singleton binding — resolved once and cached for the process lifetime. */
|
|
126
|
+
singleton<T>(token: BindingToken<T>, factory: Factory<T>): Container {
|
|
127
|
+
return _unlockedContainer("singleton").singleton(token, factory);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/** Register a request-scoped binding — resolved once per request scope. */
|
|
131
|
+
scoped<T>(token: BindingToken<T>, factory: Factory<T>): Container {
|
|
132
|
+
return _unlockedContainer("scoped").scoped(token, factory);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/** Register an already-constructed value under `token`. */
|
|
136
|
+
value<T>(token: BindingToken<T>, instance: T): Container {
|
|
137
|
+
return _unlockedContainer("value").value(token, instance);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/** Make resolving `from` resolve `to` instead. */
|
|
141
|
+
alias(from: unknown, to: unknown): Container {
|
|
142
|
+
return _unlockedContainer("alias").alias(from, to);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/** Remove a binding. Returns `true` if a binding existed for the token. */
|
|
146
|
+
forget(token: BindingToken): boolean {
|
|
147
|
+
return _unlockedContainer("forget").forget(token);
|
|
148
|
+
},
|
|
149
|
+
} as const;
|
|
150
|
+
|
|
151
|
+
/** Read the configured `app.env`, defaulting to "development" when unavailable. */
|
|
152
|
+
function _appEnv(): string {
|
|
153
|
+
const config = currentApp().container.tryMake("config");
|
|
154
|
+
return config?.get<string>("app.env", "development") ?? "development";
|
|
155
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Artisan` facade — runs registered CLI commands from inside a running
|
|
3
|
+
* application and captures their output instead of writing to the terminal.
|
|
4
|
+
*/
|
|
5
|
+
import { currentApp } from "../../application/currentApp.ts";
|
|
6
|
+
import type { CommandRunner } from "../../command/CommandRunner.ts";
|
|
7
|
+
|
|
8
|
+
/** Outcome of running a command through {@link Artisan}. */
|
|
9
|
+
export interface ArtisanResult {
|
|
10
|
+
/** Process-style exit code: 0 = success, non-zero = failure. */
|
|
11
|
+
code: number;
|
|
12
|
+
/** Everything the command wrote via info/error/warn/etc. */
|
|
13
|
+
output: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Artisan facade — run any registered command from inside the application.
|
|
18
|
+
* Output is captured; nothing is written to the server terminal.
|
|
19
|
+
* Safe to call from controllers, services, scheduled tasks.
|
|
20
|
+
*
|
|
21
|
+
* Only use for fast commands. For long-running work, use Bun.spawn or worker.ts.
|
|
22
|
+
*
|
|
23
|
+
* See: plans/boot-modes.md §6
|
|
24
|
+
*
|
|
25
|
+
* @throws {Error} From {@link Artisan.call} when no CommandRunner is registered
|
|
26
|
+
* in the container (commands are only registered for `env='web'`).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const { code, output } = await Artisan.call('cache:clear');
|
|
31
|
+
* const { code, output } = await Artisan.call('migrate', { '--fresh': true });
|
|
32
|
+
* return Response.json({ output, success: code === 0 });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const Artisan = {
|
|
36
|
+
async call(
|
|
37
|
+
commandName: string,
|
|
38
|
+
parameters: Record<string, string | boolean | number> = {},
|
|
39
|
+
): Promise<ArtisanResult> {
|
|
40
|
+
const container = currentApp().container;
|
|
41
|
+
const runner = container.tryMake("commands") as CommandRunner | undefined;
|
|
42
|
+
|
|
43
|
+
if (!runner) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Artisan.call('${commandName}') failed: no CommandRunner in container. ` +
|
|
46
|
+
`Register commands in AppServiceProvider.onBooted() for env='web'.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Booleans become bare flags (only when true); everything else becomes a
|
|
51
|
+
// `key=value` token, mirroring how the CLI parses argv.
|
|
52
|
+
const commandArguments = [commandName];
|
|
53
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
54
|
+
if (typeof value === "boolean") {
|
|
55
|
+
if (value) commandArguments.push(key);
|
|
56
|
+
} else {
|
|
57
|
+
commandArguments.push(`${key}=${String(value)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return runner.callInProcess(commandArguments);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Config` facade — static-style access to the application configuration
|
|
3
|
+
* registered under the container's `config` binding.
|
|
4
|
+
*/
|
|
5
|
+
import { createFacade } from "../Facade.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Facade over the `config` binding for reading configuration values.
|
|
9
|
+
*
|
|
10
|
+
* Resolves the live `config` instance from the container on each access, so it
|
|
11
|
+
* is only usable after `Application.boot()` has run.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { Config } from "@zerotal/core/facades";
|
|
16
|
+
*
|
|
17
|
+
* Config.get("app.name"); // read with the app's config value type
|
|
18
|
+
* Config.require("app.key"); // throws ConfigError when unset
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const Config = createFacade("config");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Events` facade — static-style access to the application event emitter
|
|
3
|
+
* registered under the container's `events` binding.
|
|
4
|
+
*/
|
|
5
|
+
import { createFacade } from "../Facade.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Facade over the `events` binding ({@link Emitter}) for emitting events and
|
|
9
|
+
* registering listeners.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { Events } from "@zerotal/core/facades";
|
|
14
|
+
*
|
|
15
|
+
* Events.on(UserRegistered, SendWelcomeEmail);
|
|
16
|
+
* await Events.emit(new UserRegistered(user.id, user.email));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const Events = createFacade("events");
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@zerotal/core/facades` subpath — the framework's static-style facades.
|
|
3
|
+
*
|
|
4
|
+
* A facade is a thin, always-safe-to-import handle over a named container
|
|
5
|
+
* binding: every property access resolves the live instance from the running
|
|
6
|
+
* application on demand, so facades hold no module-level state and stay correct
|
|
7
|
+
* across boots. {@link App} reaches the application kernel and IoC container,
|
|
8
|
+
* {@link Config} reads configuration, {@link Events} emits framework events, and
|
|
9
|
+
* {@link Artisan} runs registered CLI commands in-process. All of them require
|
|
10
|
+
* `Application.create()` (and, for resolution, `boot()`) to have run first.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { App, Config, Events, Artisan } from "@zerotal/core/facades";
|
|
15
|
+
*
|
|
16
|
+
* const name = Config.get("app.name");
|
|
17
|
+
* const users = await App.make("users");
|
|
18
|
+
* await Events.emit(new UserRegistered(user.id));
|
|
19
|
+
* const { code, output } = await Artisan.call("cache:clear");
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
export { App } from "./App.ts";
|
|
25
|
+
export { Config } from "./Config.ts";
|
|
26
|
+
export { Events } from "./Events.ts";
|
|
27
|
+
export { Artisan } from "./Artisan.ts";
|
|
28
|
+
export type { ArtisanResult } from "./Artisan.ts";
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Ambient declarations specific to this codebase.
|
|
2
|
+
//
|
|
3
|
+
// Bun, Node (`node:*`), and `bun:test` types come from `@types/bun` (→ bun-types).
|
|
4
|
+
// Only declarations that bun-types does NOT provide live here.
|
|
5
|
+
|
|
6
|
+
// Bun extends Request with route params (e.g. /users/:id → { id: '42' }).
|
|
7
|
+
interface Request {
|
|
8
|
+
readonly params?: Record<string, string>;
|
|
9
|
+
}
|
package/src/hash/Hash.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password hashing over `Bun.password` — argon2id by default, bcrypt optional.
|
|
3
|
+
* A zero-config core primitive available framework-wide (no provider required).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { Hash } from '@zerotal/core';
|
|
7
|
+
* const hash = await Hash.make('secret123');
|
|
8
|
+
* const valid = await Hash.verify('secret123', hash); // true
|
|
9
|
+
*/
|
|
10
|
+
/** Supported password-hashing algorithms. */
|
|
11
|
+
export type HashAlgorithm = "argon2id" | "argon2i" | "argon2d" | "bcrypt";
|
|
12
|
+
|
|
13
|
+
class HashManager {
|
|
14
|
+
private _default: HashAlgorithm = "argon2id";
|
|
15
|
+
|
|
16
|
+
/** Set the default hashing algorithm. */
|
|
17
|
+
setDefault(algorithm: HashAlgorithm): void {
|
|
18
|
+
this._default = algorithm;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Hash a value. Store the returned string. */
|
|
22
|
+
async make(value: string, options: { algorithm?: HashAlgorithm } = {}): Promise<string> {
|
|
23
|
+
return Bun.password.hash(value, { algorithm: options.algorithm ?? this._default });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Verify a value against a stored hash (algorithm auto-detected from the hash). */
|
|
27
|
+
async verify(value: string, hash: string): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
return await Bun.password.verify(value, hash);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Alias of {@link verify}. */
|
|
36
|
+
async check(value: string, hash: string): Promise<boolean> {
|
|
37
|
+
return this.verify(value, hash);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True when a hash was made with a different algorithm than the current default. */
|
|
41
|
+
needsRehash(hash: string): boolean {
|
|
42
|
+
return this._default.startsWith("argon")
|
|
43
|
+
? !hash.startsWith(`$${this._default}$`)
|
|
44
|
+
: !hash.startsWith("$2"); // bcrypt
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Password hashing primitive (argon2id by default).
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { Hash } from "@zerotal/core/security";
|
|
54
|
+
*
|
|
55
|
+
* const digest = await Hash.make("hunter2");
|
|
56
|
+
* await Hash.check("hunter2", digest); // true
|
|
57
|
+
* Hash.needsRehash(digest); // false — re-hash on next login when true
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export const Hash = new HashManager();
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health checks — a small registry of named probes plus the report/route logic
|
|
3
|
+
* behind the framework's `/health` endpoint.
|
|
4
|
+
*
|
|
5
|
+
* A check returns (or resolves to) a {@link HealthResult}; throwing, or
|
|
6
|
+
* returning `{ status: 'down' }`, marks it unhealthy. Checks flagged `critical`
|
|
7
|
+
* drive the overall status (and the HTTP 503 response); non-critical failures
|
|
8
|
+
* degrade the report without failing readiness.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { Health } from '@zerotal/core';
|
|
12
|
+
*
|
|
13
|
+
* Health.register('database', async () => {
|
|
14
|
+
* await DB.raw('select 1');
|
|
15
|
+
* }, { critical: true });
|
|
16
|
+
*
|
|
17
|
+
* Health.register('cache', async () => {
|
|
18
|
+
* await Cache.put('__health', '1', 5);
|
|
19
|
+
* return { meta: { driver: 'redis' } };
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Overall and per-check health status. */
|
|
24
|
+
export type HealthStatus = "ok" | "degraded" | "down";
|
|
25
|
+
|
|
26
|
+
/** What a health check returns to describe its own state. */
|
|
27
|
+
export interface HealthResult {
|
|
28
|
+
/** Defaults to `ok` when a check returns without throwing. */
|
|
29
|
+
status?: HealthStatus;
|
|
30
|
+
message?: string;
|
|
31
|
+
meta?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A single health probe; may run sync or async and may return nothing for "ok". */
|
|
35
|
+
export type HealthCheckFn = () => Promise<HealthResult | void> | HealthResult | void;
|
|
36
|
+
|
|
37
|
+
/** The outcome of running one check, as it appears in the aggregate report. */
|
|
38
|
+
export interface HealthCheckReport {
|
|
39
|
+
status: HealthStatus;
|
|
40
|
+
message?: string;
|
|
41
|
+
durationMs: number;
|
|
42
|
+
critical: boolean;
|
|
43
|
+
meta?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The full health report returned by the endpoint. */
|
|
47
|
+
export interface HealthReport {
|
|
48
|
+
status: HealthStatus;
|
|
49
|
+
app: { name: string; version: string; environment: string; bootMs?: number };
|
|
50
|
+
/** Seconds since the process started. */
|
|
51
|
+
uptime: number;
|
|
52
|
+
timestamp: string;
|
|
53
|
+
checks: Record<string, HealthCheckReport>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Application metadata the report needs that the registry can't know on its own. */
|
|
57
|
+
export interface HealthRunMeta {
|
|
58
|
+
name: string;
|
|
59
|
+
version: string;
|
|
60
|
+
environment: string;
|
|
61
|
+
uptime: number;
|
|
62
|
+
/** Wall-clock boot time in milliseconds, surfaced in the report's `app` block. */
|
|
63
|
+
bootMs?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class HealthRegistry {
|
|
67
|
+
private readonly _checks = new Map<string, { fn: HealthCheckFn; critical: boolean }>();
|
|
68
|
+
|
|
69
|
+
/** Register (or replace) a named check. `critical` checks can fail readiness. */
|
|
70
|
+
register(name: string, fn: HealthCheckFn, options: { critical?: boolean } = {}): this {
|
|
71
|
+
this._checks.set(name, { fn, critical: options.critical ?? false });
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Remove a previously registered check by name. */
|
|
76
|
+
remove(name: string): this {
|
|
77
|
+
this._checks.delete(name);
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
/** Remove every registered check. */
|
|
81
|
+
clear(): this {
|
|
82
|
+
this._checks.clear();
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
/** Whether a check is registered under the given name. */
|
|
86
|
+
has(name: string): boolean {
|
|
87
|
+
return this._checks.has(name);
|
|
88
|
+
}
|
|
89
|
+
/** The names of all registered checks. */
|
|
90
|
+
get names(): string[] {
|
|
91
|
+
return [...this._checks.keys()];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Run every check and assemble the aggregate report. */
|
|
95
|
+
async run(meta: HealthRunMeta): Promise<HealthReport> {
|
|
96
|
+
const checks: Record<string, HealthCheckReport> = {};
|
|
97
|
+
let anyCriticalDown = false;
|
|
98
|
+
let anyDegraded = false;
|
|
99
|
+
|
|
100
|
+
await Promise.all(
|
|
101
|
+
[...this._checks.entries()].map(async ([name, { fn, critical }]) => {
|
|
102
|
+
const startedAt = Date.now();
|
|
103
|
+
let report: HealthCheckReport;
|
|
104
|
+
try {
|
|
105
|
+
const result = (await fn()) ?? {};
|
|
106
|
+
const status = result.status ?? "ok";
|
|
107
|
+
report = {
|
|
108
|
+
status,
|
|
109
|
+
durationMs: Date.now() - startedAt,
|
|
110
|
+
critical,
|
|
111
|
+
...(result.message ? { message: result.message } : {}),
|
|
112
|
+
...(result.meta ? { meta: result.meta } : {}),
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
report = {
|
|
116
|
+
status: "down",
|
|
117
|
+
durationMs: Date.now() - startedAt,
|
|
118
|
+
critical,
|
|
119
|
+
message: (error as Error)?.message ?? String(error),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (report.status === "down" && critical) anyCriticalDown = true;
|
|
123
|
+
else if (report.status === "down" || report.status === "degraded") anyDegraded = true;
|
|
124
|
+
checks[name] = report;
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const status: HealthStatus = anyCriticalDown ? "down" : anyDegraded ? "degraded" : "ok";
|
|
129
|
+
return {
|
|
130
|
+
status,
|
|
131
|
+
app: {
|
|
132
|
+
name: meta.name,
|
|
133
|
+
version: meta.version,
|
|
134
|
+
environment: meta.environment,
|
|
135
|
+
...(meta.bootMs !== undefined ? { bootMs: meta.bootMs } : {}),
|
|
136
|
+
},
|
|
137
|
+
uptime: meta.uptime,
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
checks,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Global health-check registry. */
|
|
145
|
+
export const Health = new HealthRegistry();
|
|
146
|
+
|
|
147
|
+
// ── Endpoint configuration + access control ───────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/** The health config, authored under the `health` key of `config/app.ts`. */
|
|
150
|
+
export interface HealthConfigShape {
|
|
151
|
+
/** Serve the endpoint. Default: on outside production, off in production. */
|
|
152
|
+
enabled?: boolean;
|
|
153
|
+
/** Route path. Default: `/health`. */
|
|
154
|
+
path?: string;
|
|
155
|
+
/** Shared secret. Required in production; supplied via `?key=` or `X-Health-Key`. */
|
|
156
|
+
secret?: string;
|
|
157
|
+
/** Include per-check details. Set false for a bare `{ status }` body. Default: true. */
|
|
158
|
+
showDetails?: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The health config after defaults and back-compat have been applied. */
|
|
162
|
+
export interface ResolvedHealthConfig {
|
|
163
|
+
enabled: boolean;
|
|
164
|
+
path: string;
|
|
165
|
+
secret?: string;
|
|
166
|
+
showDetails: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolve the health config against the environment. `legacyEnabled` maps the
|
|
171
|
+
* older `app.health` boolean onto `enabled` for back-compat.
|
|
172
|
+
*/
|
|
173
|
+
export function resolveHealthConfig(
|
|
174
|
+
raw: HealthConfigShape | undefined,
|
|
175
|
+
isProduction: boolean,
|
|
176
|
+
legacyEnabled?: boolean,
|
|
177
|
+
): ResolvedHealthConfig {
|
|
178
|
+
const enabled = raw?.enabled ?? legacyEnabled ?? !isProduction;
|
|
179
|
+
return {
|
|
180
|
+
enabled,
|
|
181
|
+
path: raw?.path ?? "/health",
|
|
182
|
+
showDetails: raw?.showDetails ?? true,
|
|
183
|
+
...(raw?.secret ? { secret: raw.secret } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The result of an access-control decision for the health endpoint. */
|
|
188
|
+
export interface HealthAccess {
|
|
189
|
+
allowed: boolean;
|
|
190
|
+
/** HTTP status to use when `allowed` is false. */
|
|
191
|
+
code?: number;
|
|
192
|
+
reason?: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Decide whether a request may read the endpoint.
|
|
197
|
+
* - A configured `secret` is always enforced (`?key=` or `X-Health-Key`).
|
|
198
|
+
* - In production with no secret the endpoint is refused (503) — it must be
|
|
199
|
+
* protected before it can be exposed.
|
|
200
|
+
* - Otherwise (development, no secret) access is open.
|
|
201
|
+
*/
|
|
202
|
+
export function checkHealthAccess(
|
|
203
|
+
request: Request,
|
|
204
|
+
config: ResolvedHealthConfig,
|
|
205
|
+
isProduction: boolean,
|
|
206
|
+
): HealthAccess {
|
|
207
|
+
if (config.secret) {
|
|
208
|
+
const provided =
|
|
209
|
+
new URL(request.url).searchParams.get("key") ?? request.headers.get("x-health-key");
|
|
210
|
+
if (provided === config.secret) return { allowed: true };
|
|
211
|
+
return { allowed: false, code: 401, reason: "Invalid or missing health key." };
|
|
212
|
+
}
|
|
213
|
+
if (isProduction) {
|
|
214
|
+
return {
|
|
215
|
+
allowed: false,
|
|
216
|
+
code: 503,
|
|
217
|
+
reason: "Health endpoint requires `app.health.secret` in production.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return { allowed: true };
|
|
221
|
+
}
|