alepha 0.20.3 → 0.20.5
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/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/jobs/index.d.ts +14 -14
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +6 -1
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +20 -4
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts.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 +5032 -134
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +58 -10
- package/dist/api/users/index.js.map +1 -1
- package/dist/bin/index.js +0 -0
- package/dist/bucket/index.d.ts +77 -107
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +148 -4
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +7 -1
- 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/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 +419 -12
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +22 -511
- 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 +13 -15
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +10 -13
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +18 -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 +16 -13
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts +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 +6 -3
- 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/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/smtp/index.js +10636 -2
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.d.ts +8085 -4
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +33554 -3
- 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/mcp/index.d.ts +238 -31
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +198 -71
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +1 -1
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +4 -3
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +4877 -9
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +4 -3
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +608 -1
- package/dist/orm/postgres/index.d.ts.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/router/index.browser.js +20 -2
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +36 -4
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +20 -2
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/chunk-6Ep1yQYe.js +16 -0
- package/dist/react/testing/index.d.ts +411 -1
- package/dist/react/testing/index.d.ts.map +1 -1
- package/dist/react/testing/index.js +12293 -13
- 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 +61 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +84 -3
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +390 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +390 -1
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.d.ts +325 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1361 -2
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +1054 -1
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +1223 -1
- package/dist/server/auth/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.map +1 -1
- package/dist/server/core/index.js +28 -5
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +514 -1
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +4374 -4
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +3 -4
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/websocket/index.browser.js +11 -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 +21 -6
- package/dist/websocket/index.js.map +1 -1
- package/package.json +416 -8
- 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/services/SessionService.ts +27 -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 +6 -7
- package/src/cli/core/services/ProjectScaffolder.ts +18 -14
- package/src/cli/core/tasks/BuildCloudflareTask.ts +5 -0
- package/src/cli/core/templates/agentMd.ts +2 -10
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +3 -3
- package/src/cli/devtools/index.ts +12 -26
- 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/core/Alepha.ts +11 -1
- package/src/core/helpers/ref.ts +18 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/providers/SchemaValidator.ts +9 -1
- package/src/core/providers/TypeProvider.ts +1 -2
- 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/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/core/primitives/$transactional.ts +11 -0
- package/src/orm/core/schemas/updateSchema.ts +1 -1
- package/src/orm/core/services/PgRelationManager.ts +4 -2
- 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/router/__tests__/$page.spec.tsx +0 -16
- package/src/react/router/__tests__/ssr.spec.tsx +339 -0
- package/src/react/router/primitives/$page.ts +28 -4
- package/src/react/router/providers/ReactPageProvider.ts +27 -9
- 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/security/primitives/$issuer.ts +6 -3
- 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/primitives/$action.ts +16 -5
- package/src/server/core/providers/ServerRouterProvider.ts +26 -4
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -7
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +10 -4
- package/src/websocket/services/WebSocketClient.ts +11 -5
- package/src/mcp/transports/SseMcpTransport.ts +0 -182
|
@@ -470,6 +470,14 @@ export class ReactPageProvider {
|
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
// If the matched leaf opts out of SSR (own value or inherited from
|
|
474
|
+
// parents), wrap the root layer in ClientOnly so the server emits no
|
|
475
|
+
// HTML for the route chain. Loaders have already run above.
|
|
476
|
+
if (state.layers.length > 0 && !this.isSSR(route)) {
|
|
477
|
+
const rootLayer = state.layers[0];
|
|
478
|
+
rootLayer.element = createElement(ClientOnly, {}, rootLayer.element);
|
|
479
|
+
}
|
|
480
|
+
|
|
473
481
|
return { state };
|
|
474
482
|
}
|
|
475
483
|
|
|
@@ -598,14 +606,6 @@ export class ReactPageProvider {
|
|
|
598
606
|
): ReactNode {
|
|
599
607
|
view ??= this.renderEmptyView();
|
|
600
608
|
|
|
601
|
-
const element = page.client
|
|
602
|
-
? createElement(
|
|
603
|
-
ClientOnly,
|
|
604
|
-
typeof page.client === "object" ? page.client : {},
|
|
605
|
-
view,
|
|
606
|
-
)
|
|
607
|
-
: view;
|
|
608
|
-
|
|
609
609
|
return createElement(
|
|
610
610
|
RouterLayerContext.Provider,
|
|
611
611
|
{
|
|
@@ -616,10 +616,28 @@ export class ReactPageProvider {
|
|
|
616
616
|
this.getErrorHandler(page) ?? ((error) => this.renderError(error)),
|
|
617
617
|
},
|
|
618
618
|
},
|
|
619
|
-
|
|
619
|
+
view,
|
|
620
620
|
);
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Resolve the effective `ssr` value for a route by walking up the parent
|
|
625
|
+
* chain. Returns the nearest explicit `ssr` value, defaulting to `true`.
|
|
626
|
+
*
|
|
627
|
+
* The decision is made at the leaf: a parent's `ssr` only acts as a default
|
|
628
|
+
* for descendants that did not set their own value.
|
|
629
|
+
*/
|
|
630
|
+
public isSSR(route: PageRoute): boolean {
|
|
631
|
+
let current: PageRoute | undefined = route;
|
|
632
|
+
while (current) {
|
|
633
|
+
if (typeof current.ssr === "boolean") {
|
|
634
|
+
return current.ssr;
|
|
635
|
+
}
|
|
636
|
+
current = current.parent;
|
|
637
|
+
}
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
|
|
623
641
|
protected map(
|
|
624
642
|
pages: Array<PagePrimitive>,
|
|
625
643
|
target: PagePrimitive,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { $atom, type Static, t } from "alepha";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Available themes the user can pick from. Apps populate this atom on boot
|
|
5
|
+
* (e.g. `alepha.store.set(uiThemeListAtom, MY_THEMES)`); UI consumers like
|
|
6
|
+
* `<ButtonTheme/>` read it to render a picker. The selected theme id is
|
|
7
|
+
* persisted separately in `uiAtom.theme`.
|
|
8
|
+
*
|
|
9
|
+
* Defaults to a single `"default"` entry so the registry stays usable when
|
|
10
|
+
* an app doesn't declare its own list.
|
|
11
|
+
*/
|
|
12
|
+
export const uiThemeListAtom = $atom({
|
|
13
|
+
name: "alepha.react.ui.themes",
|
|
14
|
+
schema: t.array(
|
|
15
|
+
t.object({
|
|
16
|
+
/** Stable id stored in `uiAtom.theme`. Mapped to a CSS class on `<html>`. */
|
|
17
|
+
id: t.string(),
|
|
18
|
+
/** Human-readable label shown in the picker. */
|
|
19
|
+
label: t.string(),
|
|
20
|
+
/**
|
|
21
|
+
* Optional 4-color preview swatch in 2×2 order (TL, TR, BL, BR). Any
|
|
22
|
+
* CSS-valid color string.
|
|
23
|
+
*/
|
|
24
|
+
swatch: t.optional(t.array(t.string(), { minItems: 4, maxItems: 4 })),
|
|
25
|
+
/**
|
|
26
|
+
* Optional stylesheet URL (typically Google Fonts) loaded lazily when
|
|
27
|
+
* the theme is selected.
|
|
28
|
+
*/
|
|
29
|
+
fontHref: t.optional(t.string()),
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
default: [{ id: "default", label: "Default" }],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type UiThemeList = Static<typeof uiThemeListAtom.schema>;
|
|
36
|
+
export type UiTheme = UiThemeList[number];
|
package/src/react/ui/index.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { $module } from "alepha";
|
|
2
2
|
import type { UiState } from "./atoms/uiAtom.ts";
|
|
3
|
+
import type { UiThemeList } from "./atoms/uiThemeListAtom.ts";
|
|
4
|
+
import { uiThemeListAtom } from "./atoms/uiThemeListAtom.ts";
|
|
3
5
|
import { UiPersistence } from "./services/UiPersistence.ts";
|
|
4
6
|
|
|
5
7
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
6
8
|
|
|
7
9
|
export * from "./atoms/uiAtom.ts";
|
|
10
|
+
export * from "./atoms/uiThemeListAtom.ts";
|
|
8
11
|
export * from "./components/ColorScheme.tsx";
|
|
9
12
|
export * from "./hooks/useColorMode.ts";
|
|
10
13
|
export * from "./hooks/useSidebarState.ts";
|
|
11
14
|
export * from "./hooks/useTheme.ts";
|
|
15
|
+
export * from "./services/SchemaControl.ts";
|
|
12
16
|
export * from "./services/UiPersistence.ts";
|
|
13
17
|
|
|
14
18
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -16,6 +20,7 @@ export * from "./services/UiPersistence.ts";
|
|
|
16
20
|
declare module "alepha" {
|
|
17
21
|
export interface State {
|
|
18
22
|
"alepha.react.ui": UiState;
|
|
23
|
+
"alepha.react.ui.themes": UiThemeList;
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -31,5 +36,6 @@ declare module "alepha" {
|
|
|
31
36
|
*/
|
|
32
37
|
export const AlephaReactUi = $module({
|
|
33
38
|
name: "alepha.react.ui",
|
|
39
|
+
atoms: [uiThemeListAtom],
|
|
34
40
|
services: [UiPersistence],
|
|
35
41
|
});
|
|
@@ -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
|
+
}
|
|
@@ -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?: (
|
|
@@ -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
|
+
});
|