@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
package/src/http/Uri.ts
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluent, immutable URI builder for composing and manipulating URLs as objects.
|
|
3
|
+
*
|
|
4
|
+
* Every mutator returns a NEW `Uri`, so instances are safe to share. Works with both absolute
|
|
5
|
+
* URLs (`https://example.com/p?x=1`) and relative ones (`/dashboard?tab=2`).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* uri("https://example.com/users?page=1")
|
|
9
|
+
* .withQuery({ page: 2, sort: "name" })
|
|
10
|
+
* .withFragment("top")
|
|
11
|
+
* .value(); // "https://example.com/users?page=2&sort=name#top"
|
|
12
|
+
*
|
|
13
|
+
* uri("/login").redirect(); // → ResponseBuilder (302)
|
|
14
|
+
* uri().intended("/dashboard").redirect(); // → redirect to the stored intended URL
|
|
15
|
+
*/
|
|
16
|
+
import { RequestContext } from "../context/RequestContext.ts";
|
|
17
|
+
import { route } from "../router/Router.ts";
|
|
18
|
+
import { safeRedirectPath } from "../pipeline/HttpContext.ts";
|
|
19
|
+
import { ResponseBuilder } from "../helpers/response.ts";
|
|
20
|
+
import { config } from "../helpers/config.ts";
|
|
21
|
+
|
|
22
|
+
type QueryValue = string | string[];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Accepted shape for query parameters passed to the `Uri` query mutators
|
|
26
|
+
* ({@link Uri.withQuery}, {@link Uri.replaceQuery}, …) and the `url()` service.
|
|
27
|
+
* Values are coerced to strings; arrays become repeated (multi-value) parameters.
|
|
28
|
+
*/
|
|
29
|
+
export type QueryInput = Record<string, string | number | boolean | string[]>;
|
|
30
|
+
|
|
31
|
+
interface UriParts {
|
|
32
|
+
scheme?: string;
|
|
33
|
+
user?: string;
|
|
34
|
+
password?: string;
|
|
35
|
+
host?: string;
|
|
36
|
+
port?: number;
|
|
37
|
+
path: string;
|
|
38
|
+
query: Record<string, QueryValue>;
|
|
39
|
+
fragment?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Read-only view over a URI's query string, returned by `Uri.query()`. */
|
|
43
|
+
export interface UriQueryString {
|
|
44
|
+
/** All query parameters as a plain object. */
|
|
45
|
+
all(): Record<string, QueryValue>;
|
|
46
|
+
/** The first value for a key (or undefined). */
|
|
47
|
+
get(key: string): string | undefined;
|
|
48
|
+
/** Every value for a key as an array. */
|
|
49
|
+
getAll(key: string): string[];
|
|
50
|
+
/** Whether a key is present. */
|
|
51
|
+
has(key: string): boolean;
|
|
52
|
+
/** The encoded query string (without the leading `?`). */
|
|
53
|
+
toString(): string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const SCHEME_RE = /^([a-z][a-z0-9+.-]*):\/\//i;
|
|
57
|
+
|
|
58
|
+
function parseQueryString(search: string): Record<string, QueryValue> {
|
|
59
|
+
const out: Record<string, QueryValue> = {};
|
|
60
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
61
|
+
for (const key of new Set(params.keys())) {
|
|
62
|
+
const values = params.getAll(key);
|
|
63
|
+
out[key] = values.length > 1 ? values : (values[0] ?? "");
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildQueryString(query: Record<string, QueryValue>): string {
|
|
69
|
+
const params = new URLSearchParams();
|
|
70
|
+
for (const [key, value] of Object.entries(query)) {
|
|
71
|
+
if (Array.isArray(value)) for (const v of value) params.append(key, v);
|
|
72
|
+
else params.append(key, value);
|
|
73
|
+
}
|
|
74
|
+
return params.toString();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normaliseQueryInput(input: QueryInput): Record<string, QueryValue> {
|
|
78
|
+
const out: Record<string, QueryValue> = {};
|
|
79
|
+
for (const [key, value] of Object.entries(input)) {
|
|
80
|
+
out[key] = Array.isArray(value) ? value.map(String) : String(value);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The configured application base URL (`config('app.url')`, then `APP_URL`), without a trailing
|
|
87
|
+
* slash. Empty string when neither is set. @internal — shared with the `url()` helper.
|
|
88
|
+
*/
|
|
89
|
+
export function appBaseUrl(): string {
|
|
90
|
+
let base: string | undefined;
|
|
91
|
+
try {
|
|
92
|
+
base = config("app.url" as never) as string | undefined;
|
|
93
|
+
} catch {
|
|
94
|
+
base = undefined;
|
|
95
|
+
}
|
|
96
|
+
base ??= Bun.env.APP_URL;
|
|
97
|
+
return (base ?? "").replace(/\/+$/, "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parse(value: string): UriParts {
|
|
101
|
+
if (SCHEME_RE.test(value)) {
|
|
102
|
+
const u = new URL(value);
|
|
103
|
+
return {
|
|
104
|
+
scheme: u.protocol.replace(/:$/, ""),
|
|
105
|
+
user: u.username ? decodeURIComponent(u.username) : undefined,
|
|
106
|
+
password: u.password ? decodeURIComponent(u.password) : undefined,
|
|
107
|
+
host: u.hostname,
|
|
108
|
+
port: u.port ? Number(u.port) : undefined,
|
|
109
|
+
path: u.pathname,
|
|
110
|
+
query: parseQueryString(u.search),
|
|
111
|
+
fragment: u.hash ? u.hash.slice(1) : undefined,
|
|
112
|
+
} as UriParts;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Relative URI — split off #fragment, then ?query, leaving the path.
|
|
116
|
+
let rest = value;
|
|
117
|
+
let fragment: string | undefined;
|
|
118
|
+
const hashIdx = rest.indexOf("#");
|
|
119
|
+
if (hashIdx >= 0) {
|
|
120
|
+
fragment = rest.slice(hashIdx + 1);
|
|
121
|
+
rest = rest.slice(0, hashIdx);
|
|
122
|
+
}
|
|
123
|
+
let query: Record<string, QueryValue> = {};
|
|
124
|
+
const qIdx = rest.indexOf("?");
|
|
125
|
+
if (qIdx >= 0) {
|
|
126
|
+
query = parseQueryString(rest.slice(qIdx));
|
|
127
|
+
rest = rest.slice(0, qIdx);
|
|
128
|
+
}
|
|
129
|
+
return { path: rest, query, fragment } as UriParts;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Fluent, immutable URI value object. Parse an absolute or relative URL, read its
|
|
134
|
+
* parts, and rewrite scheme/host/path/query/fragment — every mutator returns a
|
|
135
|
+
* NEW `Uri`, so instances are safe to share and reuse.
|
|
136
|
+
*
|
|
137
|
+
* Construct via the {@link uri} helper or the static factories ({@link Uri.of},
|
|
138
|
+
* {@link Uri.to}, {@link Uri.current}, {@link Uri.route}). For app-aware URL
|
|
139
|
+
* generation and signing, see the higher-level {@link Url} / {@link url} service.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* Uri.of("https://example.com/users?page=1")
|
|
143
|
+
* .withQuery({ page: 2, sort: "name" })
|
|
144
|
+
* .withFragment("top")
|
|
145
|
+
* .value(); // "https://example.com/users?page=2&sort=name#top"
|
|
146
|
+
*
|
|
147
|
+
* @throws {Error} from {@link Uri.url} (and, transitively, {@link Uri.current})
|
|
148
|
+
* when called on a relative URI that has no host.
|
|
149
|
+
*/
|
|
150
|
+
export class Uri {
|
|
151
|
+
readonly #parts: UriParts;
|
|
152
|
+
|
|
153
|
+
private constructor(parts: UriParts) {
|
|
154
|
+
this.#parts = parts;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Factories ──────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build a Uri from a string (or clone another Uri).
|
|
161
|
+
* @category Construction
|
|
162
|
+
*/
|
|
163
|
+
static of(value: string | Uri): Uri {
|
|
164
|
+
if (value instanceof Uri) return new Uri({ ...value.#parts, query: { ...value.#parts.query } });
|
|
165
|
+
return new Uri(parse(value));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build an absolute Uri for `path`, relative to the app URL (`config('app.url')`).
|
|
170
|
+
* @category Construction
|
|
171
|
+
*/
|
|
172
|
+
static to(path: string): Uri {
|
|
173
|
+
const base = appBaseUrl();
|
|
174
|
+
const rel = path.startsWith("/") || SCHEME_RE.test(path) ? path : `/${path}`;
|
|
175
|
+
return Uri.of(base && !SCHEME_RE.test(rel) ? `${base}${rel}` : rel);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The current request's full URL.
|
|
180
|
+
* @category Construction
|
|
181
|
+
* @throws {Error} when called outside an active HTTP request.
|
|
182
|
+
*/
|
|
183
|
+
static current(): Uri {
|
|
184
|
+
return Uri.of(RequestContext.get().fullUrl());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build a Uri from a named route + params.
|
|
189
|
+
* @category Construction
|
|
190
|
+
*/
|
|
191
|
+
static route(name: string, params: Record<string, string | number> = {}): Uri {
|
|
192
|
+
return Uri.of(route(name, params));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#clone(patch: Partial<UriParts>): Uri {
|
|
196
|
+
return new Uri({ ...this.#parts, ...patch, query: patch.query ?? { ...this.#parts.query } });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Inspectors ─────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The scheme (e.g. `"https"`), or undefined for a relative URI.
|
|
203
|
+
* @category Components
|
|
204
|
+
*/
|
|
205
|
+
scheme(): string | undefined {
|
|
206
|
+
return this.#parts.scheme;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The decoded userinfo username, or undefined.
|
|
210
|
+
* @category Components
|
|
211
|
+
*/
|
|
212
|
+
user(): string | undefined {
|
|
213
|
+
return this.#parts.user;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The decoded userinfo password, or undefined.
|
|
217
|
+
* @category Components
|
|
218
|
+
*/
|
|
219
|
+
password(): string | undefined {
|
|
220
|
+
return this.#parts.password;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The host (without port), or undefined for a relative URI.
|
|
224
|
+
* @category Components
|
|
225
|
+
*/
|
|
226
|
+
host(): string | undefined {
|
|
227
|
+
return this.#parts.host;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The port number, or undefined when none is set.
|
|
231
|
+
* @category Components
|
|
232
|
+
*/
|
|
233
|
+
port(): number | undefined {
|
|
234
|
+
return this.#parts.port;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The path component (may be empty).
|
|
238
|
+
* @category Components
|
|
239
|
+
*/
|
|
240
|
+
path(): string {
|
|
241
|
+
return this.#parts.path;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The fragment (without the leading `#`), or undefined.
|
|
245
|
+
* @category Components
|
|
246
|
+
*/
|
|
247
|
+
fragment(): string | undefined {
|
|
248
|
+
return this.#parts.fragment;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The assembled absolute URL string.
|
|
252
|
+
* @category Output
|
|
253
|
+
* @throws {Error} when the URI is relative (has no host).
|
|
254
|
+
*/
|
|
255
|
+
url(): string {
|
|
256
|
+
const p = this.#parts;
|
|
257
|
+
if (!p.host) throw new Error("Cannot call Uri.url() on a relative URI");
|
|
258
|
+
let out = `${p.scheme ?? "http"}://`;
|
|
259
|
+
if (p.user) {
|
|
260
|
+
out += encodeURIComponent(p.user);
|
|
261
|
+
if (p.password) out += `:${encodeURIComponent(p.password)}`;
|
|
262
|
+
out += "@";
|
|
263
|
+
}
|
|
264
|
+
out += p.host;
|
|
265
|
+
if (p.port) out += `:${p.port}`;
|
|
266
|
+
out += p.path || "/";
|
|
267
|
+
const qs = buildQueryString(p.query);
|
|
268
|
+
if (qs) out += `?${qs}`;
|
|
269
|
+
if (p.fragment) out += `#${p.fragment}`;
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A read-only view over the query string.
|
|
275
|
+
* @category Query
|
|
276
|
+
*/
|
|
277
|
+
query(): UriQueryString {
|
|
278
|
+
const q = this.#parts.query;
|
|
279
|
+
return {
|
|
280
|
+
all: () => ({ ...q }),
|
|
281
|
+
get: (key) => {
|
|
282
|
+
const v = q[key];
|
|
283
|
+
return Array.isArray(v) ? v[0] : v;
|
|
284
|
+
},
|
|
285
|
+
getAll: (key) => {
|
|
286
|
+
const v = q[key];
|
|
287
|
+
return v === undefined ? [] : Array.isArray(v) ? [...v] : [v];
|
|
288
|
+
},
|
|
289
|
+
has: (key) => key in q,
|
|
290
|
+
toString: () => buildQueryString(q),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Mutators (return a new Uri) ──────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Return a copy with the scheme replaced (trailing `://` is stripped).
|
|
298
|
+
* @category Components
|
|
299
|
+
*/
|
|
300
|
+
withScheme(scheme: string): Uri {
|
|
301
|
+
return this.#clone({ scheme: scheme.replace(/:?\/?\/?$/, "") });
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Return a copy with the userinfo username (and optional password) set.
|
|
305
|
+
* @category Components
|
|
306
|
+
*/
|
|
307
|
+
withUser(user: string | undefined, password?: string): Uri {
|
|
308
|
+
return this.#clone({ user, ...(password !== undefined ? { password } : {}) } as UriParts);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Return a copy with the userinfo password set.
|
|
312
|
+
* @category Components
|
|
313
|
+
*/
|
|
314
|
+
withPassword(password: string | undefined): Uri {
|
|
315
|
+
return this.#clone({ password } as UriParts);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Return a copy with the host replaced.
|
|
319
|
+
* @category Components
|
|
320
|
+
*/
|
|
321
|
+
withHost(host: string): Uri {
|
|
322
|
+
return this.#clone({ host });
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Return a copy with the port set (pass undefined to remove it).
|
|
326
|
+
* @category Components
|
|
327
|
+
*/
|
|
328
|
+
withPort(port: number | undefined): Uri {
|
|
329
|
+
return this.#clone({ port } as UriParts);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Return a copy with the path replaced; a non-empty path is given a leading slash.
|
|
333
|
+
* @category Components
|
|
334
|
+
*/
|
|
335
|
+
withPath(path: string): Uri {
|
|
336
|
+
return this.#clone({ path: path.startsWith("/") || path === "" ? path : `/${path}` });
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Return a copy with the fragment set (pass undefined to remove it).
|
|
340
|
+
* @category Components
|
|
341
|
+
*/
|
|
342
|
+
withFragment(fragment: string | undefined): Uri {
|
|
343
|
+
return this.#clone({ fragment } as UriParts);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Merge the given parameters into the query string (overwriting existing keys).
|
|
348
|
+
* @category Query
|
|
349
|
+
*/
|
|
350
|
+
withQuery(query: QueryInput): Uri {
|
|
351
|
+
return this.#clone({ query: { ...this.#parts.query, ...normaliseQueryInput(query) } });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Add parameters only when their key is not already present.
|
|
356
|
+
* @category Query
|
|
357
|
+
*/
|
|
358
|
+
withQueryIfMissing(query: QueryInput): Uri {
|
|
359
|
+
const merged = { ...this.#parts.query };
|
|
360
|
+
for (const [key, value] of Object.entries(normaliseQueryInput(query))) {
|
|
361
|
+
if (!(key in merged)) merged[key] = value;
|
|
362
|
+
}
|
|
363
|
+
return this.#clone({ query: merged });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Replace the entire query string with the given parameters.
|
|
368
|
+
* @category Query
|
|
369
|
+
*/
|
|
370
|
+
replaceQuery(query: QueryInput): Uri {
|
|
371
|
+
return this.#clone({ query: normaliseQueryInput(query) });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Append a value to a key, turning it into a multi-value parameter.
|
|
376
|
+
* @category Query
|
|
377
|
+
*/
|
|
378
|
+
pushOntoQuery(key: string, value: string | number): Uri {
|
|
379
|
+
const current = this.#parts.query[key];
|
|
380
|
+
const next: QueryValue =
|
|
381
|
+
current === undefined
|
|
382
|
+
? String(value)
|
|
383
|
+
: Array.isArray(current)
|
|
384
|
+
? [...current, String(value)]
|
|
385
|
+
: [current, String(value)];
|
|
386
|
+
return this.#clone({ query: { ...this.#parts.query, [key]: next } });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Remove one or more keys from the query string.
|
|
391
|
+
* @category Query
|
|
392
|
+
*/
|
|
393
|
+
withoutQuery(keys: string[]): Uri {
|
|
394
|
+
const next = { ...this.#parts.query };
|
|
395
|
+
for (const key of keys) delete next[key];
|
|
396
|
+
return this.#clone({ query: next });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Zerotal extras ────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Resolve the URL the user was heading to before authentication (the session's
|
|
403
|
+
* `intended_url`, set by RequireAuth), falling back to `fallback`. Cross-origin stored URLs
|
|
404
|
+
* are rejected (open-redirect guard). Returns a fresh Uri pointing at the resolved target.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* return uri().intended("/dashboard").redirect();
|
|
408
|
+
*
|
|
409
|
+
* @category Construction
|
|
410
|
+
*/
|
|
411
|
+
intended(fallback = "/"): Uri {
|
|
412
|
+
const ctx = RequestContext.tryGet();
|
|
413
|
+
const session = (
|
|
414
|
+
ctx as unknown as { session?: { get<T>(k: string): T | undefined; forget(k: string): void } }
|
|
415
|
+
)?.session;
|
|
416
|
+
const stored = session?.get<string>("intended_url");
|
|
417
|
+
if (stored) session?.forget("intended_url");
|
|
418
|
+
const origin = ctx?.url.origin ?? "";
|
|
419
|
+
return Uri.of(safeRedirectPath(stored, origin) ?? fallback);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Issue a redirect to this URI, returning a {@link ResponseBuilder} for flash chaining.
|
|
424
|
+
* @category Output
|
|
425
|
+
* @throws {Error} when called outside an active HTTP request.
|
|
426
|
+
*/
|
|
427
|
+
redirect(status: 301 | 302 | 303 | 307 | 308 = 302): ResponseBuilder {
|
|
428
|
+
const ctx = RequestContext.get();
|
|
429
|
+
ctx.redirect(this.value(), status);
|
|
430
|
+
return new ResponseBuilder(ctx);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── Output ────────────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The assembled URI string. Unlike {@link Uri.url}, this works for both
|
|
437
|
+
* absolute and relative URIs.
|
|
438
|
+
* @category Output
|
|
439
|
+
*/
|
|
440
|
+
value(): string {
|
|
441
|
+
const p = this.#parts;
|
|
442
|
+
let out = "";
|
|
443
|
+
if (p.host) {
|
|
444
|
+
out += `${p.scheme ?? "http"}://`;
|
|
445
|
+
if (p.user) {
|
|
446
|
+
out += encodeURIComponent(p.user);
|
|
447
|
+
if (p.password) out += `:${encodeURIComponent(p.password)}`;
|
|
448
|
+
out += "@";
|
|
449
|
+
}
|
|
450
|
+
out += p.host;
|
|
451
|
+
if (p.port) out += `:${p.port}`;
|
|
452
|
+
out += p.path || "/";
|
|
453
|
+
} else {
|
|
454
|
+
out += p.path;
|
|
455
|
+
}
|
|
456
|
+
const qs = buildQueryString(p.query);
|
|
457
|
+
if (qs) out += `?${qs}`;
|
|
458
|
+
if (p.fragment) out += `#${p.fragment}`;
|
|
459
|
+
return out;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Alias for {@link Uri.value} — enables string coercion and template literals.
|
|
464
|
+
* @category Output
|
|
465
|
+
*/
|
|
466
|
+
toString(): string {
|
|
467
|
+
return this.value();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Serialize to the URI string when passed to `JSON.stringify`.
|
|
472
|
+
* @category Output
|
|
473
|
+
*/
|
|
474
|
+
toJSON(): string {
|
|
475
|
+
return this.value();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Fluent URI helper. Pass a URL string to build from it, or call with no arguments to start
|
|
481
|
+
* from the current request URL.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* uri("https://example.com/p?x=1").withQuery({ x: 2 }).value();
|
|
485
|
+
* uri().withoutQuery(["page"]).value(); // current URL, page param dropped
|
|
486
|
+
* uri().intended("/dashboard").redirect(); // redirect to the stored intended URL
|
|
487
|
+
*/
|
|
488
|
+
export function uri(value?: string | Uri): Uri {
|
|
489
|
+
return value === undefined ? Uri.current() : Uri.of(value);
|
|
490
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@zerotal/core/http` module — everything for talking HTTP from the
|
|
3
|
+
* server side and shaping HTTP responses. It groups five concerns:
|
|
4
|
+
*
|
|
5
|
+
* - **Outbound requests** — the {@link Http} facade and its fluent
|
|
6
|
+
* {@link PendingRequest} builder for server-to-server calls, with headers,
|
|
7
|
+
* auth, timeouts, retries, and a `fake()` test harness.
|
|
8
|
+
* - **URL building & signing** — the immutable {@link Uri} value object
|
|
9
|
+
* ({@link uri}) and the app-aware {@link Url} service ({@link url}) for
|
|
10
|
+
* fully-qualified, named-route, and HMAC-signed links.
|
|
11
|
+
* - **Uploads** — {@link UploadedFile} for validating and storing multipart
|
|
12
|
+
* form files.
|
|
13
|
+
* - **API resources** — {@link Resource} and {@link ResourceCollection} for
|
|
14
|
+
* transforming models into JSON response envelopes.
|
|
15
|
+
* - **Content negotiation** — {@link negotiate} / {@link detectChannel} for
|
|
16
|
+
* branching a handler by web / JSON / CLI client.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* import { Http, uri } from '@zerotal/core/http';
|
|
20
|
+
*
|
|
21
|
+
* const target = uri('https://api.example.com/users').withQuery({ page: 2 }).value();
|
|
22
|
+
* const users = await Http.withToken(token).timeout(5000).get(target).json();
|
|
23
|
+
*
|
|
24
|
+
* @packageDocumentation
|
|
25
|
+
*/
|
|
26
|
+
export { Uri, uri } from "./Uri.ts";
|
|
27
|
+
export type { UriQueryString, QueryInput } from "./Uri.ts";
|
|
28
|
+
export { url, Url, UrlKeyMissingError } from "./url.ts";
|
|
29
|
+
export type { UrlGenerator } from "./url.ts";
|
|
30
|
+
export { Http } from "./Http.ts";
|
|
31
|
+
export { UploadedFile } from "./UploadedFile.ts";
|
|
32
|
+
export type { StorageDisk, FileValidationOptions } from "./UploadedFile.ts";
|
|
33
|
+
export { HttpClientResponse, HttpClientError, PendingRequest } from "./HttpClient.ts";
|
|
34
|
+
export type { FakeStub } from "./HttpClient.ts";
|
|
35
|
+
export { Resource, ResourceCollection } from "./Resource.ts";
|
|
36
|
+
export type { PaginatedData } from "./Resource.ts";
|
|
37
|
+
export { negotiate } from "./negotiate.ts";
|
|
38
|
+
export type {
|
|
39
|
+
Channel,
|
|
40
|
+
AnsiColor,
|
|
41
|
+
WebContext,
|
|
42
|
+
ApiContext,
|
|
43
|
+
CliContext,
|
|
44
|
+
NegotiateMap,
|
|
45
|
+
} from "./negotiate.ts";
|
|
46
|
+
export { isAllowedOrigin } from "./originGuard.ts";
|