alepha 0.20.2 → 0.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/assets/swagger-ui/swagger-ui.css +1 -1
- package/dist/api/audits/index.browser.js +49 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.js +49 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +2 -61
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +4 -4
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +1 -10
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/parameters/index.browser.js +37 -0
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +12 -68
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +57 -4
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.browser.js +6 -0
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +148 -227
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +60 -14
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js +2 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/bucket/index.d.ts +77 -107
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +153 -5
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +12 -2
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +26 -0
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +11 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +11 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.d.ts +7 -5
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +2 -3
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +637 -11660
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +707 -532
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +4 -8
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js +20 -16
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +51 -77
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +65 -15
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +10 -13
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +30 -12
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.js +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +27 -3
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +8 -11
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +27 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +27 -3
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +27 -3
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.d.ts +69 -10
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +135 -13
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/smtp/index.js +130 -16
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +30 -2
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +35 -12
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.js +32 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +238 -31
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +198 -67
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +2 -362
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +18 -409
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +41 -194
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +27 -422
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +17 -20
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +1 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +17 -20
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/core/index.d.ts +102 -1
- package/dist/react/core/index.d.ts.map +1 -1
- package/dist/react/core/index.js +65 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +6 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +7 -7
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +7 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js +6 -0
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js +22 -17
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js +98 -4
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +58 -5
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +122 -6
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/{chunk-DBEY4PJZ.js → chunk-6Ep1yQYe.js} +1 -1
- package/dist/react/testing/index.js +1 -1
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +195 -1
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/index.js +64 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/react/websocket/index.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +1 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +1 -1
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +2 -2
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +24 -10
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js +10 -3
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +1 -4
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +47 -9
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js +19 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +4 -5
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/websocket/index.browser.js +32 -5
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +3 -1
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +42 -6
- package/dist/websocket/index.js.map +1 -1
- package/package.json +685 -274
- package/src/api/files/__tests__/FileController.spec.ts +1 -1
- package/src/api/jobs/__tests__/$job.spec.ts +5 -1
- package/src/api/parameters/services/ParameterProvider.ts +21 -4
- package/src/api/users/__tests__/SessionService.spec.ts +99 -0
- package/src/api/users/__tests__/UserJobs.spec.ts +67 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +15 -0
- package/src/api/users/entities/sessions.ts +6 -0
- package/src/api/users/jobs/UserJobs.ts +44 -17
- package/src/api/users/providers/RealmProvider.ts +4 -0
- package/src/api/users/schemas/userQuerySchema.ts +0 -1
- package/src/api/users/services/SessionService.ts +27 -0
- package/src/api/users/services/UserService.ts +1 -5
- package/src/api/verifications/__tests__/CodeVerification.spec.ts +14 -0
- package/src/api/verifications/__tests__/LinkVerification.spec.ts +14 -0
- package/src/api/verifications/services/VerificationService.ts +1 -0
- package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +74 -0
- package/src/bucket/index.ts +19 -2
- package/src/bucket/primitives/$bucket.ts +9 -1
- package/src/bucket/providers/CloudflareR2Provider.ts +2 -137
- package/src/bucket/providers/NodeS3BucketProvider.ts +218 -0
- package/src/cache/core/index.ts +29 -0
- package/src/cache/core/primitives/$cache.ts +14 -1
- package/src/cli/config/defineConfig.ts +13 -15
- package/src/cli/core/__tests__/init.spec.ts +214 -7
- package/src/cli/core/commands/init.ts +12 -0
- package/src/cli/core/services/PackageManagerUtils.ts +23 -6
- package/src/cli/core/services/ProjectScaffolder.ts +315 -33
- package/src/cli/core/tasks/BuildCloudflareTask.ts +5 -0
- package/src/cli/core/tasks/BuildDockerTask.ts +9 -10
- package/src/cli/core/tasks/BuildServerTask.ts +8 -0
- package/src/cli/core/templates/agentMd.ts +2 -10
- package/src/cli/core/templates/apiIndexTs.ts +23 -1
- package/src/cli/core/templates/componentsJsonTs.ts +39 -0
- package/src/cli/core/templates/mainCss.ts +1 -0
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +77 -0
- package/src/cli/core/templates/saasAdminPagesTsx.ts +26 -0
- package/src/cli/core/templates/saasAuthLayoutTsx.ts +20 -0
- package/src/cli/core/templates/saasAuthPagesTsx.ts +62 -0
- package/src/cli/core/templates/saasRealmProviderTs.ts +46 -0
- package/src/cli/core/templates/webAppRouterTs.ts +104 -1
- package/src/cli/core/templates/webIndexTs.ts +23 -1
- package/src/cli/devtools/index.ts +12 -26
- package/src/cli/platform/__tests__/SecretsCommand.spec.ts +2 -0
- package/src/cli/platform/index.ts +15 -24
- package/src/cli/vendor/atoms/vendorOptions.ts +1 -1
- package/src/cli/vendor/index.ts +14 -23
- package/src/command/providers/CliProvider.ts +1 -1
- package/src/core/Alepha.ts +11 -1
- package/src/core/helpers/ref.ts +18 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/interfaces/Service.ts +3 -1
- package/src/core/providers/SchemaValidator.ts +9 -1
- package/src/core/providers/TypeProvider.ts +2 -3
- package/src/datetime/REFACTORING.md +118 -0
- package/src/datetime/providers/DateTimeProvider.ts +203 -24
- package/src/lock/core/index.ts +31 -0
- package/src/lock/core/primitives/$lock.ts +14 -1
- package/src/logger/services/Logger.ts +1 -1
- package/src/mcp/__tests__/$resource.spec.ts +1 -1
- package/src/mcp/__tests__/$tool.spec.ts +1 -1
- package/src/mcp/__tests__/McpServerProvider.spec.ts +1 -1
- package/src/mcp/__tests__/jsonrpc.spec.ts +1 -1
- package/src/mcp/helpers/jsonrpc.ts +26 -1
- package/src/mcp/index.ts +10 -5
- package/src/mcp/interfaces/McpTypes.ts +83 -6
- package/src/mcp/primitives/$prompt.ts +18 -1
- package/src/mcp/primitives/$resource.ts +18 -1
- package/src/mcp/primitives/$tool.ts +83 -7
- package/src/mcp/providers/McpServerProvider.ts +74 -16
- package/src/mcp/transports/StreamableHttpMcpTransport.ts +226 -0
- package/src/orm/REFACTORING.md +330 -0
- package/src/orm/__tests__/$repository-tests.ts +1 -0
- package/src/orm/__tests__/orm-next-tests.ts +2 -67
- package/src/orm/__tests__/orm-next.spec.ts +0 -21
- package/src/orm/core/index.shared.ts +0 -2
- package/src/orm/core/index.ts +1 -2
- package/src/orm/core/primitives/$repository.ts +3 -6
- package/src/orm/core/primitives/$transactional.ts +11 -0
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +0 -5
- package/src/orm/core/providers/drivers/NodeSqliteProvider.ts +11 -13
- package/src/orm/core/schemas/updateSchema.ts +1 -1
- package/src/orm/core/services/ModelBuilder.ts +1 -13
- package/src/orm/core/services/PgRelationManager.ts +4 -2
- package/src/orm/core/services/Repository.ts +1 -42
- package/src/orm/core/services/SqliteModelBuilder.ts +2 -33
- package/src/orm/postgres/services/PostgresModelBuilder.ts +10 -45
- package/src/react/core/__tests__/useQuery.browser.spec.tsx +86 -0
- package/src/react/core/hooks/useQuery.ts +153 -0
- package/src/react/core/index.ts +1 -0
- package/src/react/form/services/FormModel.ts +15 -6
- package/src/react/form/services/parseField.ts +8 -0
- package/src/react/i18n/providers/I18nProvider.ts +8 -2
- package/src/react/intro/components/GettingStartedAuthSlide.tsx +11 -4
- package/src/react/router/__tests__/$page.spec.tsx +0 -16
- package/src/react/router/__tests__/ReactBrowserProvider.browser.spec.ts +213 -2
- package/src/react/router/__tests__/ssr.spec.tsx +339 -0
- package/src/react/router/primitives/$page.ts +28 -4
- package/src/react/router/providers/ReactBrowserProvider.ts +73 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +1 -1
- package/src/react/router/providers/ReactPageProvider.ts +27 -9
- package/src/react/router/providers/ReactPreloadProvider.ts +1 -1
- package/src/react/router/providers/ReactServerProvider.ts +1 -0
- package/src/react/ui/atoms/uiThemeListAtom.ts +36 -0
- package/src/react/ui/index.ts +6 -0
- package/src/react/ui/services/SchemaControl.ts +209 -0
- package/src/scheduler/providers/CronProvider.ts +1 -1
- package/src/security/primitives/$basicAuth.ts +1 -1
- package/src/security/primitives/$issuer.ts +6 -3
- package/src/server/auth/providers/ServerAuthProvider.ts +5 -1
- package/src/server/core/__tests__/ServerRouterProvider-serializationError.spec.ts +75 -0
- package/src/server/core/__tests__/ServerRouterProvider-validationError.spec.ts +306 -0
- package/src/server/core/errors/ValidationError.ts +13 -1
- package/src/server/core/interfaces/ServerRequest.ts +1 -0
- package/src/server/core/primitives/$action.ts +16 -5
- package/src/server/core/providers/ServerProvider.ts +1 -1
- package/src/server/core/providers/ServerRouterProvider.ts +28 -6
- package/src/server/core/services/HttpClient.ts +1 -1
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +6 -8
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +10 -4
- package/src/websocket/services/WebSocketClient.ts +11 -5
- package/src/mcp/transports/SseMcpTransport.ts +0 -182
- package/src/orm/core/__tests__/parseQueryString.spec.ts +0 -196
- package/src/orm/core/helpers/parseQueryString.ts +0 -502
- package/src/orm/core/primitives/$view.ts +0 -88
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { FormModel } from "alepha/react/form";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema-bound metadata read by `<Control>` (in `@alepha/ui-registry`) to
|
|
5
|
+
* configure how a field renders. Place under `$control` on any TypeBox
|
|
6
|
+
* schema option.
|
|
7
|
+
*
|
|
8
|
+
* Two forms:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Object** — static configuration baked into the schema:
|
|
11
|
+
* ```ts
|
|
12
|
+
* t.string({ $control: { password: true, icon: "key" } })
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* 2. **Function** — dynamic, computed from current form state:
|
|
16
|
+
* ```ts
|
|
17
|
+
* t.string({
|
|
18
|
+
* $control: ({ form, value }) => {
|
|
19
|
+
* if (form.currentValues.kind !== "advanced") return false; // hide
|
|
20
|
+
* return { items: () => fetchOptions(form.currentValues.kind) };
|
|
21
|
+
* },
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* The function may return:
|
|
26
|
+
* - a partial `SchemaControl` to merge with explicit `<Control>` props
|
|
27
|
+
* - `false` to hide the control entirely
|
|
28
|
+
* - `undefined` to leave the field as-is
|
|
29
|
+
*/
|
|
30
|
+
export interface SchemaControl {
|
|
31
|
+
// ── Variant forcing ────────────────────────────────────────────────
|
|
32
|
+
text?: boolean;
|
|
33
|
+
area?: boolean;
|
|
34
|
+
password?: boolean;
|
|
35
|
+
switch?: boolean;
|
|
36
|
+
number?: boolean;
|
|
37
|
+
file?: boolean;
|
|
38
|
+
date?: boolean;
|
|
39
|
+
datetime?: boolean;
|
|
40
|
+
time?: boolean;
|
|
41
|
+
select?: boolean;
|
|
42
|
+
combobox?: boolean;
|
|
43
|
+
segmented?: boolean;
|
|
44
|
+
slider?: boolean;
|
|
45
|
+
object?: boolean;
|
|
46
|
+
array?: boolean;
|
|
47
|
+
|
|
48
|
+
// ── Labels / hints ────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Icon name. The registry control maps this to its icon set
|
|
51
|
+
* (lucide-react). Pass `null` to suppress the schema-inferred icon.
|
|
52
|
+
*/
|
|
53
|
+
icon?: string | null;
|
|
54
|
+
label?: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
placeholder?: string;
|
|
57
|
+
/**
|
|
58
|
+
* HTML `autocomplete` attribute. Use standard tokens like
|
|
59
|
+
* `"username"`, `"email"`, `"new-password"`, `"current-password"`,
|
|
60
|
+
* `"street-address"`, `"address-line1"`, `"address-level2"` (city),
|
|
61
|
+
* `"postal-code"`, `"country"`, `"cc-number"`, `"cc-exp"`,
|
|
62
|
+
* `"cc-csc"`, `"cc-name"`, `"tel"`, etc.
|
|
63
|
+
*/
|
|
64
|
+
autoComplete?: string;
|
|
65
|
+
|
|
66
|
+
// ── Data ──────────────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Static or async option list for select / combobox / multi-select.
|
|
69
|
+
* Each item is either a bare string (used as both value & label) or a
|
|
70
|
+
* `{ value, label, description?, tag? }` object.
|
|
71
|
+
*/
|
|
72
|
+
items?: Array<string | SchemaControlItem> | SchemaControlItemsFn;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Re-fetch `items` (when async) whenever any of these reference values
|
|
76
|
+
* change. Useful for cascading selects.
|
|
77
|
+
*/
|
|
78
|
+
itemsWatch?: unknown[];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Allow the user to create a new option by typing into a select /
|
|
82
|
+
* multi-select. Pass `true` for `{ value: query, label: query }`, or a
|
|
83
|
+
* function returning a custom option built from the query.
|
|
84
|
+
*/
|
|
85
|
+
createNewEntry?:
|
|
86
|
+
| boolean
|
|
87
|
+
| ((query: string) => { value: string; label: string });
|
|
88
|
+
|
|
89
|
+
// ── Layout ────────────────────────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Width slot inside an `<AutoForm>` group. Mapped to a grid column span.
|
|
92
|
+
* - `100` → full row
|
|
93
|
+
* - `75` → 3/4 row
|
|
94
|
+
* - `66` → 2/3 row
|
|
95
|
+
* - `50` → half
|
|
96
|
+
* - `33` → one third (default for plain primitives)
|
|
97
|
+
* - `25` → one quarter
|
|
98
|
+
*/
|
|
99
|
+
width?: 100 | 75 | 66 | 50 | 33 | 25;
|
|
100
|
+
|
|
101
|
+
// ── Behavior ─────────────────────────────────────────────────────
|
|
102
|
+
/**
|
|
103
|
+
* Render `null` (hide) when truthy. Equivalent to a function `$control`
|
|
104
|
+
* returning `false`, but available as a static value.
|
|
105
|
+
*/
|
|
106
|
+
hidden?: boolean;
|
|
107
|
+
disabled?: boolean;
|
|
108
|
+
readOnly?: boolean;
|
|
109
|
+
|
|
110
|
+
// ── Slots ─────────────────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* Render before/after the field. Both receive the resolved input.
|
|
113
|
+
* Typed loosely — UI layer narrows to `ReactNode`.
|
|
114
|
+
*/
|
|
115
|
+
top?: unknown;
|
|
116
|
+
bottom?: unknown;
|
|
117
|
+
|
|
118
|
+
// ── File upload ───────────────────────────────────────────────────
|
|
119
|
+
/**
|
|
120
|
+
* Render a managed upload control (image preview, multi, drag-drop)
|
|
121
|
+
* that posts to the file API and stores the resulting file ID(s) in
|
|
122
|
+
* the form. Pass `true` for defaults or an options object:
|
|
123
|
+
*
|
|
124
|
+
* ```ts
|
|
125
|
+
* $control: { upload: { multi: true, accept: "image/*", maxSize: 5_000_000 } }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
upload?:
|
|
129
|
+
| boolean
|
|
130
|
+
| {
|
|
131
|
+
multi?: boolean;
|
|
132
|
+
accept?: string;
|
|
133
|
+
maxSize?: number;
|
|
134
|
+
bucket?: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ── Array specifics ───────────────────────────────────────────────
|
|
138
|
+
arrayProps?: {
|
|
139
|
+
confirmDelete?: boolean | { title?: string; message?: string };
|
|
140
|
+
/** Computed label for each tab when an array uses tabs mode. */
|
|
141
|
+
renderTabName?: (i: number, value: unknown) => string;
|
|
142
|
+
sortable?: boolean;
|
|
143
|
+
collapsible?: boolean;
|
|
144
|
+
/** Force grouped (CreateForm-style) tabs even for short arrays. */
|
|
145
|
+
forceTabs?: boolean;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// ── Open extension ────────────────────────────────────────────────
|
|
149
|
+
[key: string]: unknown;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface SchemaControlItem {
|
|
153
|
+
value: string | number | boolean;
|
|
154
|
+
label: string;
|
|
155
|
+
description?: string;
|
|
156
|
+
tag?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export type SchemaControlItemsFn = (
|
|
160
|
+
query: string,
|
|
161
|
+
) =>
|
|
162
|
+
| Array<string | SchemaControlItem>
|
|
163
|
+
| Promise<Array<string | SchemaControlItem>>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Function form of `$control`. Receives the live form model + the current
|
|
167
|
+
* field value, and returns a partial config (merged with explicit props),
|
|
168
|
+
* `false` to hide, or `undefined` to leave as-is.
|
|
169
|
+
*/
|
|
170
|
+
export type SchemaControlFn = (context: {
|
|
171
|
+
form: FormModel<any>;
|
|
172
|
+
value: unknown;
|
|
173
|
+
}) => Partial<SchemaControl> | false | undefined;
|
|
174
|
+
|
|
175
|
+
export type SchemaControlOption = SchemaControl | SchemaControlFn;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a raw `$control` value (object or function) into a concrete
|
|
179
|
+
* partial config. Returns `null` when the field should be hidden.
|
|
180
|
+
*/
|
|
181
|
+
export const resolveSchemaControl = (
|
|
182
|
+
raw: unknown,
|
|
183
|
+
context: { form: FormModel<any>; value: unknown },
|
|
184
|
+
): Partial<SchemaControl> | null => {
|
|
185
|
+
if (raw == null) return {};
|
|
186
|
+
if (typeof raw === "function") {
|
|
187
|
+
const result = (raw as SchemaControlFn)(context);
|
|
188
|
+
if (result === false) return null;
|
|
189
|
+
if (!result) return {};
|
|
190
|
+
if (result.hidden) return null;
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
if (typeof raw === "object") {
|
|
194
|
+
const obj = raw as SchemaControl;
|
|
195
|
+
if (obj.hidden) return null;
|
|
196
|
+
return obj;
|
|
197
|
+
}
|
|
198
|
+
return {};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
declare module "typebox" {
|
|
202
|
+
interface TSchemaOptions {
|
|
203
|
+
/**
|
|
204
|
+
* UI metadata read by `<Control>` from `@alepha/ui-registry`. See
|
|
205
|
+
* {@link SchemaControl}.
|
|
206
|
+
*/
|
|
207
|
+
$control?: SchemaControlOption;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -39,7 +39,7 @@ export function $basicAuth(options: BasicAuthOptions): Middleware {
|
|
|
39
39
|
|
|
40
40
|
const authHeader = request.headers.authorization;
|
|
41
41
|
|
|
42
|
-
if (!authHeader
|
|
42
|
+
if (!authHeader?.startsWith("Basic ")) {
|
|
43
43
|
sendAuthRequired(request);
|
|
44
44
|
throw new HttpError({
|
|
45
45
|
status: 401,
|
|
@@ -75,9 +75,12 @@ export interface IssuerSettings {
|
|
|
75
75
|
*/
|
|
76
76
|
expiration?: DurationLike;
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Idle invalidation is enforced by session-backed realms via
|
|
80
|
+
* `realmAuthSettings.refreshToken.expirationIdle` (in `alepha/api/users`).
|
|
81
|
+
* Token-only refresh (no `onRefreshSession`) does not support idle
|
|
82
|
+
* invalidation — it has no stateful row to track `lastUsedAt`.
|
|
83
|
+
*/
|
|
81
84
|
};
|
|
82
85
|
|
|
83
86
|
onCreateSession?: (
|
|
@@ -114,7 +114,7 @@ export class ServerAuthProvider {
|
|
|
114
114
|
// [feature] support for auth providers with fallback
|
|
115
115
|
if (!request.headers.authorization) {
|
|
116
116
|
for (const provider of this.identities) {
|
|
117
|
-
if ("fallback" in provider.options &&
|
|
117
|
+
if ("fallback" in provider.options && provider.options.fallback) {
|
|
118
118
|
const token = await provider.options.fallback();
|
|
119
119
|
if (token) {
|
|
120
120
|
request.headers.authorization = `Bearer ${token}`;
|
|
@@ -336,10 +336,12 @@ export class ServerAuthProvider {
|
|
|
336
336
|
parameters.scope = scope;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
// biome-ignore lint/complexity/useOptionalChain: oidc is `false | OidcOptions`; optional chaining doesn't narrow `false`
|
|
339
340
|
if (oidc && oidc.responseMode) {
|
|
340
341
|
parameters.response_mode = oidc.responseMode;
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
// biome-ignore lint/complexity/useOptionalChain: oidc is `false | OidcOptions`; optional chaining doesn't narrow `false`
|
|
343
345
|
if (oidc && oidc.authorizationParameters) {
|
|
344
346
|
Object.assign(parameters, oidc.authorizationParameters);
|
|
345
347
|
}
|
|
@@ -381,10 +383,12 @@ export class ServerAuthProvider {
|
|
|
381
383
|
parameters.scope = scope;
|
|
382
384
|
}
|
|
383
385
|
|
|
386
|
+
// biome-ignore lint/complexity/useOptionalChain: oidc is `false | OidcOptions`; optional chaining doesn't narrow `false`
|
|
384
387
|
if (oidc && oidc.responseMode) {
|
|
385
388
|
parameters.response_mode = oidc.responseMode;
|
|
386
389
|
}
|
|
387
390
|
|
|
391
|
+
// biome-ignore lint/complexity/useOptionalChain: oidc is `false | OidcOptions`; optional chaining doesn't narrow `false`
|
|
388
392
|
if (oidc && oidc.authorizationParameters) {
|
|
389
393
|
Object.assign(parameters, oidc.authorizationParameters);
|
|
390
394
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Alepha, t } from "alepha";
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { $action, ServerProvider } from "../index.ts";
|
|
4
|
+
|
|
5
|
+
class TestApp {
|
|
6
|
+
badResponse = $action({
|
|
7
|
+
schema: {
|
|
8
|
+
response: t.object({
|
|
9
|
+
name: t.text(),
|
|
10
|
+
age: t.integer(),
|
|
11
|
+
}),
|
|
12
|
+
},
|
|
13
|
+
handler: () =>
|
|
14
|
+
({
|
|
15
|
+
// missing required `age` → response codec.encode will throw
|
|
16
|
+
name: "John",
|
|
17
|
+
secretToken: "leaked-token-xyz",
|
|
18
|
+
}) as any,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("ServerRouterProvider - response serialization error", () => {
|
|
23
|
+
describe("in production mode", () => {
|
|
24
|
+
it("should not leak validation details or response payload in 500 response", async ({
|
|
25
|
+
expect,
|
|
26
|
+
}) => {
|
|
27
|
+
const alepha = Alepha.create({
|
|
28
|
+
env: { NODE_ENV: "production", SERVER_PORT: 0 },
|
|
29
|
+
}).with(TestApp);
|
|
30
|
+
|
|
31
|
+
await alepha.start();
|
|
32
|
+
const hostname = alepha.inject(ServerProvider).hostname;
|
|
33
|
+
|
|
34
|
+
const response = await fetch(`${hostname}/api/badResponse`);
|
|
35
|
+
const json = await response.json();
|
|
36
|
+
|
|
37
|
+
expect(response.status).toBe(500);
|
|
38
|
+
expect(json.error).toBe("InternalServerError");
|
|
39
|
+
expect(json.message).toBe("Internal Server Error");
|
|
40
|
+
|
|
41
|
+
// payload from the handler must not leak through the error
|
|
42
|
+
const dump = JSON.stringify(json);
|
|
43
|
+
expect(dump).not.toContain("leaked-token-xyz");
|
|
44
|
+
expect(dump).not.toContain("secretToken");
|
|
45
|
+
|
|
46
|
+
await alepha.stop();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("in development mode", () => {
|
|
51
|
+
it("should include explicit validation details in 500 response", async ({
|
|
52
|
+
expect,
|
|
53
|
+
}) => {
|
|
54
|
+
const alepha = Alepha.create().with(TestApp);
|
|
55
|
+
|
|
56
|
+
await alepha.start();
|
|
57
|
+
const hostname = alepha.inject(ServerProvider).hostname;
|
|
58
|
+
|
|
59
|
+
const response = await fetch(`${hostname}/api/badResponse`);
|
|
60
|
+
const json = await response.json();
|
|
61
|
+
|
|
62
|
+
expect(response.status).toBe(500);
|
|
63
|
+
expect(json.error).toBe("InternalServerError");
|
|
64
|
+
|
|
65
|
+
// dev mode must surface enough context to debug the schema mismatch
|
|
66
|
+
expect(json.message).not.toBe("Internal Server Error");
|
|
67
|
+
expect(json.message.toLowerCase()).toMatch(/age|required|expected/);
|
|
68
|
+
|
|
69
|
+
// even in dev, the raw handler payload (potential secrets) must not be echoed
|
|
70
|
+
expect(json.message).not.toContain("leaked-token-xyz");
|
|
71
|
+
|
|
72
|
+
await alepha.stop();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { Alepha, t } from "alepha";
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { $route, ServerProvider } from "../index.ts";
|
|
4
|
+
|
|
5
|
+
class TestApp {
|
|
6
|
+
createUser = $route({
|
|
7
|
+
method: "POST",
|
|
8
|
+
path: "/users",
|
|
9
|
+
schema: {
|
|
10
|
+
body: t.object({
|
|
11
|
+
name: t.text(),
|
|
12
|
+
age: t.integer(),
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
handler: () => {},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
getUser = $route({
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/users/:id",
|
|
21
|
+
schema: {
|
|
22
|
+
params: t.object({
|
|
23
|
+
id: t.integer(),
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
handler: () => {},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
searchUsers = $route({
|
|
30
|
+
method: "GET",
|
|
31
|
+
path: "/users",
|
|
32
|
+
schema: {
|
|
33
|
+
query: t.object({
|
|
34
|
+
limit: t.integer({ minimum: 1, maximum: 100 }),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
handler: () => {},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
protectedRoute = $route({
|
|
41
|
+
method: "GET",
|
|
42
|
+
path: "/protected",
|
|
43
|
+
schema: {
|
|
44
|
+
headers: t.object({
|
|
45
|
+
"x-api-version": t.integer(),
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
handler: () => {},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fetchAndStop = async (
|
|
53
|
+
alepha: Alepha,
|
|
54
|
+
build: (hostname: string) => { url: string; init?: RequestInit },
|
|
55
|
+
) => {
|
|
56
|
+
await alepha.start();
|
|
57
|
+
const hostname = alepha.inject(ServerProvider).hostname;
|
|
58
|
+
const { url, init } = build(hostname);
|
|
59
|
+
const response = await fetch(url, init);
|
|
60
|
+
const json = await response.json();
|
|
61
|
+
await alepha.stop();
|
|
62
|
+
return { response, json };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe("ServerRouterProvider - request validation error", () => {
|
|
66
|
+
describe("body validation", () => {
|
|
67
|
+
it("should expose a wrong-type rejection (expected integer, got string)", async ({
|
|
68
|
+
expect,
|
|
69
|
+
}) => {
|
|
70
|
+
const alepha = Alepha.create({
|
|
71
|
+
env: { NODE_ENV: "production", SERVER_PORT: 0 },
|
|
72
|
+
}).with(TestApp);
|
|
73
|
+
|
|
74
|
+
const { response, json } = await fetchAndStop(alepha, (host) => ({
|
|
75
|
+
url: `${host}/users`,
|
|
76
|
+
init: {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "content-type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ name: "John", age: "not-a-number" }),
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
expect(response.status).toBe(400);
|
|
84
|
+
expect(json.error).toBe("ValidationError");
|
|
85
|
+
expect(json.message).toMatch(/^Invalid request body:/);
|
|
86
|
+
expect(json.message.toLowerCase()).toMatch(/integer|expected/);
|
|
87
|
+
// path locates the offending field
|
|
88
|
+
expect(json.details).toBe("/age");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should expose a missing-field rejection (required property `age`)", async ({
|
|
92
|
+
expect,
|
|
93
|
+
}) => {
|
|
94
|
+
const alepha = Alepha.create({
|
|
95
|
+
env: { NODE_ENV: "production", SERVER_PORT: 0 },
|
|
96
|
+
}).with(TestApp);
|
|
97
|
+
|
|
98
|
+
const { response, json } = await fetchAndStop(alepha, (host) => ({
|
|
99
|
+
url: `${host}/users`,
|
|
100
|
+
init: {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "content-type": "application/json" },
|
|
103
|
+
body: JSON.stringify({ name: "John" }),
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
expect(response.status).toBe(400);
|
|
108
|
+
expect(json.error).toBe("ValidationError");
|
|
109
|
+
expect(json.message).toMatch(/^Invalid request body:/);
|
|
110
|
+
// message must name the missing field
|
|
111
|
+
expect(json.message).toContain("age");
|
|
112
|
+
expect(json.message.toLowerCase()).toMatch(/required/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should behave identically in development mode", async ({ expect }) => {
|
|
116
|
+
const alepha = Alepha.create().with(TestApp);
|
|
117
|
+
|
|
118
|
+
const { response, json } = await fetchAndStop(alepha, (host) => ({
|
|
119
|
+
url: `${host}/users`,
|
|
120
|
+
init: {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "content-type": "application/json" },
|
|
123
|
+
body: JSON.stringify({ name: "John" }),
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
expect(response.status).toBe(400);
|
|
128
|
+
expect(json.error).toBe("ValidationError");
|
|
129
|
+
expect(json.message).toMatch(/^Invalid request body:/);
|
|
130
|
+
expect(json.message.toLowerCase()).toMatch(/age|required/);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("params validation", () => {
|
|
135
|
+
it("should expose the rejection reason in production", async ({
|
|
136
|
+
expect,
|
|
137
|
+
}) => {
|
|
138
|
+
const alepha = Alepha.create({
|
|
139
|
+
env: { NODE_ENV: "production", SERVER_PORT: 0 },
|
|
140
|
+
}).with(TestApp);
|
|
141
|
+
|
|
142
|
+
const { response, json } = await fetchAndStop(alepha, (host) => ({
|
|
143
|
+
url: `${host}/users/not-a-number`,
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
expect(response.status).toBe(400);
|
|
147
|
+
expect(json.error).toBe("ValidationError");
|
|
148
|
+
expect(json.message).toMatch(/^Invalid request params:/);
|
|
149
|
+
expect(json.message.toLowerCase()).toMatch(/integer|number|expected/);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("query validation", () => {
|
|
154
|
+
it("should expose the rejection reason in production", async ({
|
|
155
|
+
expect,
|
|
156
|
+
}) => {
|
|
157
|
+
const alepha = Alepha.create({
|
|
158
|
+
env: { NODE_ENV: "production", SERVER_PORT: 0 },
|
|
159
|
+
}).with(TestApp);
|
|
160
|
+
|
|
161
|
+
const { response, json } = await fetchAndStop(alepha, (host) => ({
|
|
162
|
+
url: `${host}/users?limit=9999`,
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
expect(response.status).toBe(400);
|
|
166
|
+
expect(json.error).toBe("ValidationError");
|
|
167
|
+
expect(json.message).toMatch(/^Invalid request query:/);
|
|
168
|
+
expect(json.message.toLowerCase()).toMatch(/maximum|less|<=|100/);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("header validation", () => {
|
|
173
|
+
it("should expose the rejection reason in production", async ({
|
|
174
|
+
expect,
|
|
175
|
+
}) => {
|
|
176
|
+
const alepha = Alepha.create({
|
|
177
|
+
env: { NODE_ENV: "production", SERVER_PORT: 0 },
|
|
178
|
+
}).with(TestApp);
|
|
179
|
+
|
|
180
|
+
const { response, json } = await fetchAndStop(alepha, (host) => ({
|
|
181
|
+
url: `${host}/protected`,
|
|
182
|
+
// omit required header
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
expect(response.status).toBe(400);
|
|
186
|
+
expect(json.error).toBe("ValidationError");
|
|
187
|
+
expect(json.message).toMatch(/^Invalid request header:/);
|
|
188
|
+
expect(json.message.toLowerCase()).toMatch(/x-api-version|required/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should coerce string header values to declared schema types", async ({
|
|
192
|
+
expect,
|
|
193
|
+
}) => {
|
|
194
|
+
const seen: { version?: unknown } = {};
|
|
195
|
+
class CoerceApp {
|
|
196
|
+
captured = $route({
|
|
197
|
+
method: "GET",
|
|
198
|
+
path: "/coerce",
|
|
199
|
+
schema: {
|
|
200
|
+
headers: t.object({
|
|
201
|
+
"x-api-version": t.integer(),
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
handler: ({ headers }) => {
|
|
205
|
+
seen.version = headers["x-api-version"];
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const alepha = Alepha.create({
|
|
211
|
+
env: { NODE_ENV: "production", SERVER_PORT: 0 },
|
|
212
|
+
}).with(CoerceApp);
|
|
213
|
+
|
|
214
|
+
await alepha.start();
|
|
215
|
+
const host = alepha.inject(ServerProvider).hostname;
|
|
216
|
+
const response = await fetch(`${host}/coerce`, {
|
|
217
|
+
headers: { "x-api-version": "42" },
|
|
218
|
+
});
|
|
219
|
+
await alepha.stop();
|
|
220
|
+
|
|
221
|
+
expect([200, 204]).toContain(response.status);
|
|
222
|
+
expect(seen.version).toBe(42);
|
|
223
|
+
expect(typeof seen.version).toBe("number");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should preserve undeclared headers (auth, user-agent, ...)", async ({
|
|
227
|
+
expect,
|
|
228
|
+
}) => {
|
|
229
|
+
const seen: { authorization?: string; userAgent?: string } = {};
|
|
230
|
+
class PreserveApp {
|
|
231
|
+
check = $route({
|
|
232
|
+
method: "GET",
|
|
233
|
+
path: "/preserve",
|
|
234
|
+
schema: {
|
|
235
|
+
headers: t.object({
|
|
236
|
+
"x-api-version": t.integer(),
|
|
237
|
+
}),
|
|
238
|
+
},
|
|
239
|
+
handler: ({ headers }) => {
|
|
240
|
+
const all = headers as unknown as Record<string, string>;
|
|
241
|
+
seen.authorization = all.authorization;
|
|
242
|
+
seen.userAgent = all["user-agent"];
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const alepha = Alepha.create({
|
|
248
|
+
env: { NODE_ENV: "production", SERVER_PORT: 0 },
|
|
249
|
+
}).with(PreserveApp);
|
|
250
|
+
|
|
251
|
+
await alepha.start();
|
|
252
|
+
const host = alepha.inject(ServerProvider).hostname;
|
|
253
|
+
const response = await fetch(`${host}/preserve`, {
|
|
254
|
+
headers: {
|
|
255
|
+
"x-api-version": "1",
|
|
256
|
+
authorization: "Bearer abc.def.ghi",
|
|
257
|
+
"user-agent": "MyClient/1.0",
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
await alepha.stop();
|
|
261
|
+
|
|
262
|
+
expect([200, 204]).toContain(response.status);
|
|
263
|
+
expect(seen.authorization).toBe("Bearer abc.def.ghi");
|
|
264
|
+
expect(seen.userAgent).toBe("MyClient/1.0");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should accept schema keys regardless of case (lowercase normalization)", async ({
|
|
268
|
+
expect,
|
|
269
|
+
}) => {
|
|
270
|
+
const seen: { version?: unknown } = {};
|
|
271
|
+
class CaseApp {
|
|
272
|
+
// Schema declares keys with mixed case — Node lowercases incoming
|
|
273
|
+
// header names, so the framework must lowercase schema keys when
|
|
274
|
+
// matching values.
|
|
275
|
+
check = $route({
|
|
276
|
+
method: "GET",
|
|
277
|
+
path: "/case",
|
|
278
|
+
schema: {
|
|
279
|
+
headers: t.object({
|
|
280
|
+
"X-Api-Version": t.integer(),
|
|
281
|
+
}),
|
|
282
|
+
},
|
|
283
|
+
handler: ({ headers }) => {
|
|
284
|
+
seen.version = (headers as Record<string, unknown>)[
|
|
285
|
+
"x-api-version"
|
|
286
|
+
];
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const alepha = Alepha.create({
|
|
292
|
+
env: { NODE_ENV: "production", SERVER_PORT: 0 },
|
|
293
|
+
}).with(CaseApp);
|
|
294
|
+
|
|
295
|
+
await alepha.start();
|
|
296
|
+
const host = alepha.inject(ServerProvider).hostname;
|
|
297
|
+
const response = await fetch(`${host}/case`, {
|
|
298
|
+
headers: { "x-api-version": "7" },
|
|
299
|
+
});
|
|
300
|
+
await alepha.stop();
|
|
301
|
+
|
|
302
|
+
expect([200, 204]).toContain(response.status);
|
|
303
|
+
expect(seen.version).toBe(7);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
+
import { TypeBoxError } from "alepha";
|
|
1
2
|
import { HttpError } from "./HttpError.ts";
|
|
2
3
|
|
|
3
4
|
export class ValidationError extends HttpError {
|
|
4
5
|
constructor(message = "Validation has failed", cause?: unknown) {
|
|
6
|
+
let fullMessage = message;
|
|
7
|
+
let details: string | undefined;
|
|
8
|
+
|
|
9
|
+
if (cause instanceof TypeBoxError) {
|
|
10
|
+
fullMessage = `${message}: ${cause.cause.message}`;
|
|
11
|
+
if (cause.cause.instancePath) {
|
|
12
|
+
details = cause.cause.instancePath;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
super(
|
|
6
17
|
{
|
|
7
|
-
message,
|
|
18
|
+
message: fullMessage,
|
|
8
19
|
status: 400,
|
|
20
|
+
details,
|
|
9
21
|
},
|
|
10
22
|
cause,
|
|
11
23
|
);
|
|
@@ -212,6 +212,7 @@ export type ResponseKind = "json" | "text" | "void" | "file" | "any";
|
|
|
212
212
|
|
|
213
213
|
export type ResponseBodyType =
|
|
214
214
|
// not: object is not allowed, you want object ? add schema !
|
|
215
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: handlers may return void (no return statement)
|
|
215
216
|
string | Buffer | StreamLike | undefined | null | void;
|
|
216
217
|
|
|
217
218
|
export type ServerHandler<
|