mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
feat: support preferred external ports besides 443 (#3117)
* docs: update preferred external port design in TODO * docs: add user-controlled public/private and port forward mapping to design * docs: overhaul interfaces page design with view/manage split and per-address controls * docs: move address enable/disable to overflow menu, add SSL indicator, defer UI placement decisions * chore: remove tor from startos core Tor is being moved from a built-in OS feature to a service. This removes the Arti-based Tor client, onion address management, hidden service creation, and all related code from the core backend, frontend, and SDK. - Delete core/src/net/tor/ module (~2060 lines) - Remove OnionAddress, TorSecretKey, TorController from all consumers - Remove HostnameInfo::Onion and HostAddress::Onion variants - Remove onion CRUD RPC endpoints and tor subcommand - Remove tor key handling from account and backup/restore - Remove ~12 tor-related Cargo dependencies (arti-client, torut, etc.) - Remove tor UI components, API methods, mock data, and routes - Remove OnionHostname and tor patterns/regexes from SDK - Add v0_4_0_alpha_20 database migration to strip onion data - Bump version to 0.4.0-alpha.20 * chore: flatten HostnameInfo from enum to struct HostnameInfo only had one variant (Ip) after removing Tor. Flatten it into a plain struct with fields gateway, public, hostname. Remove all kind === 'ip' type guards and narrowing across SDK, frontend, and container runtime. Update DB migration to strip the kind field. * chore: format RPCSpec.md markdown table * docs: update TODO.md with DerivedAddressInfo design, remove completed tor task * feat: implement preferred port allocation and per-address enable/disable - Add AvailablePorts::try_alloc() with SSL tracking (BTreeMap<u16, bool>) - Add DerivedAddressInfo on BindInfo with private_disabled/public_enabled/possible sets - Add Bindings wrapper with Map impl for patchdb indexed access - Flatten HostAddress from single-variant enum to struct - Replace set-gateway-enabled RPC with set-address-enabled - Remove hostname_info from Host; computed addresses now in BindInfo.addresses.possible - Compute possible addresses inline in NetServiceData::update() - Update DB migration, SDK types, frontend, and container-runtime * feat: replace InterfaceFilter with ForwardRequirements, add WildcardListener, complete alpha.20 bump - Replace DynInterfaceFilter with ForwardRequirements for per-IP forward precision with source-subnet iptables filtering for private forwards - Add WildcardListener (binds [::]:port) to replace the per-gateway NetworkInterfaceListener/SelfContainedNetworkInterfaceListener/ UpgradableListener infrastructure - Update forward-port script with src_subnet and excluded_src env vars - Remove unused filter types and listener infrastructure from gateway.rs - Add availablePorts migration (IdPool -> BTreeMap<u16, bool>) to alpha.20 - Complete version bump to 0.4.0-alpha.20 in SDK and web * outbound gateway support (#3120) * Multiple (#3111) * fix alerts i18n, fix status display, better, remove usb media, hide shutdown for install complete * trigger chnage detection for localize pipe and round out implementing localize pipe for consistency even though not needed * Fix PackageInfoShort to handle LocaleString on releaseNotes (#3112) * Fix PackageInfoShort to handle LocaleString on releaseNotes * fix: filter by target_version in get_matching_models and pass otherVersions from install * chore: add exver documentation for ai agents * frontend plus some be types --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> * feat: replace SourceFilter with IpNet, add policy routing, remove MASQUERADE * build ts types and fix i18n * fix license display in marketplace * wip refactor * chore: update ts bindings for preferred port design * feat: refactor NetService to watch DB and reconcile network state - NetService sync task now uses PatchDB DbWatch instead of being called directly after DB mutations - Read gateways from DB instead of network interface context when updating host addresses - gateway sync updates all host addresses in the DB - Add Watch<u64> channel for callers to wait on sync completion - Fix ts-rs codegen bug with #[ts(skip)] on flattened Plugin field - Update SDK getServiceInterface.ts for new HostnameInfo shape - Remove unnecessary HTTPS redirect in static_server.rs - Fix tunnel/api.rs to filter for WAN IPv4 address * re-arrange (#3123) * new service interfacee page * feat: add mdns hostname metadata variant and fix vhost routing - Add HostnameMetadata::Mdns variant to distinguish mDNS from private domains - Mark mDNS addresses as private (public: false) since mDNS is local-only - Fall back to null SNI entry when hostname not found in vhost mapping - Simplify public detection in ProxyTarget filter - Pass hostname to update_addresses for mDNS domain name generation * looking good * feat: add port_forwards field to Host for tracking gateway forwarding rules * update bindings for API types, add ARCHITECTURE (#3124) * update binding for API types, add ARCHITECTURE * translations * fix: add CONNMARK restore-mark to mangle OUTPUT chain The CONNMARK --restore-mark rule was only in PREROUTING, which handles forwarded packets. Locally-bound listeners (e.g. vhost) generate replies through the OUTPUT chain, where the fwmark was never restored. This caused response packets to route via the default table instead of back through the originating interface. * chore: reserialize db on equal version, update bindings and docs - Run de/ser roundtrip in pre_init even when db version matches, ensuring all #[serde(default)] fields are populated before any typed access - Add patchdb.md documentation for TypedDbWatch patterns - Update TS bindings for CheckPortParams, CheckPortRes, ifconfigUrl - Update CLAUDE.md docs with patchdb and component-level references * fix: include public gateways for IP-based addresses in vhost targets The server hostname vhost construction only collected private IPs, always setting public to empty. Public IP addresses (Ipv4/Ipv6 metadata with public=true) were never added to the vhost target's public gateway set, causing the vhost filter to reject public traffic for IP-based addresses. * fix: add TLS handshake timeout and fix accept loop deadlock Two issues in TlsListener::poll_accept: 1. No timeout on TLS handshakes: LazyConfigAcceptor waits indefinitely for ClientHello. Attackers that complete TCP handshake but never send TLS data create zombie futures in `in_progress` that never complete. Fix: wrap the entire handshake in tokio::time::timeout(15s). 2. Missing waker on new-connection pending path: when a TCP connection is accepted and the TLS handshake is pending, poll_accept returned Pending without calling wake_by_ref(). Since the TcpListener returned Ready (not Pending), no waker was registered for it. With edge- triggered epoll and no other wakeup source, the task sleeps forever and remaining connections in the kernel accept queue are never drained. Fix: add cx.waker().wake_by_ref() so the task immediately re-polls and continues draining the accept queue. * fix: switch BackgroundJobRunner from Vec to FuturesUnordered BackgroundJobRunner stored active jobs in a Vec<BoxFuture> and polled ALL of them on every wakeup — O(n) per poll. Since this runs in the same tokio::select! as the WebServer accept loop, polling overhead from active connections directly delayed acceptance of new connections. FuturesUnordered only polls woken futures — O(woken) instead of O(n). * chore: update bindings and use typed params for outbound gateway API * feat: per-service and default outbound gateway routing Add set-outbound-gateway RPC for packages and set-default-outbound RPC for the server, with policy routing enforcement via ip rules. Fix connmark restore to skip packets with existing fwmarks, add bridge subnet routes to per-interface tables, and fix squashfs path in update-image-local.sh. * refactor: manifest wraps PackageMetadata, move dependency_metadata to PackageVersionInfo Manifest now embeds PackageMetadata via #[serde(flatten)] instead of duplicating ~14 fields. icon and dependency_metadata moved from PackageMetadata to PackageVersionInfo since they are registry-enrichment data loaded from the S9PK archive. merge_with now returns errors on metadata/icon/dependency_metadata mismatches instead of silently ignoring them. * fix: replace .status() with .invoke() for iptables/ip commands Using .status() leaks stderr directly to system logs, causing noisy iptables error messages. Switch all networking CLI invocations to use .invoke() which captures stderr properly. For check-then-act patterns (iptables -C), use .invoke().await.is_err() instead of .status().await.map_or(false, |s| s.success()). * feat: add check-dns gateway endpoint and fix per-interface routing tables Add a `check-dns` RPC endpoint that verifies whether a gateway's DNS is properly configured for private domain resolution. Uses a three-tier check: direct match (DNS == server IP), TXT challenge probe (DNS on LAN), or failure (DNS off-subnet). Fix per-interface routing tables to clone all non-default routes from the main table instead of only the interface's own subnets. This preserves LAN reachability when the priority-75 catch-all overrides default routing. Filter out status-only flags (linkdown, dead) that are invalid for `ip route add`. * refactor: rename manifest metadata fields and improve error display Rename wrapperRepo→packageRepo, marketingSite→marketingUrl, docsUrl→docsUrls (array), remove supportSite. Add display_src/display_dbg helpers to Error. Fix DepInfo description type to LocaleString. Update web UI, SDK bindings, tests, and fixtures to match. Clean up cli_attach error handling and remove dead commented code. * chore: bump sdk version to 0.4.0-beta.49 * chore: add createTask decoupling TODO * chore: add TODO to clear service error state on install/update * round out dns check, dns server check, port forward check, and gateway port forwards * chore: add TODOs for URL plugins, NAT hairpinning, and start-tunnel OTA updates * version instead of os query param * interface row clickable again, bu now with a chevron! * feat: implement URL plugins with table/row actions and prefill support - Add URL plugin effects (register, export_url, clear_urls) in core - Add PluginHostnameInfo, HostnameMetadata::Plugin, and plugin registration types - Implement plugin URL table in web UI with tableAction button and rowAction overflow menus - Thread urlPluginMetadata (packageId, hostId, interfaceId, internalPort) as prefill to actions - Add prefill support to PackageActionData so metadata passes through form dialogs - Add i18n translations for plugin error messages - Clean up plugin URLs on package uninstall * feat: split row_actions into remove_action and overflow_actions for URL plugins * touch up URL plugins table * show table even when no addresses * feat: NAT hairpinning, DNS static servers, clear service error on install - Add POSTROUTING MASQUERADE rules for container and host hairpin NAT - Allow bridge subnet containers to reach private forwards via LAN IPs - Pass bridge_subnet env var from forward.rs to forward-port script - Use DB-configured static DNS servers in resolver with DB watcher - Fall back to resolv.conf servers when no static servers configured - Clear service error state when install/update completes successfully - Remove completed TODO items * feat: builder-style InputSpec API, prefill plumbing, and port forward fix - Add addKey() and add() builder methods to InputSpec with InputSpecTools - Move OuterType to last generic param on Value, List, and all dynamic methods - Plumb prefill through getActionInput end-to-end (core → container-runtime → SDK) - Filter port_forwards to enabled addresses only - Bump SDK to 0.4.0-beta.50 * fix: propagate host locale into LXC containers and write locale.conf * chore: remove completed URL plugins TODO * feat: OTA updates for start-tunnel via apt repository (untested) - Add apt repo publish script (build/apt/publish-deb.sh) for S3-hosted repo - Add apt source config and GPG key placeholder (apt/) - Add tunnel.update.check and tunnel.update.apply RPC endpoints - Wire up update API in tunnel frontend (api service + mock) - Uses systemd-run --scope to survive service restart during update * fix: publish script dpkg-name, s3cfg fallback, and --reinstall for apply * chore: replace OTA updates TODO with UI TODO for MattDHill * feat: add getOutboundGateway effect and simplify VersionGraph init/uninit Add getOutboundGateway effect across core, container-runtime, and SDK to let services query their effective outbound gateway with callback support. Remove preInstall/uninstall hooks from VersionGraph as they are no longer needed. * frontend start-tunnel updates * chore: remove completed TODO * feat: tor hidden service key migration * chore: migrate from ts-matches to zod across all TypeScript packages * feat(core): allow setting server hostname * send prefill for tasks and hide operations to hidden fields * fix(core): preserve plugin URLs across binding updates BindInfo::update was replacing addresses with a new DerivedAddressInfo that cleared the available set, wiping plugin-exported URLs whenever bind() was called. Also simplify update_addresses plugin preservation to use retain in place rather than collecting into a separate set. * minor cleanup from patch-db audit * clean up prefill flow * frontend support for setting and changing hostname * feat(core): refactor hostname to ServerHostnameInfo with name/hostname pair - Rename Hostname to ServerHostnameInfo, add name + hostname fields - Add set_hostname_rpc for changing hostname at runtime - Migrate alpha_20: generate serverInfo.name from hostname, delete ui.name - Extract gateway.rs helpers to fix rustfmt nesting depth issue - Add i18n key for hostname validation error - Update SDK bindings * add comments to everything potentially consumer facing (#3127) * add comments to everything potentially consumer facing * rework smtp --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> * implement server name * setup changes * clean up copy around addresses table * feat: add zod-deep-partial, partialValidator on InputSpec, and z.deepPartial re-export * fix: header color in zoom (#3128) * fix: merge version ranges when adding existing package signer (#3125) * fix: merge version ranges when adding existing package signer Previously, add_package_signer unconditionally inserted the new version range, overwriting any existing authorization for that signer. Now it OR-merges the new range with the existing one, so running signer add multiple times accumulates permissions rather than replacing them. * add --merge flag to registry package signer add Default behavior remains overwrite. When --merge is passed, the new version range is OR-merged with the existing one, allowing admins to accumulate permissions incrementally. * add missing attribute to TS type * make merge optional * upsert instead of insert * VersionRange::None on upsert * fix: header color in zoom --------- Co-authored-by: Dominion5254 <musashidisciple@proton.me> * update snake and add about this server to system general * chore: bump sdk to beta.53, wrap z.deepPartial with passthrough * reset instead of reset defaults * action failure show dialog * chore: bump sdk to beta.54, add device-info RPC, improve SDK abort handling and InputSpec filtering - Bump SDK version to 0.4.0-beta.54 - Add `server.device-info` RPC endpoint and `s9pk select` CLI command - Extract `HardwareRequirements::is_compatible()` method, reuse in registry filtering - Add `AbortedError` class with `muteUnhandled` flag, replace generic abort errors - Handle unhandled promise rejections in container-runtime with mute support - Improve `InputSpec.filter()` with `keepByDefault` param and boolean filter values - Accept readonly tuples in `CommandType` and `splitCommand` - Remove `sync_host` calls from host API handlers (binding/address changes) - Filter mDNS hostnames by secure gateway availability - Derive mDNS enabled state from LAN IPs in web UI - Add "Open UI" action to address table, disable mDNS toggle - Hide debug details in service error component - Update rpc-toolkit docs for no-params handlers * fix: add --no-nvram to efi grub-install to preserve built-in boot order * update snake * diable actions when in error state * chore: split out nvidia variant * misc bugfixes * create manage-release script (untested) * fix: preserve z namespace types for sdk consumers * sdk version bump * new checkPort types * multiple bugs and better port forward ux * fix link * chore: todos and formatting * fix build --------- Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com> Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Dominion5254 <musashidisciple@proton.me>
This commit is contained in:
8
sdk/CLAUDE.md
Normal file
8
sdk/CLAUDE.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# SDK — TypeScript Service Packaging
|
||||
|
||||
TypeScript SDK for packaging services for StartOS (`@start9labs/start-sdk`).
|
||||
|
||||
## Structure
|
||||
|
||||
- `base/` — Core types, ABI definitions, effects interface (`@start9labs/start-sdk-base`)
|
||||
- `package/` — Full SDK for package developers, re-exports base
|
||||
32
sdk/Makefile
32
sdk/Makefile
@@ -1,12 +1,12 @@
|
||||
PACKAGE_TS_FILES := $(shell git ls-files package/lib) package/lib/test/output.ts
|
||||
BASE_TS_FILES := $(shell git ls-files base/lib) package/lib/test/output.ts
|
||||
PACKAGE_TS_FILES := $(shell git ls-files package/lib)
|
||||
BASE_TS_FILES := $(shell git ls-files base/lib)
|
||||
version = $(shell git tag --sort=committerdate | tail -1)
|
||||
|
||||
.PHONY: test base/test package/test clean bundle fmt buildOutput check
|
||||
|
||||
all: bundle
|
||||
|
||||
package/test: $(PACKAGE_TS_FILES) package/lib/test/output.ts package/node_modules base/node_modules
|
||||
package/test: $(PACKAGE_TS_FILES) package/node_modules base/node_modules
|
||||
cd package && npm test
|
||||
|
||||
base/test: $(BASE_TS_FILES) base/node_modules
|
||||
@@ -21,25 +21,39 @@ clean:
|
||||
rm -f package/lib/test/output.ts
|
||||
rm -rf package/node_modules
|
||||
|
||||
package/lib/test/output.ts: package/node_modules package/lib/test/makeOutput.ts package/scripts/oldSpecToBuilder.ts
|
||||
cd package && npm run buildOutput
|
||||
|
||||
bundle: baseDist dist | test fmt
|
||||
touch dist
|
||||
|
||||
base/lib/exver/exver.ts: base/node_modules base/lib/exver/exver.pegjs
|
||||
cd base && npm run peggy
|
||||
|
||||
baseDist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) base/package.json base/node_modules base/README.md base/LICENSE
|
||||
baseDist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) base/package.json base/node_modules base/README.md base/LICENSE
|
||||
(cd base && npm run tsc)
|
||||
# Copy hand-written .js/.d.ts pairs (no corresponding .ts source) into the output.
|
||||
cd base/lib && find . -name '*.js' | while read f; do \
|
||||
base="$${f%.js}"; \
|
||||
if [ -f "$$base.d.ts" ] && [ ! -f "$$base.ts" ]; then \
|
||||
mkdir -p "../../baseDist/$$(dirname "$$f")"; \
|
||||
cp "$$f" "../../baseDist/$$f"; \
|
||||
cp "$$base.d.ts" "../../baseDist/$$base.d.ts"; \
|
||||
fi; \
|
||||
done
|
||||
rsync -ac base/node_modules baseDist/
|
||||
cp base/package.json baseDist/package.json
|
||||
cp base/README.md baseDist/README.md
|
||||
cp base/LICENSE baseDist/LICENSE
|
||||
touch baseDist
|
||||
|
||||
dist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) package/package.json package/.npmignore package/node_modules package/README.md package/LICENSE
|
||||
dist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) package/package.json package/.npmignore package/node_modules package/README.md package/LICENSE
|
||||
(cd package && npm run tsc)
|
||||
cd base/lib && find . -name '*.js' | while read f; do \
|
||||
base="$${f%.js}"; \
|
||||
if [ -f "$$base.d.ts" ] && [ ! -f "$$base.ts" ]; then \
|
||||
mkdir -p "../../dist/base/lib/$$(dirname "$$f")"; \
|
||||
cp "$$f" "../../dist/base/lib/$$f"; \
|
||||
cp "$$base.d.ts" "../../dist/base/lib/$$base.d.ts"; \
|
||||
fi; \
|
||||
done
|
||||
rsync -ac package/node_modules dist/
|
||||
cp package/.npmignore dist/.npmignore
|
||||
cp package/package.json dist/package.json
|
||||
@@ -73,7 +87,7 @@ base/node_modules: base/package-lock.json
|
||||
node_modules: package/node_modules base/node_modules
|
||||
|
||||
publish: bundle package/package.json package/README.md package/LICENSE
|
||||
cd dist && npm publish --access=public
|
||||
cd dist && npm publish --access=public --tag=latest
|
||||
|
||||
link: bundle
|
||||
cd dist && npm link
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
MountParams,
|
||||
StatusInfo,
|
||||
Manifest,
|
||||
HostnameInfo,
|
||||
} from './osBindings'
|
||||
import {
|
||||
PackageId,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
ServiceInterfaceId,
|
||||
SmtpValue,
|
||||
ActionResult,
|
||||
PluginHostnameInfo,
|
||||
} from './types'
|
||||
|
||||
/** Used to reach out from the pure js runtime */
|
||||
@@ -133,6 +135,8 @@ export type Effects = {
|
||||
}): Promise<string>
|
||||
/** Returns the IP address of StartOS */
|
||||
getOsIp(): Promise<string>
|
||||
/** Returns the effective outbound gateway for this service */
|
||||
getOutboundGateway(options: { callback?: () => void }): Promise<string>
|
||||
// interface
|
||||
/** Creates an interface bound to a specific host and port to show to the user */
|
||||
exportServiceInterface(options: ExportServiceInterfaceParams): Promise<null>
|
||||
@@ -151,6 +155,18 @@ export type Effects = {
|
||||
clearServiceInterfaces(options: {
|
||||
except: ServiceInterfaceId[]
|
||||
}): Promise<null>
|
||||
|
||||
plugin: {
|
||||
url: {
|
||||
register(options: { tableAction: ActionId }): Promise<null>
|
||||
exportUrl(options: {
|
||||
hostnameInfo: PluginHostnameInfo
|
||||
removeAction: ActionId | null
|
||||
overflowActions: ActionId[]
|
||||
}): Promise<null>
|
||||
clearUrls(options: { except: PluginHostnameInfo[] }): Promise<null>
|
||||
}
|
||||
}
|
||||
// ssl
|
||||
/** Returns a PEM encoded fullchain for the hostnames specified */
|
||||
getSslCertificate: (options: {
|
||||
|
||||
@@ -2,21 +2,84 @@ import { ValueSpec } from '../inputSpecTypes'
|
||||
import { Value } from './value'
|
||||
import { _ } from '../../../util'
|
||||
import { Effects } from '../../../Effects'
|
||||
import { Parser, object } from 'ts-matches'
|
||||
import { z } from 'zod'
|
||||
import { zodDeepPartial } from 'zod-deep-partial'
|
||||
import { DeepPartial } from '../../../types'
|
||||
import { InputSpecTools, createInputSpecTools } from './inputSpecTools'
|
||||
|
||||
export type LazyBuildOptions = {
|
||||
/** Options passed to a lazy builder function when resolving dynamic form field values. */
|
||||
export type LazyBuildOptions<Type> = {
|
||||
/** The effects interface for runtime operations (e.g. reading files, querying state). */
|
||||
effects: Effects
|
||||
/** Previously saved form data to pre-fill the form with, or `null` for fresh creation. */
|
||||
prefill: DeepPartial<Type> | null
|
||||
}
|
||||
export type LazyBuild<ExpectedOut> = (
|
||||
options: LazyBuildOptions,
|
||||
/**
|
||||
* A function that lazily produces a value, potentially using effects and prefill data.
|
||||
* Used by `dynamic*` variants of {@link Value} to compute form field options at runtime.
|
||||
*/
|
||||
export type LazyBuild<ExpectedOut, Type> = (
|
||||
options: LazyBuildOptions<Type>,
|
||||
) => Promise<ExpectedOut> | ExpectedOut
|
||||
|
||||
/**
|
||||
* Defines which keys to keep when filtering an InputSpec.
|
||||
* Use `true` to keep a field as-is, or a nested object to filter sub-fields of an object-typed field.
|
||||
*/
|
||||
export type FilterKeys<F> = {
|
||||
[K in keyof F]?: F[K] extends Record<string, any>
|
||||
? boolean | FilterKeys<F[K]>
|
||||
: boolean
|
||||
}
|
||||
|
||||
type RetainKey<T, F, Default extends boolean> = {
|
||||
[K in keyof T]: K extends keyof F
|
||||
? F[K] extends false
|
||||
? never
|
||||
: K
|
||||
: Default extends true
|
||||
? K
|
||||
: never
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* Computes the resulting type after applying a {@link FilterKeys} shape to a type.
|
||||
*/
|
||||
export type ApplyFilter<T, F, Default extends boolean = false> = {
|
||||
[K in RetainKey<T, F, Default>]: K extends keyof F
|
||||
? true extends F[K]
|
||||
? F[K] extends true
|
||||
? T[K]
|
||||
: T[K] | undefined
|
||||
: T[K] extends Record<string, any>
|
||||
? F[K] extends FilterKeys<T[K]>
|
||||
? ApplyFilter<T[K], F[K]>
|
||||
: undefined
|
||||
: undefined
|
||||
: Default extends true
|
||||
? T[K]
|
||||
: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the union of all valid key-path tuples through a nested type.
|
||||
* Each tuple represents a path from root to a field, recursing into object-typed sub-fields.
|
||||
*/
|
||||
export type KeyPaths<T> = {
|
||||
[K in keyof T & string]: T[K] extends any[]
|
||||
? [K]
|
||||
: T[K] extends Record<string, any>
|
||||
? [K] | [K, ...KeyPaths<T[K]>]
|
||||
: [K]
|
||||
}[keyof T & string]
|
||||
|
||||
/** Extracts the runtime type from an {@link InputSpec}. */
|
||||
// prettier-ignore
|
||||
export type ExtractInputSpecType<A extends InputSpec<Record<string, any>, any>> =
|
||||
export type ExtractInputSpecType<A extends InputSpec<Record<string, any>, any>> =
|
||||
A extends InputSpec<infer B, any> ? B :
|
||||
never
|
||||
|
||||
/** Extracts the static validation type from an {@link InputSpec}. */
|
||||
export type ExtractInputSpecStaticValidatedAs<
|
||||
A extends InputSpec<any, Record<string, any>>,
|
||||
> = A extends InputSpec<any, infer B> ? B : never
|
||||
@@ -25,11 +88,13 @@ export type ExtractInputSpecStaticValidatedAs<
|
||||
// A extends Record<string, any> | InputSpec<Record<string, any>>,
|
||||
// > = A extends InputSpec<infer B> ? DeepPartial<B> : DeepPartial<A>
|
||||
|
||||
/** Maps an object type to a record of {@link Value} entries for use with `InputSpec.of`. */
|
||||
export type InputSpecOf<A extends Record<string, any>> = {
|
||||
[K in keyof A]: Value<A[K]>
|
||||
}
|
||||
|
||||
export type MaybeLazyValues<A> = LazyBuild<A> | A
|
||||
/** A value that is either directly provided or lazily computed via a {@link LazyBuild} function. */
|
||||
export type MaybeLazyValues<A, T> = LazyBuild<A, T> | A
|
||||
/**
|
||||
* InputSpecs are the specs that are used by the os input specification form for this service.
|
||||
* Here is an example of a simple input specification
|
||||
@@ -94,21 +159,28 @@ export class InputSpec<
|
||||
private readonly spec: {
|
||||
[K in keyof Type]: Value<Type[K]>
|
||||
},
|
||||
public readonly validator: Parser<unknown, StaticValidatedAs>,
|
||||
public readonly validator: z.ZodType<StaticValidatedAs>,
|
||||
) {}
|
||||
public _TYPE: Type = null as any as Type
|
||||
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
|
||||
async build(options: LazyBuildOptions): Promise<{
|
||||
public readonly partialValidator: z.ZodType<DeepPartial<StaticValidatedAs>> =
|
||||
zodDeepPartial(this.validator) as any
|
||||
/**
|
||||
* Builds the runtime form specification and combined Zod validator from this InputSpec's fields.
|
||||
*
|
||||
* @returns An object containing the resolved `spec` (field specs keyed by name) and a combined `validator`
|
||||
*/
|
||||
async build<OuterType>(options: LazyBuildOptions<OuterType>): Promise<{
|
||||
spec: {
|
||||
[K in keyof Type]: ValueSpec
|
||||
}
|
||||
validator: Parser<unknown, Type>
|
||||
validator: z.ZodType<Type>
|
||||
}> {
|
||||
const answer = {} as {
|
||||
[K in keyof Type]: ValueSpec
|
||||
}
|
||||
const validator = {} as {
|
||||
[K in keyof Type]: Parser<unknown, any>
|
||||
[K in keyof Type]: z.ZodType<any>
|
||||
}
|
||||
for (const k in this.spec) {
|
||||
const built = await this.spec[k].build(options as any)
|
||||
@@ -117,22 +189,311 @@ export class InputSpec<
|
||||
}
|
||||
return {
|
||||
spec: answer,
|
||||
validator: object(validator) as any,
|
||||
validator: z.object(validator) as any,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple fields to this spec at once, returning a new `InputSpec` with extended types.
|
||||
*
|
||||
* @param build - A record of {@link Value} entries, or a function receiving typed tools that returns one
|
||||
*/
|
||||
add<AddSpec extends Record<string, Value<any, any, any>>>(
|
||||
build: AddSpec | ((tools: InputSpecTools<Type>) => AddSpec),
|
||||
): InputSpec<
|
||||
Type & {
|
||||
[K in keyof AddSpec]: AddSpec[K] extends Value<infer T, any, any>
|
||||
? T
|
||||
: never
|
||||
},
|
||||
StaticValidatedAs & {
|
||||
[K in keyof AddSpec]: AddSpec[K] extends Value<any, infer S, any>
|
||||
? S
|
||||
: never
|
||||
}
|
||||
> {
|
||||
const addedValues =
|
||||
build instanceof Function ? build(createInputSpecTools<Type>()) : build
|
||||
const newSpec = { ...this.spec, ...addedValues } as any
|
||||
const newValidator = z.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(newSpec).map(([k, v]) => [
|
||||
k,
|
||||
(v as Value<any>).validator,
|
||||
]),
|
||||
),
|
||||
)
|
||||
return new InputSpec(newSpec, newValidator as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new InputSpec containing only the specified keys.
|
||||
* Use `true` to keep a field as-is, or a nested object to filter sub-fields of object-typed fields.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const full = InputSpec.of({
|
||||
* name: Value.text({ name: 'Name', required: true, default: null }),
|
||||
* settings: Value.object({ name: 'Settings' }, InputSpec.of({
|
||||
* debug: Value.toggle({ name: 'Debug', default: false }),
|
||||
* port: Value.number({ name: 'Port', required: true, default: 8080, integer: true }),
|
||||
* })),
|
||||
* })
|
||||
* const filtered = full.filter({ name: true, settings: { debug: true } })
|
||||
* ```
|
||||
*/
|
||||
filter<F extends FilterKeys<Type>, Default extends boolean = false>(
|
||||
keys: F,
|
||||
keepByDefault?: Default,
|
||||
): InputSpec<
|
||||
ApplyFilter<Type, F, Default> & ApplyFilter<StaticValidatedAs, F, Default>,
|
||||
ApplyFilter<StaticValidatedAs, F, Default>
|
||||
> {
|
||||
const newSpec: Record<string, Value<any>> = {}
|
||||
for (const k of Object.keys(this.spec)) {
|
||||
const filterVal = (keys as any)[k]
|
||||
const value = (this.spec as any)[k] as Value<any> | undefined
|
||||
if (!value) continue
|
||||
if (filterVal === true) {
|
||||
newSpec[k] = value
|
||||
} else if (typeof filterVal === 'object' && filterVal !== null) {
|
||||
const objectMeta = value._objectSpec
|
||||
if (objectMeta) {
|
||||
const filteredInner = objectMeta.inputSpec.filter(
|
||||
filterVal,
|
||||
keepByDefault,
|
||||
)
|
||||
newSpec[k] = Value.object(objectMeta.params, filteredInner)
|
||||
} else {
|
||||
newSpec[k] = value
|
||||
}
|
||||
} else if (keepByDefault && filterVal !== false) {
|
||||
newSpec[k] = value
|
||||
}
|
||||
}
|
||||
const newValidator = z.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(newSpec).map(([k, v]) => [k, v.validator]),
|
||||
),
|
||||
)
|
||||
return new InputSpec(newSpec as any, newValidator as any) as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new InputSpec with the specified keys disabled.
|
||||
* Use `true` to disable a field, or a nested object to disable sub-fields of object-typed fields.
|
||||
* All fields remain in the spec — disabled fields simply cannot be edited by the user.
|
||||
*
|
||||
* @param keys - Which fields to disable, using the same shape as {@link FilterKeys}
|
||||
* @param message - The reason the fields are disabled, displayed to the user
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const spec = InputSpec.of({
|
||||
* name: Value.text({ name: 'Name', required: true, default: null }),
|
||||
* settings: Value.object({ name: 'Settings' }, InputSpec.of({
|
||||
* debug: Value.toggle({ name: 'Debug', default: false }),
|
||||
* port: Value.number({ name: 'Port', required: true, default: 8080, integer: true }),
|
||||
* })),
|
||||
* })
|
||||
* const disabled = spec.disable({ name: true, settings: { debug: true } }, 'Managed by the system')
|
||||
* ```
|
||||
*/
|
||||
disable(
|
||||
keys: FilterKeys<Type>,
|
||||
message: string,
|
||||
): InputSpec<Type, StaticValidatedAs> {
|
||||
const newSpec: Record<string, Value<any>> = {}
|
||||
for (const k in this.spec) {
|
||||
const filterVal = (keys as any)[k]
|
||||
const value = (this.spec as any)[k] as Value<any>
|
||||
if (!filterVal) {
|
||||
newSpec[k] = value
|
||||
} else if (filterVal === true) {
|
||||
newSpec[k] = value.withDisabled(message)
|
||||
} else if (typeof filterVal === 'object' && filterVal !== null) {
|
||||
const objectMeta = value._objectSpec
|
||||
if (objectMeta) {
|
||||
const disabledInner = objectMeta.inputSpec.disable(filterVal, message)
|
||||
newSpec[k] = Value.object(objectMeta.params, disabledInner)
|
||||
} else {
|
||||
newSpec[k] = value.withDisabled(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
const newValidator = z.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(newSpec).map(([k, v]) => [k, v.validator]),
|
||||
),
|
||||
)
|
||||
return new InputSpec(newSpec as any, newValidator as any) as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a key path to its corresponding display name path.
|
||||
* Each key is mapped to the `name` property of its built {@link ValueSpec}.
|
||||
* Recurses into `Value.object` sub-specs for nested paths.
|
||||
*
|
||||
* @param path - Typed tuple of field keys (e.g. `["settings", "debug"]`)
|
||||
* @param options - Build options providing effects and prefill data
|
||||
* @returns Array of display names (e.g. `["Settings", "Debug"]`)
|
||||
*/
|
||||
async namePath<OuterType>(
|
||||
path: KeyPaths<Type>,
|
||||
options: LazyBuildOptions<OuterType>,
|
||||
): Promise<string[]> {
|
||||
if (path.length === 0) return []
|
||||
const [key, ...rest] = path as [string, ...string[]]
|
||||
const value = (this.spec as any)[key] as Value<any> | undefined
|
||||
if (!value) return []
|
||||
const built = await value.build(options as any)
|
||||
const name =
|
||||
'name' in built.spec ? (built.spec as { name: string }).name : key
|
||||
if (rest.length === 0) return [name]
|
||||
const objectMeta = value._objectSpec
|
||||
if (objectMeta) {
|
||||
const innerNames = await objectMeta.inputSpec.namePath(
|
||||
rest as any,
|
||||
options,
|
||||
)
|
||||
return [name, ...innerNames]
|
||||
}
|
||||
return [name]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a key path to the description of the target field.
|
||||
* Recurses into `Value.object` sub-specs for nested paths.
|
||||
*
|
||||
* @param path - Typed tuple of field keys (e.g. `["settings", "debug"]`)
|
||||
* @param options - Build options providing effects and prefill data
|
||||
* @returns The description string, or `null` if the field has no description or was not found
|
||||
*/
|
||||
async description<OuterType>(
|
||||
path: KeyPaths<Type>,
|
||||
options: LazyBuildOptions<OuterType>,
|
||||
): Promise<string | null> {
|
||||
if (path.length === 0) return null
|
||||
const [key, ...rest] = path as [string, ...string[]]
|
||||
const value = (this.spec as any)[key] as Value<any> | undefined
|
||||
if (!value) return null
|
||||
if (rest.length === 0) {
|
||||
const built = await value.build(options as any)
|
||||
return 'description' in built.spec
|
||||
? (built.spec as { description: string | null }).description
|
||||
: null
|
||||
}
|
||||
const objectMeta = value._objectSpec
|
||||
if (objectMeta) {
|
||||
return objectMeta.inputSpec.description(rest as any, options)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new InputSpec filtered to only include keys present in the given partial object.
|
||||
* For nested `Value.object` fields, recurses into the partial value to filter sub-fields.
|
||||
*
|
||||
* @param partial - A deep-partial object whose defined keys determine which fields to keep
|
||||
*/
|
||||
filterFromPartial(
|
||||
partial: DeepPartial<Type>,
|
||||
): InputSpec<
|
||||
DeepPartial<Type> & DeepPartial<StaticValidatedAs>,
|
||||
DeepPartial<StaticValidatedAs>
|
||||
> {
|
||||
const newSpec: Record<string, Value<any>> = {}
|
||||
for (const k of Object.keys(partial)) {
|
||||
const value = (this.spec as any)[k] as Value<any> | undefined
|
||||
if (!value) continue
|
||||
const objectMeta = value._objectSpec
|
||||
if (objectMeta) {
|
||||
const partialVal = (partial as any)[k]
|
||||
if (typeof partialVal === 'object' && partialVal !== null) {
|
||||
const filteredInner =
|
||||
objectMeta.inputSpec.filterFromPartial(partialVal)
|
||||
newSpec[k] = Value.object(objectMeta.params, filteredInner)
|
||||
continue
|
||||
}
|
||||
}
|
||||
newSpec[k] = value
|
||||
}
|
||||
const newValidator = z.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(newSpec).map(([k, v]) => [k, v.validator]),
|
||||
),
|
||||
)
|
||||
return new InputSpec(newSpec as any, newValidator as any) as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new InputSpec with fields disabled based on which keys are present in the given partial object.
|
||||
* For nested `Value.object` fields, recurses into the partial value to disable sub-fields.
|
||||
* All fields remain in the spec — disabled fields simply cannot be edited by the user.
|
||||
*
|
||||
* @param partial - A deep-partial object whose defined keys determine which fields to disable
|
||||
* @param message - The reason the fields are disabled, displayed to the user
|
||||
*/
|
||||
disableFromPartial(
|
||||
partial: DeepPartial<Type>,
|
||||
message: string,
|
||||
): InputSpec<Type, StaticValidatedAs> {
|
||||
const newSpec: Record<string, Value<any>> = {}
|
||||
for (const k in this.spec) {
|
||||
const value = (this.spec as any)[k] as Value<any>
|
||||
if (!(k in (partial as any))) {
|
||||
newSpec[k] = value
|
||||
continue
|
||||
}
|
||||
const objectMeta = value._objectSpec
|
||||
if (objectMeta) {
|
||||
const partialVal = (partial as any)[k]
|
||||
if (typeof partialVal === 'object' && partialVal !== null) {
|
||||
const disabledInner = objectMeta.inputSpec.disableFromPartial(
|
||||
partialVal,
|
||||
message,
|
||||
)
|
||||
newSpec[k] = Value.object(objectMeta.params, disabledInner)
|
||||
continue
|
||||
}
|
||||
}
|
||||
newSpec[k] = value.withDisabled(message)
|
||||
}
|
||||
const newValidator = z.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(newSpec).map(([k, v]) => [k, v.validator]),
|
||||
),
|
||||
)
|
||||
return new InputSpec(newSpec as any, newValidator as any) as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an `InputSpec` from a plain record of {@link Value} entries.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const spec = InputSpec.of({
|
||||
* username: Value.text({ name: 'Username', required: true, default: null }),
|
||||
* verbose: Value.toggle({ name: 'Verbose Logging', default: false }),
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
static of<Spec extends Record<string, Value<any, any>>>(spec: Spec) {
|
||||
const validator = object(
|
||||
const validator = z.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(spec).map(([k, v]) => [k, v.validator]),
|
||||
),
|
||||
)
|
||||
return new InputSpec<
|
||||
{
|
||||
[K in keyof Spec]: Spec[K] extends Value<infer T, any> ? T : never
|
||||
[K in keyof Spec]: Spec[K] extends Value<infer T, any, unknown>
|
||||
? T
|
||||
: never
|
||||
},
|
||||
{
|
||||
[K in keyof Spec]: Spec[K] extends Value<any, infer T> ? T : never
|
||||
[K in keyof Spec]: Spec[K] extends Value<any, infer T, unknown>
|
||||
? T
|
||||
: never
|
||||
}
|
||||
>(spec, validator as any)
|
||||
}
|
||||
|
||||
274
sdk/base/lib/actions/input/builder/inputSpecTools.ts
Normal file
274
sdk/base/lib/actions/input/builder/inputSpecTools.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { InputSpec, LazyBuild } from './inputSpec'
|
||||
import { AsRequired, FileInfo, Value } from './value'
|
||||
import { List } from './list'
|
||||
import { UnionRes, UnionResStaticValidatedAs, Variants } from './variants'
|
||||
import {
|
||||
Pattern,
|
||||
RandomString,
|
||||
ValueSpecDatetime,
|
||||
ValueSpecText,
|
||||
} from '../inputSpecTypes'
|
||||
import { DefaultString } from '../inputSpecTypes'
|
||||
import { z } from 'zod'
|
||||
import { ListValueSpecText } from '../inputSpecTypes'
|
||||
|
||||
export interface InputSpecTools<OuterType> {
|
||||
Value: BoundValue<OuterType>
|
||||
Variants: typeof Variants
|
||||
InputSpec: typeof InputSpec
|
||||
List: BoundList<OuterType>
|
||||
}
|
||||
|
||||
export interface BoundValue<OuterType> {
|
||||
// Static (non-dynamic) methods — no OuterType involved
|
||||
toggle: typeof Value.toggle
|
||||
text: typeof Value.text
|
||||
textarea: typeof Value.textarea
|
||||
number: typeof Value.number
|
||||
color: typeof Value.color
|
||||
datetime: typeof Value.datetime
|
||||
select: typeof Value.select
|
||||
multiselect: typeof Value.multiselect
|
||||
object: typeof Value.object
|
||||
file: typeof Value.file
|
||||
list: typeof Value.list
|
||||
hidden: typeof Value.hidden
|
||||
union: typeof Value.union
|
||||
|
||||
// Dynamic methods with OuterType pre-bound (last generic param removed)
|
||||
dynamicToggle(
|
||||
a: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: boolean
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<boolean, boolean, OuterType>
|
||||
|
||||
dynamicText<Required extends boolean>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: DefaultString | null
|
||||
required: Required
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
inputmode?: ValueSpecText['inputmode']
|
||||
disabled?: string | false
|
||||
generate?: null | RandomString
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<AsRequired<string, Required>, string | null, OuterType>
|
||||
|
||||
dynamicTextarea<Required extends boolean>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
minRows?: number
|
||||
maxRows?: number
|
||||
placeholder?: string | null
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<AsRequired<string, Required>, string | null, OuterType>
|
||||
|
||||
dynamicNumber<Required extends boolean>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: number | null
|
||||
required: Required
|
||||
min?: number | null
|
||||
max?: number | null
|
||||
step?: number | null
|
||||
integer: boolean
|
||||
units?: string | null
|
||||
placeholder?: string | null
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<AsRequired<number, Required>, number | null, OuterType>
|
||||
|
||||
dynamicColor<Required extends boolean>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<AsRequired<string, Required>, string | null, OuterType>
|
||||
|
||||
dynamicDatetime<Required extends boolean>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
inputmode?: ValueSpecDatetime['inputmode']
|
||||
min?: string | null
|
||||
max?: string | null
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<AsRequired<string, Required>, string | null, OuterType>
|
||||
|
||||
dynamicSelect<Values extends Record<string, string>>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string
|
||||
values: Values
|
||||
disabled?: false | string | string[]
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<keyof Values & string, keyof Values & string, OuterType>
|
||||
|
||||
dynamicMultiselect<Values extends Record<string, string>>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string[]
|
||||
values: Values
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
disabled?: false | string | string[]
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<(keyof Values & string)[], (keyof Values & string)[], OuterType>
|
||||
|
||||
dynamicFile<Required extends boolean>(
|
||||
a: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
extensions: string[]
|
||||
required: Required
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<AsRequired<FileInfo, Required>, FileInfo | null, OuterType>
|
||||
|
||||
dynamicUnion<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any>
|
||||
}
|
||||
},
|
||||
>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<UnionRes<VariantValues>, UnionRes<VariantValues>, OuterType>
|
||||
dynamicUnion<
|
||||
StaticVariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, any>
|
||||
}
|
||||
},
|
||||
VariantValues extends StaticVariantValues,
|
||||
>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
validator: z.ZodType<UnionResStaticValidatedAs<StaticVariantValues>>,
|
||||
): Value<
|
||||
UnionRes<VariantValues>,
|
||||
UnionResStaticValidatedAs<StaticVariantValues>,
|
||||
OuterType
|
||||
>
|
||||
|
||||
dynamicHidden<T>(
|
||||
getParser: LazyBuild<z.ZodType<T>, OuterType>,
|
||||
): Value<T, T, OuterType>
|
||||
}
|
||||
|
||||
export interface BoundList<OuterType> {
|
||||
text: typeof List.text
|
||||
obj: typeof List.obj
|
||||
dynamicText(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default?: string[]
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
disabled?: false | string
|
||||
generate?: null | RandomString
|
||||
spec: {
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
inputmode?: ListValueSpecText['inputmode']
|
||||
}
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): List<string[], string[], OuterType>
|
||||
}
|
||||
|
||||
export function createInputSpecTools<OuterType>(): InputSpecTools<OuterType> {
|
||||
return {
|
||||
Value: Value as any as BoundValue<OuterType>,
|
||||
Variants,
|
||||
InputSpec,
|
||||
List: List as any as BoundList<OuterType>,
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,39 @@ import {
|
||||
ValueSpecList,
|
||||
ValueSpecListOf,
|
||||
} from '../inputSpecTypes'
|
||||
import { Parser, arrayOf, string } from 'ts-matches'
|
||||
import { z } from 'zod'
|
||||
|
||||
export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
/**
|
||||
* Builder class for defining list-type form fields.
|
||||
*
|
||||
* A list presents an interface to add, remove, and reorder items. Items can be
|
||||
* either text strings ({@link List.text}) or structured objects ({@link List.obj}).
|
||||
*
|
||||
* Used with {@link Value.list} to include a list field in an {@link InputSpec}.
|
||||
*/
|
||||
export class List<
|
||||
Type extends StaticValidatedAs,
|
||||
StaticValidatedAs = Type,
|
||||
OuterType = unknown,
|
||||
> {
|
||||
private constructor(
|
||||
public build: LazyBuild<{
|
||||
spec: ValueSpecList
|
||||
validator: Parser<unknown, Type>
|
||||
}>,
|
||||
public readonly validator: Parser<unknown, StaticValidatedAs>,
|
||||
public build: LazyBuild<
|
||||
{
|
||||
spec: ValueSpecList
|
||||
validator: z.ZodType<Type>
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
public readonly validator: z.ZodType<StaticValidatedAs>,
|
||||
) {}
|
||||
readonly _TYPE: Type = null as any
|
||||
|
||||
/**
|
||||
* Creates a list of text input items.
|
||||
*
|
||||
* @param a - List-level options (name, description, min/max length, defaults)
|
||||
* @param aSpec - Item-level options (patterns, input mode, masking, generation)
|
||||
*/
|
||||
static text(
|
||||
a: {
|
||||
name: string
|
||||
@@ -62,7 +83,7 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
generate?: null | RandomString
|
||||
},
|
||||
) {
|
||||
const validator = arrayOf(string)
|
||||
const validator = z.array(z.string())
|
||||
return new List<string[]>(() => {
|
||||
const spec = {
|
||||
type: 'text' as const,
|
||||
@@ -90,28 +111,32 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
}, validator)
|
||||
}
|
||||
|
||||
static dynamicText(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default?: string[]
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
disabled?: false | string
|
||||
generate?: null | RandomString
|
||||
spec: {
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
/** Like {@link List.text} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicText<OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default?: string[]
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
inputmode?: ListValueSpecText['inputmode']
|
||||
}
|
||||
}>,
|
||||
disabled?: false | string
|
||||
generate?: null | RandomString
|
||||
spec: {
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
inputmode?: ListValueSpecText['inputmode']
|
||||
}
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
const validator = arrayOf(string)
|
||||
return new List<string[]>(async (options) => {
|
||||
const validator = z.array(z.string())
|
||||
return new List<string[], string[], OuterType>(async (options) => {
|
||||
const { spec: aSpec, ...a } = await getA(options)
|
||||
const spec = {
|
||||
type: 'text' as const,
|
||||
@@ -140,6 +165,12 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
}, validator)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of structured object items, each defined by a nested {@link InputSpec}.
|
||||
*
|
||||
* @param a - List-level options (name, description, min/max length)
|
||||
* @param aSpec - Item-level options (the nested spec, display expression, uniqueness constraint)
|
||||
*/
|
||||
static obj<
|
||||
Type extends StaticValidatedAs,
|
||||
StaticValidatedAs extends Record<string, any>,
|
||||
@@ -183,8 +214,8 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
disabled: false,
|
||||
...value,
|
||||
},
|
||||
validator: arrayOf(built.validator),
|
||||
validator: z.array(built.validator),
|
||||
}
|
||||
}, arrayOf(aSpec.spec.validator))
|
||||
}, z.array(aSpec.spec.validator))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,51 +12,69 @@ import {
|
||||
} from '../inputSpecTypes'
|
||||
import { DefaultString } from '../inputSpecTypes'
|
||||
import { _, once } from '../../../util'
|
||||
import {
|
||||
Parser,
|
||||
any,
|
||||
anyOf,
|
||||
arrayOf,
|
||||
boolean,
|
||||
literal,
|
||||
literals,
|
||||
number,
|
||||
object,
|
||||
string,
|
||||
} from 'ts-matches'
|
||||
import { z } from 'zod'
|
||||
import { DeepPartial } from '../../../types'
|
||||
|
||||
export const fileInfoParser = object({
|
||||
path: string,
|
||||
commitment: object({ hash: string, size: number }),
|
||||
/** Zod schema for a file upload result — validates `{ path, commitment: { hash, size } }`. */
|
||||
export const fileInfoParser = z.object({
|
||||
path: z.string(),
|
||||
commitment: z.object({ hash: z.string(), size: z.number() }),
|
||||
})
|
||||
export type FileInfo = typeof fileInfoParser._TYPE
|
||||
/** The parsed result of a file upload, containing the file path and its content commitment (hash + size). */
|
||||
export type FileInfo = z.infer<typeof fileInfoParser>
|
||||
|
||||
type AsRequired<T, Required extends boolean> = Required extends true
|
||||
/** Conditional type: returns `T` if `Required` is `true`, otherwise `T | null`. */
|
||||
export type AsRequired<T, Required extends boolean> = Required extends true
|
||||
? T
|
||||
: T | null
|
||||
|
||||
const testForAsRequiredParser = once(
|
||||
() => object({ required: literal(true) }).test,
|
||||
() => (v: unknown) =>
|
||||
z.object({ required: z.literal(true) }).safeParse(v).success,
|
||||
)
|
||||
function asRequiredParser<Type, Input extends { required: boolean }>(
|
||||
parser: Parser<unknown, Type>,
|
||||
parser: z.ZodType<Type>,
|
||||
input: Input,
|
||||
): Parser<unknown, AsRequired<Type, Input['required']>> {
|
||||
): z.ZodType<AsRequired<Type, Input['required']>> {
|
||||
if (testForAsRequiredParser()(input)) return parser as any
|
||||
return parser.nullable() as any
|
||||
}
|
||||
|
||||
export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
/**
|
||||
* Core builder class for defining a single form field in a service configuration spec.
|
||||
*
|
||||
* Each static factory method (e.g. `Value.text()`, `Value.toggle()`, `Value.select()`) creates
|
||||
* a typed `Value` instance representing a specific field type. Dynamic variants (e.g. `Value.dynamicText()`)
|
||||
* allow the field options to be computed lazily at runtime.
|
||||
*
|
||||
* Use with {@link InputSpec} to compose complete form specifications.
|
||||
*
|
||||
* @typeParam Type - The runtime type this field produces when filled in
|
||||
* @typeParam StaticValidatedAs - The compile-time validated type (usually same as Type)
|
||||
* @typeParam OuterType - The parent form's type context (used by dynamic variants)
|
||||
*/
|
||||
export class Value<
|
||||
Type extends StaticValidatedAs,
|
||||
StaticValidatedAs = Type,
|
||||
OuterType = unknown,
|
||||
> {
|
||||
protected constructor(
|
||||
public build: LazyBuild<{
|
||||
spec: ValueSpec
|
||||
validator: Parser<unknown, Type>
|
||||
}>,
|
||||
public readonly validator: Parser<unknown, StaticValidatedAs>,
|
||||
public build: LazyBuild<
|
||||
{
|
||||
spec: ValueSpec
|
||||
validator: z.ZodType<Type>
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
public readonly validator: z.ZodType<StaticValidatedAs>,
|
||||
) {}
|
||||
public _TYPE: Type = null as any as Type
|
||||
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
|
||||
/** @internal Used by {@link InputSpec.filter} to support nested filtering of object-typed fields. */
|
||||
_objectSpec?: {
|
||||
inputSpec: InputSpec<any, any>
|
||||
params: { name: string; description?: string | null }
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Displays a boolean toggle to enable/disable
|
||||
@@ -86,7 +104,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = boolean
|
||||
const validator = z.boolean()
|
||||
return new Value<boolean>(
|
||||
async () => ({
|
||||
spec: {
|
||||
@@ -102,17 +120,21 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicToggle(
|
||||
a: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: boolean
|
||||
disabled?: false | string
|
||||
}>,
|
||||
/** Like {@link Value.toggle} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicToggle<OuterType = unknown>(
|
||||
a: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: boolean
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
const validator = boolean
|
||||
return new Value<boolean>(
|
||||
const validator = z.boolean()
|
||||
return new Value<boolean, boolean, OuterType>(
|
||||
async (options) => ({
|
||||
spec: {
|
||||
description: null,
|
||||
@@ -202,7 +224,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
*/
|
||||
generate?: RandomString | null
|
||||
}) {
|
||||
const validator = asRequiredParser(string, a)
|
||||
const validator = asRequiredParser(z.string(), a)
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
async () => ({
|
||||
spec: {
|
||||
@@ -225,24 +247,28 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicText<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: DefaultString | null
|
||||
required: Required
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
inputmode?: ValueSpecText['inputmode']
|
||||
disabled?: string | false
|
||||
generate?: null | RandomString
|
||||
}>,
|
||||
/** Like {@link Value.text} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicText<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: DefaultString | null
|
||||
required: Required
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
inputmode?: ValueSpecText['inputmode']
|
||||
disabled?: string | false
|
||||
generate?: null | RandomString
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
return new Value<AsRequired<string, Required>, string | null>(
|
||||
return new Value<AsRequired<string, Required>, string | null, OuterType>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
@@ -261,10 +287,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
generate: a.generate ?? null,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(string, a),
|
||||
validator: asRequiredParser(z.string(), a),
|
||||
}
|
||||
},
|
||||
string.nullable(),
|
||||
z.string().nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
@@ -323,7 +349,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = asRequiredParser(string, a)
|
||||
const validator = asRequiredParser(z.string(), a)
|
||||
return new Value<AsRequired<string, Required>>(async () => {
|
||||
const built: ValueSpecTextarea = {
|
||||
description: null,
|
||||
@@ -342,23 +368,27 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
return { spec: built, validator }
|
||||
}, validator)
|
||||
}
|
||||
static dynamicTextarea<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
minRows?: number
|
||||
maxRows?: number
|
||||
placeholder?: string | null
|
||||
disabled?: false | string
|
||||
}>,
|
||||
/** Like {@link Value.textarea} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicTextarea<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
minRows?: number
|
||||
maxRows?: number
|
||||
placeholder?: string | null
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
return new Value<AsRequired<string, Required>, string | null>(
|
||||
return new Value<AsRequired<string, Required>, string | null, OuterType>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
@@ -376,10 +406,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(string, a),
|
||||
validator: asRequiredParser(z.string(), a),
|
||||
}
|
||||
},
|
||||
string.nullable(),
|
||||
z.string().nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
@@ -440,7 +470,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = asRequiredParser(number, a)
|
||||
const validator = asRequiredParser(z.number(), a)
|
||||
return new Value<AsRequired<number, Required>>(
|
||||
() => ({
|
||||
spec: {
|
||||
@@ -461,23 +491,27 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicNumber<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: number | null
|
||||
required: Required
|
||||
min?: number | null
|
||||
max?: number | null
|
||||
step?: number | null
|
||||
integer: boolean
|
||||
units?: string | null
|
||||
placeholder?: string | null
|
||||
disabled?: false | string
|
||||
}>,
|
||||
/** Like {@link Value.number} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicNumber<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: number | null
|
||||
required: Required
|
||||
min?: number | null
|
||||
max?: number | null
|
||||
step?: number | null
|
||||
integer: boolean
|
||||
units?: string | null
|
||||
placeholder?: string | null
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
return new Value<AsRequired<number, Required>, number | null>(
|
||||
return new Value<AsRequired<number, Required>, number | null, OuterType>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
@@ -494,10 +528,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(number, a),
|
||||
validator: asRequiredParser(z.number(), a),
|
||||
}
|
||||
},
|
||||
number.nullable(),
|
||||
z.number().nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
@@ -536,7 +570,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = asRequiredParser(string, a)
|
||||
const validator = asRequiredParser(z.string(), a)
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
() => ({
|
||||
spec: {
|
||||
@@ -553,17 +587,21 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
)
|
||||
}
|
||||
|
||||
static dynamicColor<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
disabled?: false | string
|
||||
}>,
|
||||
/** Like {@link Value.color} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicColor<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
return new Value<AsRequired<string, Required>, string | null>(
|
||||
return new Value<AsRequired<string, Required>, string | null, OuterType>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
@@ -575,10 +613,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(string, a),
|
||||
validator: asRequiredParser(z.string(), a),
|
||||
}
|
||||
},
|
||||
string.nullable(),
|
||||
z.string().nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
@@ -627,7 +665,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = asRequiredParser(string, a)
|
||||
const validator = asRequiredParser(z.string(), a)
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
() => ({
|
||||
spec: {
|
||||
@@ -647,20 +685,24 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicDatetime<Required extends boolean>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
inputmode?: ValueSpecDatetime['inputmode']
|
||||
min?: string | null
|
||||
max?: string | null
|
||||
disabled?: false | string
|
||||
}>,
|
||||
/** Like {@link Value.datetime} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicDatetime<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
inputmode?: ValueSpecDatetime['inputmode']
|
||||
min?: string | null
|
||||
max?: string | null
|
||||
disabled?: false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
return new Value<AsRequired<string, Required>, string | null>(
|
||||
return new Value<AsRequired<string, Required>, string | null, OuterType>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
@@ -675,10 +717,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: asRequiredParser(string, a),
|
||||
validator: asRequiredParser(z.string(), a),
|
||||
}
|
||||
},
|
||||
string.nullable(),
|
||||
z.string().nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
@@ -732,8 +774,12 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = anyOf(
|
||||
...Object.keys(a.values).map((x: keyof Values & string) => literal(x)),
|
||||
const validator = z.union(
|
||||
Object.keys(a.values).map((x: keyof Values & string) => z.literal(x)) as [
|
||||
z.ZodLiteral<string>,
|
||||
z.ZodLiteral<string>,
|
||||
...z.ZodLiteral<string>[],
|
||||
],
|
||||
)
|
||||
return new Value<keyof Values & string>(
|
||||
() => ({
|
||||
@@ -750,34 +796,48 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicSelect<Values extends Record<string, string>>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string
|
||||
values: Values
|
||||
disabled?: false | string | string[]
|
||||
}>,
|
||||
/** Like {@link Value.select} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicSelect<
|
||||
Values extends Record<string, string>,
|
||||
OuterType = unknown,
|
||||
>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string
|
||||
values: Values
|
||||
disabled?: false | string | string[]
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
return new Value<keyof Values & string, string>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: 'select' as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: anyOf(
|
||||
...Object.keys(a.values).map((x: keyof Values & string) =>
|
||||
literal(x),
|
||||
return new Value<keyof Values & string, keyof Values & string, OuterType>(
|
||||
async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: 'select' as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: z.union(
|
||||
Object.keys(a.values).map((x: keyof Values & string) =>
|
||||
z.literal(x),
|
||||
) as [
|
||||
z.ZodLiteral<string>,
|
||||
z.ZodLiteral<string>,
|
||||
...z.ZodLiteral<string>[],
|
||||
],
|
||||
),
|
||||
),
|
||||
}
|
||||
}, string)
|
||||
}
|
||||
},
|
||||
z.string(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @description Displays a select modal with checkboxes, allowing for multiple selections.
|
||||
@@ -831,8 +891,14 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
const validator = arrayOf(
|
||||
literals(...(Object.keys(a.values) as any as [keyof Values & string])),
|
||||
const validator = z.array(
|
||||
z.union(
|
||||
Object.keys(a.values).map((x) => z.literal(x)) as [
|
||||
z.ZodLiteral<string>,
|
||||
z.ZodLiteral<string>,
|
||||
...z.ZodLiteral<string>[],
|
||||
],
|
||||
),
|
||||
)
|
||||
return new Value<(keyof Values & string)[]>(
|
||||
() => ({
|
||||
@@ -851,19 +917,30 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
validator,
|
||||
)
|
||||
}
|
||||
static dynamicMultiselect<Values extends Record<string, string>>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string[]
|
||||
values: Values
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
disabled?: false | string | string[]
|
||||
}>,
|
||||
/** Like {@link Value.multiselect} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicMultiselect<
|
||||
Values extends Record<string, string>,
|
||||
OuterType = unknown,
|
||||
>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
default: string[]
|
||||
values: Values
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
disabled?: false | string | string[]
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
return new Value<(keyof Values & string)[], string[]>(async (options) => {
|
||||
return new Value<
|
||||
(keyof Values & string)[],
|
||||
(keyof Values & string)[],
|
||||
OuterType
|
||||
>(async (options) => {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
@@ -876,13 +953,17 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
immutable: false,
|
||||
...a,
|
||||
},
|
||||
validator: arrayOf(
|
||||
literals(
|
||||
...(Object.keys(a.values) as any as [keyof Values & string]),
|
||||
validator: z.array(
|
||||
z.union(
|
||||
Object.keys(a.values).map((x) => z.literal(x)) as [
|
||||
z.ZodLiteral<string>,
|
||||
z.ZodLiteral<string>,
|
||||
...z.ZodLiteral<string>[],
|
||||
],
|
||||
),
|
||||
),
|
||||
}
|
||||
}, arrayOf(string))
|
||||
}, z.array(z.string()))
|
||||
}
|
||||
/**
|
||||
* @description Display a collapsable grouping of additional fields, a "sub form". The second value is the inputSpec spec for the sub form.
|
||||
@@ -911,7 +992,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
},
|
||||
spec: InputSpec<Type, StaticValidatedAs>,
|
||||
) {
|
||||
return new Value<Type, StaticValidatedAs>(async (options) => {
|
||||
const value = new Value<Type, StaticValidatedAs>(async (options) => {
|
||||
const built = await spec.build(options as any)
|
||||
return {
|
||||
spec: {
|
||||
@@ -924,7 +1005,15 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
validator: built.validator,
|
||||
}
|
||||
}, spec.validator)
|
||||
value._objectSpec = { inputSpec: spec, params: a }
|
||||
return value
|
||||
}
|
||||
/**
|
||||
* Displays a file upload input field.
|
||||
*
|
||||
* @param a.extensions - Allowed file extensions (e.g. `[".pem", ".crt"]`)
|
||||
* @param a.required - Whether a file must be selected
|
||||
*/
|
||||
static file<Required extends boolean>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
@@ -948,30 +1037,35 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
asRequiredParser(fileInfoParser, a),
|
||||
)
|
||||
}
|
||||
static dynamicFile<Required extends boolean>(
|
||||
a: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
extensions: string[]
|
||||
required: Required
|
||||
}>,
|
||||
) {
|
||||
return new Value<AsRequired<FileInfo, Required>, FileInfo | null>(
|
||||
async (options) => {
|
||||
const spec = {
|
||||
type: 'file' as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...(await a(options)),
|
||||
}
|
||||
return {
|
||||
spec,
|
||||
validator: asRequiredParser(fileInfoParser, spec),
|
||||
}
|
||||
/** Like {@link Value.file} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicFile<Required extends boolean, OuterType = unknown>(
|
||||
a: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
extensions: string[]
|
||||
required: Required
|
||||
},
|
||||
fileInfoParser.nullable(),
|
||||
)
|
||||
OuterType
|
||||
>,
|
||||
) {
|
||||
return new Value<
|
||||
AsRequired<FileInfo, Required>,
|
||||
FileInfo | null,
|
||||
OuterType
|
||||
>(async (options) => {
|
||||
const spec = {
|
||||
type: 'file' as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...(await a(options)),
|
||||
}
|
||||
return {
|
||||
spec,
|
||||
validator: asRequiredParser(fileInfoParser, spec),
|
||||
}
|
||||
}, fileInfoParser.nullable())
|
||||
}
|
||||
/**
|
||||
* @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented.
|
||||
@@ -1029,7 +1123,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
}) {
|
||||
return new Value<
|
||||
typeof a.variants._TYPE,
|
||||
typeof a.variants.validator._TYPE
|
||||
typeof a.variants.validator._output
|
||||
>(async (options) => {
|
||||
const built = await a.variants.build(options as any)
|
||||
return {
|
||||
@@ -1046,6 +1140,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
}
|
||||
}, a.variants.validator)
|
||||
}
|
||||
/** Like {@link Value.union} but options (including which variants are available) are resolved lazily at runtime. */
|
||||
static dynamicUnion<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
@@ -1053,37 +1148,47 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
spec: InputSpec<any>
|
||||
}
|
||||
},
|
||||
OuterType = unknown,
|
||||
>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
}>,
|
||||
): Value<UnionRes<VariantValues>, unknown>
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
): Value<UnionRes<VariantValues>, UnionRes<VariantValues>, OuterType>
|
||||
/** Like {@link Value.union} but options are resolved lazily, with an explicit static validator type. */
|
||||
static dynamicUnion<
|
||||
VariantValues extends StaticVariantValues,
|
||||
StaticVariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, any>
|
||||
}
|
||||
},
|
||||
VariantValues extends StaticVariantValues,
|
||||
OuterType = unknown,
|
||||
>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
}>,
|
||||
validator: Parser<unknown, UnionResStaticValidatedAs<StaticVariantValues>>,
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
validator: z.ZodType<UnionResStaticValidatedAs<StaticVariantValues>>,
|
||||
): Value<
|
||||
UnionRes<VariantValues>,
|
||||
UnionResStaticValidatedAs<StaticVariantValues>
|
||||
UnionResStaticValidatedAs<StaticVariantValues>,
|
||||
OuterType
|
||||
>
|
||||
static dynamicUnion<
|
||||
VariantValues extends {
|
||||
@@ -1092,35 +1197,40 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
spec: InputSpec<any>
|
||||
}
|
||||
},
|
||||
OuterType = unknown,
|
||||
>(
|
||||
getA: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
}>,
|
||||
validator: Parser<unknown, unknown> = any,
|
||||
) {
|
||||
return new Value<UnionRes<VariantValues>, typeof validator._TYPE>(
|
||||
async (options) => {
|
||||
const newValues = await getA(options)
|
||||
const built = await newValues.variants.build(options as any)
|
||||
return {
|
||||
spec: {
|
||||
type: 'union' as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...newValues,
|
||||
variants: built.spec,
|
||||
immutable: false,
|
||||
},
|
||||
validator: built.validator,
|
||||
}
|
||||
getA: LazyBuild<
|
||||
{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
variants: Variants<VariantValues>
|
||||
default: keyof VariantValues & string
|
||||
disabled: string[] | false | string
|
||||
},
|
||||
validator,
|
||||
)
|
||||
OuterType
|
||||
>,
|
||||
validator: z.ZodType<unknown> = z.any(),
|
||||
) {
|
||||
return new Value<
|
||||
UnionRes<VariantValues>,
|
||||
z.infer<typeof validator>,
|
||||
OuterType
|
||||
>(async (options) => {
|
||||
const newValues = await getA(options)
|
||||
const built = await newValues.variants.build(options as any)
|
||||
return {
|
||||
spec: {
|
||||
type: 'union' as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...newValues,
|
||||
variants: built.spec,
|
||||
immutable: false,
|
||||
},
|
||||
validator: built.validator,
|
||||
}
|
||||
}, validator)
|
||||
}
|
||||
/**
|
||||
* @description Presents an interface to add/remove/edit items in a list.
|
||||
@@ -1196,10 +1306,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
hiddenExample: Value.hidden(),
|
||||
* ```
|
||||
*/
|
||||
static hidden<T>(): Value<T, unknown>
|
||||
static hidden<T>(parser: Parser<unknown, T>): Value<T>
|
||||
static hidden<T>(parser: Parser<unknown, T> = any) {
|
||||
return new Value<T, typeof parser._TYPE>(async () => {
|
||||
static hidden<T>(): Value<T>
|
||||
static hidden<T>(parser: z.ZodType<T>): Value<T>
|
||||
static hidden<T>(parser: z.ZodType<T> = z.any()) {
|
||||
return new Value<T, z.infer<typeof parser>>(async () => {
|
||||
return {
|
||||
spec: {
|
||||
type: 'hidden' as const,
|
||||
@@ -1216,8 +1326,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
hiddenExample: Value.hidden(),
|
||||
* ```
|
||||
*/
|
||||
static dynamicHidden<T>(getParser: LazyBuild<Parser<unknown, T>>) {
|
||||
return new Value<T, unknown>(async (options) => {
|
||||
static dynamicHidden<T, OuterType = unknown>(
|
||||
getParser: LazyBuild<z.ZodType<T>, OuterType>,
|
||||
) {
|
||||
return new Value<T, T, OuterType>(async (options) => {
|
||||
const validator = await getParser(options)
|
||||
return {
|
||||
spec: {
|
||||
@@ -1225,16 +1337,41 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
} as ValueSpecHidden,
|
||||
validator,
|
||||
}
|
||||
}, any)
|
||||
}, z.any())
|
||||
}
|
||||
|
||||
map<U>(fn: (value: StaticValidatedAs) => U): Value<U> {
|
||||
return new Value(async (effects) => {
|
||||
const built = await this.build(effects)
|
||||
/**
|
||||
* Returns a new Value that produces the same field spec but with `disabled` set to the given message.
|
||||
* The field remains in the form but cannot be edited by the user.
|
||||
*
|
||||
* @param message - The reason the field is disabled, displayed to the user
|
||||
*/
|
||||
withDisabled(message: string): Value<Type, StaticValidatedAs, OuterType> {
|
||||
const original = this
|
||||
const v = new Value<Type, StaticValidatedAs, OuterType>(async (options) => {
|
||||
const built = await original.build(options)
|
||||
return {
|
||||
spec: { ...built.spec, disabled: message } as ValueSpec,
|
||||
validator: built.validator,
|
||||
}
|
||||
}, this.validator)
|
||||
v._objectSpec = this._objectSpec
|
||||
return v
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the validated output value using a mapping function.
|
||||
* The form field itself remains unchanged, but the value is transformed after validation.
|
||||
*
|
||||
* @param fn - A function to transform the validated value
|
||||
*/
|
||||
map<U>(fn: (value: StaticValidatedAs) => U): Value<U, U, OuterType> {
|
||||
return new Value<U, U, OuterType>(async (options) => {
|
||||
const built = await this.build(options)
|
||||
return {
|
||||
spec: built.spec,
|
||||
validator: built.validator.map(fn),
|
||||
validator: built.validator.transform(fn),
|
||||
}
|
||||
}, this.validator.map(fn))
|
||||
}, this.validator.transform(fn))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,13 @@ import {
|
||||
ExtractInputSpecType,
|
||||
ExtractInputSpecStaticValidatedAs,
|
||||
} from './inputSpec'
|
||||
import { Parser, any, anyOf, literal, object } from 'ts-matches'
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* The runtime result type of a discriminated union form field.
|
||||
* Contains `selection` (the chosen variant key), `value` (the variant's form data),
|
||||
* and optionally `other` (partial data from previously selected variants).
|
||||
*/
|
||||
export type UnionRes<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
@@ -28,6 +33,7 @@ export type UnionRes<
|
||||
}
|
||||
}[K]
|
||||
|
||||
/** Like {@link UnionRes} but using the static (Zod-inferred) validated types. */
|
||||
export type UnionResStaticValidatedAs<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
@@ -103,18 +109,26 @@ export class Variants<
|
||||
spec: InputSpec<any, any>
|
||||
}
|
||||
},
|
||||
OuterType = unknown,
|
||||
> {
|
||||
private constructor(
|
||||
public build: LazyBuild<{
|
||||
spec: ValueSpecUnion['variants']
|
||||
validator: Parser<unknown, UnionRes<VariantValues>>
|
||||
}>,
|
||||
public readonly validator: Parser<
|
||||
unknown,
|
||||
public build: LazyBuild<
|
||||
{
|
||||
spec: ValueSpecUnion['variants']
|
||||
validator: z.ZodType<UnionRes<VariantValues>>
|
||||
},
|
||||
OuterType
|
||||
>,
|
||||
public readonly validator: z.ZodType<
|
||||
UnionResStaticValidatedAs<VariantValues>
|
||||
>,
|
||||
) {}
|
||||
readonly _TYPE: UnionRes<VariantValues> = null as any
|
||||
/**
|
||||
* Creates a `Variants` instance from a record mapping variant keys to their display name and form spec.
|
||||
*
|
||||
* @param a - A record of `{ name: string, spec: InputSpec }` entries, one per variant
|
||||
*/
|
||||
static of<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
@@ -124,8 +138,7 @@ export class Variants<
|
||||
},
|
||||
>(a: VariantValues) {
|
||||
const staticValidators = {} as {
|
||||
[K in keyof VariantValues]: Parser<
|
||||
unknown,
|
||||
[K in keyof VariantValues]: z.ZodType<
|
||||
ExtractInputSpecStaticValidatedAs<VariantValues[K]['spec']>
|
||||
>
|
||||
}
|
||||
@@ -133,16 +146,20 @@ export class Variants<
|
||||
const value = a[key]
|
||||
staticValidators[key] = value.spec.validator
|
||||
}
|
||||
const other = object(
|
||||
Object.fromEntries(
|
||||
Object.entries(staticValidators).map(([k, v]) => [k, any.optional()]),
|
||||
),
|
||||
).optional()
|
||||
const other = z
|
||||
.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(staticValidators).map(([k, v]) => [
|
||||
k,
|
||||
z.any().optional(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.optional()
|
||||
return new Variants<VariantValues>(
|
||||
async (options) => {
|
||||
const validators = {} as {
|
||||
[K in keyof VariantValues]: Parser<
|
||||
unknown,
|
||||
[K in keyof VariantValues]: z.ZodType<
|
||||
ExtractInputSpecType<VariantValues[K]['spec']>
|
||||
>
|
||||
}
|
||||
@@ -161,32 +178,37 @@ export class Variants<
|
||||
}
|
||||
validators[key] = built.validator
|
||||
}
|
||||
const other = object(
|
||||
Object.fromEntries(
|
||||
Object.entries(validators).map(([k, v]) => [k, any.optional()]),
|
||||
),
|
||||
).optional()
|
||||
const other = z
|
||||
.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(validators).map(([k, v]) => [
|
||||
k,
|
||||
z.any().optional(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.optional()
|
||||
return {
|
||||
spec: variants,
|
||||
validator: anyOf(
|
||||
...Object.entries(validators).map(([k, v]) =>
|
||||
object({
|
||||
selection: literal(k),
|
||||
validator: z.union(
|
||||
Object.entries(validators).map(([k, v]) =>
|
||||
z.object({
|
||||
selection: z.literal(k),
|
||||
value: v,
|
||||
other,
|
||||
}),
|
||||
),
|
||||
) as [z.ZodObject<any>, z.ZodObject<any>, ...z.ZodObject<any>[]],
|
||||
) as any,
|
||||
}
|
||||
},
|
||||
anyOf(
|
||||
...Object.entries(staticValidators).map(([k, v]) =>
|
||||
object({
|
||||
selection: literal(k),
|
||||
z.union(
|
||||
Object.entries(staticValidators).map(([k, v]) =>
|
||||
z.object({
|
||||
selection: z.literal(k),
|
||||
value: v,
|
||||
other,
|
||||
}),
|
||||
),
|
||||
) as [z.ZodObject<any>, z.ZodObject<any>, ...z.ZodObject<any>[]],
|
||||
) as any,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,42 +5,124 @@ import { Value } from './builder/value'
|
||||
import { Variants } from './builder/variants'
|
||||
|
||||
/**
|
||||
* Base SMTP settings, to be used by StartOS for system wide SMTP
|
||||
* Creates an SMTP field spec with provider-specific defaults pre-filled.
|
||||
*/
|
||||
export const customSmtp: InputSpec<SmtpValue> = InputSpec.of<
|
||||
InputSpecOf<SmtpValue>
|
||||
>({
|
||||
server: Value.text({
|
||||
name: 'SMTP Server',
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
port: Value.number({
|
||||
name: 'Port',
|
||||
required: true,
|
||||
default: 587,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
integer: true,
|
||||
}),
|
||||
from: Value.text({
|
||||
name: 'From Address',
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'Example Name <test@example.com>',
|
||||
inputmode: 'email',
|
||||
patterns: [Patterns.emailWithName],
|
||||
}),
|
||||
login: Value.text({
|
||||
name: 'Login',
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
password: Value.text({
|
||||
name: 'Password',
|
||||
required: false,
|
||||
default: null,
|
||||
masked: true,
|
||||
function smtpFields(
|
||||
defaults: {
|
||||
host?: string
|
||||
port?: number
|
||||
security?: 'starttls' | 'tls'
|
||||
} = {},
|
||||
): InputSpec<SmtpValue> {
|
||||
return InputSpec.of<InputSpecOf<SmtpValue>>({
|
||||
host: Value.text({
|
||||
name: 'Host',
|
||||
required: true,
|
||||
default: defaults.host ?? null,
|
||||
placeholder: 'smtp.example.com',
|
||||
}),
|
||||
port: Value.number({
|
||||
name: 'Port',
|
||||
required: true,
|
||||
default: defaults.port ?? 587,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
integer: true,
|
||||
}),
|
||||
security: Value.select({
|
||||
name: 'Connection Security',
|
||||
default: defaults.security ?? 'starttls',
|
||||
values: {
|
||||
starttls: 'STARTTLS',
|
||||
tls: 'TLS',
|
||||
},
|
||||
}),
|
||||
from: Value.text({
|
||||
name: 'From Address',
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'Example Name <test@example.com>',
|
||||
patterns: [Patterns.emailWithName],
|
||||
}),
|
||||
username: Value.text({
|
||||
name: 'Username',
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
password: Value.text({
|
||||
name: 'Password',
|
||||
required: false,
|
||||
default: null,
|
||||
masked: true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Base SMTP settings with no provider-specific defaults.
|
||||
*/
|
||||
export const customSmtp = smtpFields()
|
||||
|
||||
/**
|
||||
* Provider presets for SMTP configuration.
|
||||
* Each variant has SMTP fields pre-filled with the provider's recommended settings.
|
||||
*/
|
||||
export const smtpProviderVariants = Variants.of({
|
||||
gmail: {
|
||||
name: 'Gmail',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
ses: {
|
||||
name: 'Amazon SES',
|
||||
spec: smtpFields({
|
||||
host: 'email-smtp.us-east-1.amazonaws.com',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
sendgrid: {
|
||||
name: 'SendGrid',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.sendgrid.net',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
mailgun: {
|
||||
name: 'Mailgun',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.mailgun.org',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
protonmail: {
|
||||
name: 'Proton Mail',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.protonmail.ch',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
other: {
|
||||
name: 'Other',
|
||||
spec: customSmtp,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* System SMTP settings with provider presets.
|
||||
* Wraps smtpProviderVariants in a union for use by the system email settings page.
|
||||
*/
|
||||
export const systemSmtpSpec = InputSpec.of({
|
||||
provider: Value.union({
|
||||
name: 'Provider',
|
||||
default: null as any,
|
||||
variants: smtpProviderVariants,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -55,19 +137,24 @@ const smtpVariants = Variants.of({
|
||||
'A custom from address for this service. If not provided, the system from address will be used.',
|
||||
required: false,
|
||||
default: null,
|
||||
placeholder: '<name>test@example.com',
|
||||
inputmode: 'email',
|
||||
patterns: [Patterns.email],
|
||||
placeholder: 'Name <test@example.com>',
|
||||
patterns: [Patterns.emailWithName],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
custom: {
|
||||
name: 'Custom Credentials',
|
||||
spec: customSmtp,
|
||||
spec: InputSpec.of({
|
||||
provider: Value.union({
|
||||
name: 'Provider',
|
||||
default: null as any,
|
||||
variants: smtpProviderVariants,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
/**
|
||||
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
|
||||
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings with provider presets
|
||||
*/
|
||||
export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => {
|
||||
const smtp = await new GetSystemSmtp(effects).once()
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
/**
|
||||
* A record mapping field keys to their {@link ValueSpec} definitions.
|
||||
* This is the root shape of a dynamic form specification — it defines the complete set
|
||||
* of configurable fields for a service or action.
|
||||
*/
|
||||
export type InputSpec = Record<string, ValueSpec>
|
||||
/**
|
||||
* The discriminator for all supported form field types.
|
||||
*/
|
||||
export type ValueType =
|
||||
| 'text'
|
||||
| 'textarea'
|
||||
@@ -13,6 +21,7 @@ export type ValueType =
|
||||
| 'file'
|
||||
| 'union'
|
||||
| 'hidden'
|
||||
/** Union of all concrete form field spec types. Discriminate on the `type` field. */
|
||||
export type ValueSpec = ValueSpecOf<ValueType>
|
||||
/** core spec types. These types provide the metadata for performing validations */
|
||||
// prettier-ignore
|
||||
@@ -32,37 +41,56 @@ export type ValueSpecOf<T extends ValueType> =
|
||||
T extends "hidden" ? ValueSpecHidden :
|
||||
never
|
||||
|
||||
/** Spec for a single-line text input field. */
|
||||
export type ValueSpecText = {
|
||||
/** Display label for the field. */
|
||||
name: string
|
||||
/** Optional help text displayed below the field. */
|
||||
description: string | null
|
||||
/** Optional warning message displayed to the user. */
|
||||
warning: string | null
|
||||
|
||||
type: 'text'
|
||||
/** Regex patterns used to validate the input value. */
|
||||
patterns: Pattern[]
|
||||
/** Minimum character length, or `null` for no minimum. */
|
||||
minLength: number | null
|
||||
/** Maximum character length, or `null` for no maximum. */
|
||||
maxLength: number | null
|
||||
/** Whether the field should obscure input (e.g. for passwords). */
|
||||
masked: boolean
|
||||
|
||||
/** HTML input mode hint for mobile keyboards. */
|
||||
inputmode: 'text' | 'email' | 'tel' | 'url'
|
||||
/** Placeholder text shown when the field is empty. */
|
||||
placeholder: string | null
|
||||
|
||||
/** Whether the field must have a value. */
|
||||
required: boolean
|
||||
/** Default value, which may be a literal string or a {@link RandomString} generation spec. */
|
||||
default: DefaultString | null
|
||||
/** `false` if editable, or a string message explaining why the field is disabled. */
|
||||
disabled: false | string
|
||||
/** If set, provides a "generate" button that fills the field with a random string matching this spec. */
|
||||
generate: null | RandomString
|
||||
/** Whether the field value cannot be changed after initial configuration. */
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a multi-line textarea input field. */
|
||||
export type ValueSpecTextarea = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'textarea'
|
||||
/** Regex patterns used to validate the input value. */
|
||||
patterns: Pattern[]
|
||||
placeholder: string | null
|
||||
minLength: number | null
|
||||
maxLength: number | null
|
||||
/** Minimum number of visible rows. */
|
||||
minRows: number
|
||||
/** Maximum number of visible rows before scrolling. */
|
||||
maxRows: number
|
||||
required: boolean
|
||||
default: string | null
|
||||
@@ -70,12 +98,18 @@ export type ValueSpecTextarea = {
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/** Spec for a numeric input field. */
|
||||
export type ValueSpecNumber = {
|
||||
type: 'number'
|
||||
/** Minimum allowed value, or `null` for unbounded. */
|
||||
min: number | null
|
||||
/** Maximum allowed value, or `null` for unbounded. */
|
||||
max: number | null
|
||||
/** Whether only whole numbers are accepted. */
|
||||
integer: boolean
|
||||
/** Step increment for the input spinner, or `null` for any precision. */
|
||||
step: number | null
|
||||
/** Display label for the unit (e.g. `"MB"`, `"seconds"`), shown next to the field. */
|
||||
units: string | null
|
||||
placeholder: string | null
|
||||
name: string
|
||||
@@ -86,6 +120,7 @@ export type ValueSpecNumber = {
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a browser-native color picker field. */
|
||||
export type ValueSpecColor = {
|
||||
name: string
|
||||
description: string | null
|
||||
@@ -93,34 +128,44 @@ export type ValueSpecColor = {
|
||||
|
||||
type: 'color'
|
||||
required: boolean
|
||||
/** Default hex color string (e.g. `"#ff0000"`), or `null`. */
|
||||
default: string | null
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a date, time, or datetime input field. */
|
||||
export type ValueSpecDatetime = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'datetime'
|
||||
required: boolean
|
||||
/** Controls which kind of picker is displayed. */
|
||||
inputmode: 'date' | 'time' | 'datetime-local'
|
||||
/** Minimum selectable date/time as an ISO string, or `null`. */
|
||||
min: string | null
|
||||
/** Maximum selectable date/time as an ISO string, or `null`. */
|
||||
max: string | null
|
||||
default: string | null
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a single-select field displayed as radio buttons in a modal. */
|
||||
export type ValueSpecSelect = {
|
||||
/** Map of option keys to display labels. */
|
||||
values: Record<string, string>
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'select'
|
||||
default: string | null
|
||||
/** `false` if all enabled, a string disabling the whole field, or an array of disabled option keys. */
|
||||
disabled: false | string | string[]
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a multi-select field displayed as checkboxes in a modal. */
|
||||
export type ValueSpecMultiselect = {
|
||||
/** Map of option keys to display labels. */
|
||||
values: Record<string, string>
|
||||
|
||||
name: string
|
||||
@@ -128,12 +173,17 @@ export type ValueSpecMultiselect = {
|
||||
warning: string | null
|
||||
|
||||
type: 'multiselect'
|
||||
/** Minimum number of selections required, or `null`. */
|
||||
minLength: number | null
|
||||
/** Maximum number of selections allowed, or `null`. */
|
||||
maxLength: number | null
|
||||
/** `false` if all enabled, a string disabling the whole field, or an array of disabled option keys. */
|
||||
disabled: false | string | string[]
|
||||
/** Array of option keys selected by default. */
|
||||
default: string[]
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a boolean toggle (on/off switch). */
|
||||
export type ValueSpecToggle = {
|
||||
name: string
|
||||
description: string | null
|
||||
@@ -144,57 +194,81 @@ export type ValueSpecToggle = {
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
}
|
||||
/**
|
||||
* Spec for a discriminated union field — displays a dropdown for variant selection,
|
||||
* and each variant can have its own nested sub-form.
|
||||
*/
|
||||
export type ValueSpecUnion = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'union'
|
||||
/** Map of variant keys to their display name and nested form spec. */
|
||||
variants: Record<
|
||||
string,
|
||||
{
|
||||
/** Display name for this variant in the dropdown. */
|
||||
name: string
|
||||
/** Nested form spec shown when this variant is selected. */
|
||||
spec: InputSpec
|
||||
}
|
||||
>
|
||||
/** `false` if all enabled, a string disabling the whole field, or an array of disabled variant keys. */
|
||||
disabled: false | string | string[]
|
||||
default: string | null
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a file upload input field. */
|
||||
export type ValueSpecFile = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'file'
|
||||
/** Allowed file extensions (e.g. `[".pem", ".crt"]`). */
|
||||
extensions: string[]
|
||||
required: boolean
|
||||
}
|
||||
/** Spec for a collapsible grouping of nested fields (a "sub-form"). */
|
||||
export type ValueSpecObject = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'object'
|
||||
/** The nested form spec containing this object's fields. */
|
||||
spec: InputSpec
|
||||
}
|
||||
/** Spec for a hidden field — not displayed to the user but included in the form data. */
|
||||
export type ValueSpecHidden = {
|
||||
type: 'hidden'
|
||||
}
|
||||
/** The two supported list item types. */
|
||||
export type ListValueSpecType = 'text' | 'object'
|
||||
/** Maps a {@link ListValueSpecType} to its concrete list item spec. */
|
||||
// prettier-ignore
|
||||
export type ListValueSpecOf<T extends ListValueSpecType> =
|
||||
export type ListValueSpecOf<T extends ListValueSpecType> =
|
||||
T extends "text" ? ListValueSpecText :
|
||||
T extends "object" ? ListValueSpecObject :
|
||||
never
|
||||
/** A list field spec — union of text-list and object-list variants. */
|
||||
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
|
||||
/**
|
||||
* Spec for a list field — an interface to add, remove, and edit items in an ordered collection.
|
||||
* The `spec` field determines whether list items are text strings or structured objects.
|
||||
*/
|
||||
export type ValueSpecListOf<T extends ListValueSpecType> = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'list'
|
||||
/** The item spec — determines whether this is a list of text values or objects. */
|
||||
spec: ListValueSpecOf<T>
|
||||
/** Minimum number of items, or `null` for no minimum. */
|
||||
minLength: number | null
|
||||
/** Maximum number of items, or `null` for no maximum. */
|
||||
maxLength: number | null
|
||||
disabled: false | string
|
||||
/** Default list items to populate on creation. */
|
||||
default:
|
||||
| string[]
|
||||
| DefaultString[]
|
||||
@@ -203,10 +277,14 @@ export type ValueSpecListOf<T extends ListValueSpecType> = {
|
||||
| readonly DefaultString[]
|
||||
| readonly Record<string, unknown>[]
|
||||
}
|
||||
/** A regex validation pattern with a human-readable description of what it enforces. */
|
||||
export type Pattern = {
|
||||
/** The regex pattern string (without delimiters). */
|
||||
regex: string
|
||||
/** A user-facing explanation shown when validation fails (e.g. `"Must be a valid email"`). */
|
||||
description: string
|
||||
}
|
||||
/** Spec for text items within a list field. */
|
||||
export type ListValueSpecText = {
|
||||
type: 'text'
|
||||
patterns: Pattern[]
|
||||
@@ -218,13 +296,24 @@ export type ListValueSpecText = {
|
||||
inputmode: 'text' | 'email' | 'tel' | 'url'
|
||||
placeholder: string | null
|
||||
}
|
||||
/** Spec for object items within a list field. */
|
||||
export type ListValueSpecObject = {
|
||||
type: 'object'
|
||||
/** The form spec for each object item. */
|
||||
spec: InputSpec
|
||||
/** Defines how uniqueness is determined among list items. */
|
||||
uniqueBy: UniqueBy
|
||||
/** An expression used to generate the display string for each item in the list summary (e.g. a key path). */
|
||||
displayAs: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes how list items determine uniqueness.
|
||||
* - `null`: no uniqueness constraint
|
||||
* - `string`: unique by a specific field key
|
||||
* - `{ any: UniqueBy[] }`: unique if any of the sub-constraints match
|
||||
* - `{ all: UniqueBy[] }`: unique if all sub-constraints match together
|
||||
*/
|
||||
export type UniqueBy =
|
||||
| null
|
||||
| string
|
||||
@@ -234,12 +323,21 @@ export type UniqueBy =
|
||||
| {
|
||||
all: readonly UniqueBy[] | UniqueBy[]
|
||||
}
|
||||
/** A default value that is either a literal string or a {@link RandomString} generation spec. */
|
||||
export type DefaultString = string | RandomString
|
||||
/** Spec for generating a random string — used for default passwords, API keys, etc. */
|
||||
export type RandomString = {
|
||||
/** The character set to draw from (e.g. `"a-zA-Z0-9"`). */
|
||||
charset: string
|
||||
/** The length of the generated string. */
|
||||
len: number
|
||||
}
|
||||
// sometimes the type checker needs just a little bit of help
|
||||
/**
|
||||
* Type guard that narrows a {@link ValueSpec} to a {@link ValueSpecListOf} of a specific item type.
|
||||
*
|
||||
* @param t - The value spec to check
|
||||
* @param s - The list item type to narrow to (`"text"` or `"object"`)
|
||||
*/
|
||||
export function isValueSpecListOf<S extends ListValueSpecType>(
|
||||
t: ValueSpec,
|
||||
s: S,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ExtractInputSpecType } from './input/builder/inputSpec'
|
||||
import * as T from '../types'
|
||||
import { once } from '../util'
|
||||
import { InitScript } from '../inits'
|
||||
import { Parser } from 'ts-matches'
|
||||
import { z } from 'zod'
|
||||
|
||||
type MaybeInputSpec<Type> = {} extends Type ? null : InputSpec<Type>
|
||||
export type Run<A extends Record<string, any>> = (options: {
|
||||
@@ -13,12 +13,15 @@ export type Run<A extends Record<string, any>> = (options: {
|
||||
}) => Promise<(T.ActionResult & { version: '1' }) | null | void | undefined>
|
||||
export type GetInput<A extends Record<string, any>> = (options: {
|
||||
effects: T.Effects
|
||||
prefill: T.DeepPartial<A> | null
|
||||
}) => Promise<null | void | undefined | T.DeepPartial<A>>
|
||||
|
||||
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
|
||||
function callMaybeFn<T>(
|
||||
maybeFn: MaybeFn<T>,
|
||||
options: { effects: T.Effects },
|
||||
export type MaybeFn<T, Opts = { effects: T.Effects }> =
|
||||
| T
|
||||
| ((options: Opts) => Promise<T>)
|
||||
function callMaybeFn<T, Opts = { effects: T.Effects }>(
|
||||
maybeFn: MaybeFn<T, Opts>,
|
||||
options: Opts,
|
||||
): Promise<T> {
|
||||
if (maybeFn instanceof Function) {
|
||||
return maybeFn(options)
|
||||
@@ -51,12 +54,18 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
readonly _INPUT: Type = null as any as Type
|
||||
private prevInputSpec: Record<
|
||||
string,
|
||||
{ spec: T.inputSpecTypes.InputSpec; validator: Parser<unknown, Type> }
|
||||
{ spec: T.inputSpecTypes.InputSpec; validator: z.ZodType<Type> }
|
||||
> = {}
|
||||
private constructor(
|
||||
readonly id: Id,
|
||||
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
|
||||
private readonly inputSpec: MaybeInputSpec<Type>,
|
||||
private readonly inputSpec: MaybeFn<
|
||||
MaybeInputSpec<Type>,
|
||||
{
|
||||
effects: T.Effects
|
||||
prefill: unknown | null
|
||||
}
|
||||
>,
|
||||
private readonly getInputFn: GetInput<Type>,
|
||||
private readonly runFn: Run<Type>,
|
||||
) {}
|
||||
@@ -66,7 +75,13 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, 'hasInput'>>,
|
||||
inputSpec: InputSpecType,
|
||||
inputSpec: MaybeFn<
|
||||
InputSpecType,
|
||||
{
|
||||
effects: T.Effects
|
||||
prefill: unknown | null
|
||||
}
|
||||
>,
|
||||
getInput: GetInput<ExtractInputSpecType<InputSpecType>>,
|
||||
run: Run<ExtractInputSpecType<InputSpecType>>,
|
||||
): Action<Id, ExtractInputSpecType<InputSpecType>> {
|
||||
@@ -104,12 +119,18 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
await options.effects.action.export({ id: this.id, metadata })
|
||||
return metadata
|
||||
}
|
||||
async getInput(options: { effects: T.Effects }): Promise<T.ActionInput> {
|
||||
async getInput(options: {
|
||||
effects: T.Effects
|
||||
prefill: T.DeepPartial<Type> | null
|
||||
}): Promise<T.ActionInput> {
|
||||
let spec = {}
|
||||
if (this.inputSpec) {
|
||||
const built = await this.inputSpec.build(options)
|
||||
this.prevInputSpec[options.effects.eventId!] = built
|
||||
spec = built.spec
|
||||
const inputSpec = await callMaybeFn(this.inputSpec, options)
|
||||
const built = await inputSpec?.build(options)
|
||||
if (built) {
|
||||
this.prevInputSpec[options.effects.eventId!] = built
|
||||
spec = built.spec
|
||||
}
|
||||
}
|
||||
return {
|
||||
eventId: options.effects.eventId!,
|
||||
@@ -133,7 +154,7 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
`getActionInput has not been called for EventID ${options.effects.eventId}`,
|
||||
)
|
||||
}
|
||||
options.input = prevInputSpec.validator.unsafeCast(options.input)
|
||||
options.input = prevInputSpec.validator.parse(options.input)
|
||||
spec = prevInputSpec.spec
|
||||
}
|
||||
return (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,17 @@
|
||||
import { DeepMap } from 'deep-equality-data-structures'
|
||||
import * as P from './exver'
|
||||
|
||||
/**
|
||||
* Compile-time utility type that validates a version string literal conforms to semver format.
|
||||
*
|
||||
* Resolves to `unknown` if valid, `never` if invalid. Used with {@link testTypeVersion}.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Valid = ValidateVersion<"1.2.3"> // unknown (valid)
|
||||
* type Invalid = ValidateVersion<"-3"> // never (invalid)
|
||||
* ```
|
||||
*/
|
||||
// prettier-ignore
|
||||
export type ValidateVersion<T extends String> =
|
||||
T extends `-${infer A}` ? never :
|
||||
@@ -9,12 +20,32 @@ T extends `${infer A}-${string}` ? ValidateVersion<A> :
|
||||
T extends `${bigint}.${infer A}` ? ValidateVersion<A> :
|
||||
never
|
||||
|
||||
/**
|
||||
* Compile-time utility type that validates an extended version string literal.
|
||||
*
|
||||
* Extended versions have the format `upstream:downstream` or `#flavor:upstream:downstream`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Valid = ValidateExVer<"1.2.3:0"> // valid
|
||||
* type Flavored = ValidateExVer<"#bitcoin:1.0:0"> // valid
|
||||
* type Bad = ValidateExVer<"1.2-3"> // never (invalid)
|
||||
* ```
|
||||
*/
|
||||
// prettier-ignore
|
||||
export type ValidateExVer<T extends string> =
|
||||
T extends `#${string}:${infer A}:${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
|
||||
T extends `${infer A}:${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
|
||||
never
|
||||
|
||||
/**
|
||||
* Validates a tuple of extended version string literals at compile time.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Valid = ValidateExVers<["1.0:0", "2.0:0"]> // valid
|
||||
* ```
|
||||
*/
|
||||
// prettier-ignore
|
||||
export type ValidateExVers<T> =
|
||||
T extends [] ? unknown[] :
|
||||
@@ -460,6 +491,28 @@ class VersionRangeTable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a parsed version range expression used to match against {@link Version} or {@link ExtendedVersion} values.
|
||||
*
|
||||
* Version ranges support standard comparison operators (`=`, `>`, `<`, `>=`, `<=`, `!=`),
|
||||
* caret (`^`) and tilde (`~`) ranges, boolean logic (`&&`, `||`, `!`), and flavor matching (`#flavor`).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const range = VersionRange.parse(">=1.0.0:0 && <2.0.0:0")
|
||||
* const version = ExtendedVersion.parse("1.5.0:0")
|
||||
* console.log(range.satisfiedBy(version)) // true
|
||||
*
|
||||
* // Combine ranges with boolean logic
|
||||
* const combined = VersionRange.and(
|
||||
* VersionRange.parse(">=1.0:0"),
|
||||
* VersionRange.parse("<3.0:0"),
|
||||
* )
|
||||
*
|
||||
* // Match a specific flavor
|
||||
* const flavored = VersionRange.parse("#bitcoin")
|
||||
* ```
|
||||
*/
|
||||
export class VersionRange {
|
||||
constructor(public atom: Anchor | And | Or | Not | P.Any | P.None | Flavor) {}
|
||||
|
||||
@@ -488,6 +541,7 @@ export class VersionRange {
|
||||
}
|
||||
}
|
||||
|
||||
/** Serializes this version range back to its canonical string representation. */
|
||||
toString(): string {
|
||||
switch (this.atom.type) {
|
||||
case 'Anchor':
|
||||
@@ -563,38 +617,69 @@ export class VersionRange {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a version range string into a `VersionRange`.
|
||||
*
|
||||
* @param range - A version range expression, e.g. `">=1.0.0:0 && <2.0.0:0"`, `"^1.2:0"`, `"*"`
|
||||
* @returns The parsed `VersionRange`
|
||||
* @throws If the string is not a valid version range expression
|
||||
*/
|
||||
static parse(range: string): VersionRange {
|
||||
return VersionRange.parseRange(
|
||||
P.parse(range, { startRule: 'VersionRange' }),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a version range from a comparison operator and an {@link ExtendedVersion}.
|
||||
*
|
||||
* @param operator - One of `"="`, `">"`, `"<"`, `">="`, `"<="`, `"!="`, `"^"`, `"~"`
|
||||
* @param version - The version to compare against
|
||||
*/
|
||||
static anchor(operator: P.CmpOp, version: ExtendedVersion) {
|
||||
return new VersionRange({ type: 'Anchor', operator, version })
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a version range that matches only versions with the specified flavor.
|
||||
*
|
||||
* @param flavor - The flavor string to match, or `null` for the default (unflavored) variant
|
||||
*/
|
||||
static flavor(flavor: string | null) {
|
||||
return new VersionRange({ type: 'Flavor', flavor })
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a legacy "emver" format version range string.
|
||||
*
|
||||
* @param range - A version range in the legacy emver format
|
||||
* @returns The parsed `VersionRange`
|
||||
*/
|
||||
static parseEmver(range: string): VersionRange {
|
||||
return VersionRange.parseRange(
|
||||
P.parse(range, { startRule: 'EmverVersionRange' }),
|
||||
)
|
||||
}
|
||||
|
||||
/** Returns the intersection of this range with another (logical AND). */
|
||||
and(right: VersionRange) {
|
||||
return new VersionRange({ type: 'And', left: this, right })
|
||||
}
|
||||
|
||||
/** Returns the union of this range with another (logical OR). */
|
||||
or(right: VersionRange) {
|
||||
return new VersionRange({ type: 'Or', left: this, right })
|
||||
}
|
||||
|
||||
/** Returns the negation of this range (logical NOT). */
|
||||
not() {
|
||||
return new VersionRange({ type: 'Not', value: this })
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the logical AND (intersection) of multiple version ranges.
|
||||
* Short-circuits on `none()` and skips `any()`.
|
||||
*/
|
||||
static and(...xs: Array<VersionRange>) {
|
||||
let y = VersionRange.any()
|
||||
for (let x of xs) {
|
||||
@@ -613,6 +698,10 @@ export class VersionRange {
|
||||
return y
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the logical OR (union) of multiple version ranges.
|
||||
* Short-circuits on `any()` and skips `none()`.
|
||||
*/
|
||||
static or(...xs: Array<VersionRange>) {
|
||||
let y = VersionRange.none()
|
||||
for (let x of xs) {
|
||||
@@ -631,14 +720,21 @@ export class VersionRange {
|
||||
return y
|
||||
}
|
||||
|
||||
/** Returns a version range that matches all versions (wildcard `*`). */
|
||||
static any() {
|
||||
return new VersionRange({ type: 'Any' })
|
||||
}
|
||||
|
||||
/** Returns a version range that matches no versions (`!`). */
|
||||
static none() {
|
||||
return new VersionRange({ type: 'None' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the given version satisfies this range.
|
||||
*
|
||||
* @param version - A {@link Version} or {@link ExtendedVersion} to test
|
||||
*/
|
||||
satisfiedBy(version: Version | ExtendedVersion) {
|
||||
return version.satisfies(this)
|
||||
}
|
||||
@@ -714,29 +810,60 @@ export class VersionRange {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns `true` if any version exists that could satisfy this range. */
|
||||
satisfiable(): boolean {
|
||||
return VersionRangeTable.collapse(this.tables()) !== false
|
||||
}
|
||||
|
||||
/** Returns `true` if this range and `other` share at least one satisfying version. */
|
||||
intersects(other: VersionRange): boolean {
|
||||
return VersionRange.and(this, other).satisfiable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a canonical (simplified) form of this range using minterm expansion.
|
||||
* Useful for normalizing complex boolean expressions into a minimal representation.
|
||||
*/
|
||||
normalize(): VersionRange {
|
||||
return VersionRangeTable.minterms(this.tables())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a semantic version number with numeric segments and optional prerelease identifiers.
|
||||
*
|
||||
* Follows semver precedence rules: numeric segments are compared left-to-right,
|
||||
* and a version with prerelease identifiers has lower precedence than the same version without.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const v = Version.parse("1.2.3")
|
||||
* console.log(v.toString()) // "1.2.3"
|
||||
* console.log(v.compare(Version.parse("1.3.0"))) // "less"
|
||||
*
|
||||
* const pre = Version.parse("2.0.0-beta.1")
|
||||
* console.log(pre.compare(Version.parse("2.0.0"))) // "less" (prerelease < release)
|
||||
* ```
|
||||
*/
|
||||
export class Version {
|
||||
constructor(
|
||||
/** The numeric version segments (e.g. `[1, 2, 3]` for `"1.2.3"`). */
|
||||
public number: number[],
|
||||
/** Optional prerelease identifiers (e.g. `["beta", 1]` for `"-beta.1"`). */
|
||||
public prerelease: (string | number)[],
|
||||
) {}
|
||||
|
||||
/** Serializes this version to its string form (e.g. `"1.2.3"` or `"1.0.0-beta.1"`). */
|
||||
toString(): string {
|
||||
return `${this.number.join('.')}${this.prerelease.length > 0 ? `-${this.prerelease.join('.')}` : ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this version against another using semver precedence rules.
|
||||
*
|
||||
* @param other - The version to compare against
|
||||
* @returns `'greater'`, `'equal'`, or `'less'`
|
||||
*/
|
||||
compare(other: Version): 'greater' | 'equal' | 'less' {
|
||||
const numLen = Math.max(this.number.length, other.number.length)
|
||||
for (let i = 0; i < numLen; i++) {
|
||||
@@ -783,6 +910,11 @@ export class Version {
|
||||
return 'equal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two versions, returning a numeric value suitable for use with `Array.sort()`.
|
||||
*
|
||||
* @returns `-1` if less, `0` if equal, `1` if greater
|
||||
*/
|
||||
compareForSort(other: Version): -1 | 0 | 1 {
|
||||
switch (this.compare(other)) {
|
||||
case 'greater':
|
||||
@@ -794,11 +926,21 @@ export class Version {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a version string into a `Version` instance.
|
||||
*
|
||||
* @param version - A semver-compatible string, e.g. `"1.2.3"` or `"1.0.0-beta.1"`
|
||||
* @throws If the string is not a valid version
|
||||
*/
|
||||
static parse(version: string): Version {
|
||||
const parsed = P.parse(version, { startRule: 'Version' })
|
||||
return new Version(parsed.number, parsed.prerelease)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this version satisfies the given {@link VersionRange}.
|
||||
* Internally treats this as an unflavored {@link ExtendedVersion} with downstream `0`.
|
||||
*/
|
||||
satisfies(versionRange: VersionRange): boolean {
|
||||
return new ExtendedVersion(null, this, new Version([0], [])).satisfies(
|
||||
versionRange,
|
||||
@@ -806,18 +948,50 @@ export class Version {
|
||||
}
|
||||
}
|
||||
|
||||
// #flavor:0.1.2-beta.1:0
|
||||
/**
|
||||
* Represents an extended version with an optional flavor, an upstream version, and a downstream version.
|
||||
*
|
||||
* The format is `#flavor:upstream:downstream` (e.g. `#bitcoin:1.2.3:0`) or `upstream:downstream`
|
||||
* for unflavored versions. Flavors allow multiple variants of a package to coexist.
|
||||
*
|
||||
* - **flavor**: An optional string identifier for the variant (e.g. `"bitcoin"`, `"litecoin"`)
|
||||
* - **upstream**: The version of the upstream software being packaged
|
||||
* - **downstream**: The version of the StartOS packaging itself
|
||||
*
|
||||
* Versions with different flavors are incomparable (comparison returns `null`).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const v = ExtendedVersion.parse("#bitcoin:1.2.3:0")
|
||||
* console.log(v.flavor) // "bitcoin"
|
||||
* console.log(v.upstream) // Version { number: [1, 2, 3] }
|
||||
* console.log(v.downstream) // Version { number: [0] }
|
||||
* console.log(v.toString()) // "#bitcoin:1.2.3:0"
|
||||
*
|
||||
* const range = VersionRange.parse(">=1.0.0:0")
|
||||
* console.log(v.satisfies(range)) // true
|
||||
* ```
|
||||
*/
|
||||
export class ExtendedVersion {
|
||||
constructor(
|
||||
/** The flavor identifier (e.g. `"bitcoin"`), or `null` for unflavored versions. */
|
||||
public flavor: string | null,
|
||||
/** The upstream software version. */
|
||||
public upstream: Version,
|
||||
/** The downstream packaging version. */
|
||||
public downstream: Version,
|
||||
) {}
|
||||
|
||||
/** Serializes this extended version to its string form (e.g. `"#bitcoin:1.2.3:0"` or `"1.0.0:1"`). */
|
||||
toString(): string {
|
||||
return `${this.flavor ? `#${this.flavor}:` : ''}${this.upstream.toString()}:${this.downstream.toString()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this extended version against another.
|
||||
*
|
||||
* @returns `'greater'`, `'equal'`, `'less'`, or `null` if the flavors differ (incomparable)
|
||||
*/
|
||||
compare(other: ExtendedVersion): 'greater' | 'equal' | 'less' | null {
|
||||
if (this.flavor !== other.flavor) {
|
||||
return null
|
||||
@@ -829,6 +1003,10 @@ export class ExtendedVersion {
|
||||
return this.downstream.compare(other.downstream)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexicographic comparison — compares flavors alphabetically first, then versions.
|
||||
* Unlike {@link compare}, this never returns `null`: different flavors are ordered alphabetically.
|
||||
*/
|
||||
compareLexicographic(other: ExtendedVersion): 'greater' | 'equal' | 'less' {
|
||||
if ((this.flavor || '') > (other.flavor || '')) {
|
||||
return 'greater'
|
||||
@@ -839,6 +1017,10 @@ export class ExtendedVersion {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a numeric comparison result suitable for use with `Array.sort()`.
|
||||
* Uses lexicographic ordering (flavors sorted alphabetically, then by version).
|
||||
*/
|
||||
compareForSort(other: ExtendedVersion): 1 | 0 | -1 {
|
||||
switch (this.compareLexicographic(other)) {
|
||||
case 'greater':
|
||||
@@ -850,26 +1032,37 @@ export class ExtendedVersion {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns `true` if this version is strictly greater than `other`. Returns `false` if flavors differ. */
|
||||
greaterThan(other: ExtendedVersion): boolean {
|
||||
return this.compare(other) === 'greater'
|
||||
}
|
||||
|
||||
/** Returns `true` if this version is greater than or equal to `other`. Returns `false` if flavors differ. */
|
||||
greaterThanOrEqual(other: ExtendedVersion): boolean {
|
||||
return ['greater', 'equal'].includes(this.compare(other) as string)
|
||||
}
|
||||
|
||||
/** Returns `true` if this version equals `other` (same flavor, upstream, and downstream). */
|
||||
equals(other: ExtendedVersion): boolean {
|
||||
return this.compare(other) === 'equal'
|
||||
}
|
||||
|
||||
/** Returns `true` if this version is strictly less than `other`. Returns `false` if flavors differ. */
|
||||
lessThan(other: ExtendedVersion): boolean {
|
||||
return this.compare(other) === 'less'
|
||||
}
|
||||
|
||||
/** Returns `true` if this version is less than or equal to `other`. Returns `false` if flavors differ. */
|
||||
lessThanOrEqual(other: ExtendedVersion): boolean {
|
||||
return ['less', 'equal'].includes(this.compare(other) as string)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an extended version string into an `ExtendedVersion`.
|
||||
*
|
||||
* @param extendedVersion - A string like `"1.2.3:0"` or `"#bitcoin:1.0.0:0"`
|
||||
* @throws If the string is not a valid extended version
|
||||
*/
|
||||
static parse(extendedVersion: string): ExtendedVersion {
|
||||
const parsed = P.parse(extendedVersion, { startRule: 'ExtendedVersion' })
|
||||
return new ExtendedVersion(
|
||||
@@ -879,6 +1072,12 @@ export class ExtendedVersion {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a legacy "emver" format extended version string.
|
||||
*
|
||||
* @param extendedVersion - A version string in the legacy emver format
|
||||
* @throws If the string is not a valid emver version (error message includes the input string)
|
||||
*/
|
||||
static parseEmver(extendedVersion: string): ExtendedVersion {
|
||||
try {
|
||||
const parsed = P.parse(extendedVersion, { startRule: 'Emver' })
|
||||
@@ -1014,8 +1213,29 @@ export class ExtendedVersion {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile-time type-checking helper that validates an extended version string literal.
|
||||
* If the string is invalid, TypeScript will report a type error at the call site.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* testTypeExVer("1.2.3:0") // compiles
|
||||
* testTypeExVer("#bitcoin:1.0:0") // compiles
|
||||
* testTypeExVer("invalid") // type error
|
||||
* ```
|
||||
*/
|
||||
export const testTypeExVer = <T extends string>(t: T & ValidateExVer<T>) => t
|
||||
|
||||
/**
|
||||
* Compile-time type-checking helper that validates a version string literal.
|
||||
* If the string is invalid, TypeScript will report a type error at the call site.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* testTypeVersion("1.2.3") // compiles
|
||||
* testTypeVersion("-3") // type error
|
||||
* ```
|
||||
*/
|
||||
export const testTypeVersion = <T extends string>(t: T & ValidateVersion<T>) =>
|
||||
t
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@ export * as types from './types'
|
||||
export * as T from './types'
|
||||
export * as yaml from 'yaml'
|
||||
export * as inits from './inits'
|
||||
export * as matches from 'ts-matches'
|
||||
export { z } from './zExport'
|
||||
|
||||
export * as utils from './util'
|
||||
|
||||
@@ -2,21 +2,37 @@ import { VersionRange } from '../../../base/lib/exver'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { once } from '../util'
|
||||
|
||||
/**
|
||||
* The reason a service's init function is being called:
|
||||
* - `'install'` — first-time installation
|
||||
* - `'update'` — after a package update
|
||||
* - `'restore'` — after restoring from backup
|
||||
* - `null` — regular startup (no special lifecycle event)
|
||||
*/
|
||||
export type InitKind = 'install' | 'update' | 'restore' | null
|
||||
|
||||
/** Function signature for an init handler that runs during service startup. */
|
||||
export type InitFn<Kind extends InitKind = InitKind> = (
|
||||
effects: T.Effects,
|
||||
kind: Kind,
|
||||
) => Promise<void | null | undefined>
|
||||
|
||||
/** Object form of an init handler — implements an `init()` method. */
|
||||
export interface InitScript<Kind extends InitKind = InitKind> {
|
||||
init(effects: T.Effects, kind: Kind): Promise<void>
|
||||
}
|
||||
|
||||
/** Either an {@link InitScript} object or an {@link InitFn} function. */
|
||||
export type InitScriptOrFn<Kind extends InitKind = InitKind> =
|
||||
| InitScript<Kind>
|
||||
| InitFn<Kind>
|
||||
|
||||
/**
|
||||
* Composes multiple init handlers into a single `ExpectedExports.init`-compatible function.
|
||||
* Handlers are executed sequentially in the order provided.
|
||||
*
|
||||
* @param inits - One or more init handlers to compose
|
||||
*/
|
||||
export function setupInit(...inits: InitScriptOrFn[]): T.ExpectedExports.init {
|
||||
return async (opts) => {
|
||||
for (const idx in inits) {
|
||||
@@ -42,6 +58,7 @@ export function setupInit(...inits: InitScriptOrFn[]): T.ExpectedExports.init {
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalizes an {@link InitScriptOrFn} into an {@link InitScript} object. */
|
||||
export function setupOnInit(onInit: InitScriptOrFn): InitScript {
|
||||
return 'init' in onInit
|
||||
? onInit
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ExtendedVersion, VersionRange } from '../../../base/lib/exver'
|
||||
import * as T from '../../../base/lib/types'
|
||||
|
||||
/**
|
||||
* Function signature for an uninit handler that runs during service shutdown/uninstall.
|
||||
*/
|
||||
export type UninitFn = (
|
||||
effects: T.Effects,
|
||||
/**
|
||||
@@ -13,6 +16,7 @@ export type UninitFn = (
|
||||
target: VersionRange | ExtendedVersion | null,
|
||||
) => Promise<void | null | undefined>
|
||||
|
||||
/** Object form of an uninit handler — implements an `uninit()` method. */
|
||||
export interface UninitScript {
|
||||
uninit(
|
||||
effects: T.Effects,
|
||||
@@ -27,8 +31,15 @@ export interface UninitScript {
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
/** Either a {@link UninitScript} object or a {@link UninitFn} function. */
|
||||
export type UninitScriptOrFn = UninitScript | UninitFn
|
||||
|
||||
/**
|
||||
* Composes multiple uninit handlers into a single `ExpectedExports.uninit`-compatible function.
|
||||
* Handlers are executed sequentially in the order provided.
|
||||
*
|
||||
* @param uninits - One or more uninit handlers to compose
|
||||
*/
|
||||
export function setupUninit(
|
||||
...uninits: UninitScriptOrFn[]
|
||||
): T.ExpectedExports.uninit {
|
||||
@@ -40,6 +51,7 @@ export function setupUninit(
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalizes a {@link UninitScriptOrFn} into a {@link UninitScript} object. */
|
||||
export function setupOnUninit(onUninit: UninitScriptOrFn): UninitScript {
|
||||
return 'uninit' in onUninit
|
||||
? onUninit
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { object, string } from 'ts-matches'
|
||||
import { z } from 'zod'
|
||||
import { Effects } from '../Effects'
|
||||
import { Origin } from './Origin'
|
||||
import { AddSslOptions, BindParams } from '../osBindings'
|
||||
@@ -69,9 +69,8 @@ export type BindOptionsByProtocol =
|
||||
| BindOptionsByKnownProtocol
|
||||
| (BindOptions & { protocol: null })
|
||||
|
||||
const hasStringProtocol = object({
|
||||
protocol: string,
|
||||
}).test
|
||||
const hasStringProtocol = (v: unknown): v is { protocol: string } =>
|
||||
z.object({ protocol: z.string() }).safeParse(v).success
|
||||
|
||||
export class MultiHost {
|
||||
constructor(
|
||||
|
||||
28
sdk/base/lib/interfaces/setupExportedUrls.ts
Normal file
28
sdk/base/lib/interfaces/setupExportedUrls.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Effects, PluginHostnameInfo } from '../types'
|
||||
|
||||
export type SetExportedUrls = (opts: { effects: Effects }) => Promise<void>
|
||||
export type UpdateExportedUrls = (effects: Effects) => Promise<null>
|
||||
export type SetupExportedUrls = (fn: SetExportedUrls) => UpdateExportedUrls
|
||||
|
||||
export const setupExportedUrls: SetupExportedUrls = (fn: SetExportedUrls) => {
|
||||
return (async (effects: Effects) => {
|
||||
const urls: PluginHostnameInfo[] = []
|
||||
await fn({
|
||||
effects: {
|
||||
...effects,
|
||||
plugin: {
|
||||
...effects.plugin,
|
||||
url: {
|
||||
...effects.plugin.url,
|
||||
exportUrl: (params) => {
|
||||
urls.push(params.hostnameInfo)
|
||||
return effects.plugin.url.exportUrl(params)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await effects.plugin.url.clearUrls({ except: urls })
|
||||
return null
|
||||
}) as UpdateExportedUrls
|
||||
}
|
||||
4
sdk/base/lib/osBindings/AddPrivateDomainParams.ts
Normal file
4
sdk/base/lib/osBindings/AddPrivateDomainParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type AddPrivateDomainParams = { fqdn: string; gateway: GatewayId }
|
||||
9
sdk/base/lib/osBindings/AddPublicDomainParams.ts
Normal file
9
sdk/base/lib/osBindings/AddPublicDomainParams.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AcmeProvider } from './AcmeProvider'
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type AddPublicDomainParams = {
|
||||
fqdn: string
|
||||
acme: AcmeProvider | null
|
||||
gateway: GatewayId
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayType } from './GatewayType'
|
||||
|
||||
export type AddTunnelParams = { name: string; config: string; public: boolean }
|
||||
export type AddTunnelParams = {
|
||||
name: string
|
||||
config: string
|
||||
type: GatewayType | null
|
||||
setAsDefaultOutbound: boolean
|
||||
}
|
||||
|
||||
9
sdk/base/lib/osBindings/BackupInfo.ts
Normal file
9
sdk/base/lib/osBindings/BackupInfo.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PackageBackupInfo } from './PackageBackupInfo'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type BackupInfo = {
|
||||
version: string
|
||||
timestamp: string | null
|
||||
packageBackups: { [key: PackageId]: PackageBackupInfo }
|
||||
}
|
||||
11
sdk/base/lib/osBindings/BackupParams.ts
Normal file
11
sdk/base/lib/osBindings/BackupParams.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BackupTargetId } from './BackupTargetId'
|
||||
import type { PackageId } from './PackageId'
|
||||
import type { PasswordType } from './PasswordType'
|
||||
|
||||
export type BackupParams = {
|
||||
targetId: BackupTargetId
|
||||
oldPassword: PasswordType | null
|
||||
packageIds: Array<PackageId> | null
|
||||
password: PasswordType
|
||||
}
|
||||
9
sdk/base/lib/osBindings/BackupReport.ts
Normal file
9
sdk/base/lib/osBindings/BackupReport.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PackageBackupReport } from './PackageBackupReport'
|
||||
import type { PackageId } from './PackageId'
|
||||
import type { ServerBackupReport } from './ServerBackupReport'
|
||||
|
||||
export type BackupReport = {
|
||||
server: ServerBackupReport
|
||||
packages: { [key: PackageId]: PackageBackupReport }
|
||||
}
|
||||
17
sdk/base/lib/osBindings/BackupTarget.ts
Normal file
17
sdk/base/lib/osBindings/BackupTarget.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CifsBackupTarget } from './CifsBackupTarget'
|
||||
import type { StartOsRecoveryInfo } from './StartOsRecoveryInfo'
|
||||
|
||||
export type BackupTarget =
|
||||
| {
|
||||
type: 'disk'
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
logicalname: string
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
startOs: { [key: string]: StartOsRecoveryInfo }
|
||||
guid: string | null
|
||||
}
|
||||
| ({ type: 'cifs' } & CifsBackupTarget)
|
||||
3
sdk/base/lib/osBindings/BackupTargetId.ts
Normal file
3
sdk/base/lib/osBindings/BackupTargetId.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type BackupTargetId = string
|
||||
@@ -1,5 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BindOptions } from './BindOptions'
|
||||
import type { DerivedAddressInfo } from './DerivedAddressInfo'
|
||||
import type { NetInfo } from './NetInfo'
|
||||
|
||||
export type BindInfo = { enabled: boolean; options: BindOptions; net: NetInfo }
|
||||
export type BindInfo = {
|
||||
enabled: boolean
|
||||
options: BindOptions
|
||||
net: NetInfo
|
||||
addresses: DerivedAddressInfo
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type BindingGatewaySetEnabledParams = {
|
||||
export type BindingSetAddressEnabledParams = {
|
||||
internalPort: number
|
||||
gateway: GatewayId
|
||||
address: string
|
||||
enabled: boolean | null
|
||||
}
|
||||
4
sdk/base/lib/osBindings/Bindings.ts
Normal file
4
sdk/base/lib/osBindings/Bindings.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BindInfo } from './BindInfo'
|
||||
|
||||
export type Bindings = { [key: number]: BindInfo }
|
||||
4
sdk/base/lib/osBindings/CancelInstallParams.ts
Normal file
4
sdk/base/lib/osBindings/CancelInstallParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type CancelInstallParams = { id: PackageId }
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Celsius = number
|
||||
export type Celsius = { value: string; unit: string }
|
||||
|
||||
4
sdk/base/lib/osBindings/CheckDnsParams.ts
Normal file
4
sdk/base/lib/osBindings/CheckDnsParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type CheckDnsParams = { gateway: GatewayId }
|
||||
4
sdk/base/lib/osBindings/CheckPortParams.ts
Normal file
4
sdk/base/lib/osBindings/CheckPortParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type CheckPortParams = { port: number; gateway: GatewayId }
|
||||
9
sdk/base/lib/osBindings/CheckPortRes.ts
Normal file
9
sdk/base/lib/osBindings/CheckPortRes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CheckPortRes = {
|
||||
ip: string
|
||||
port: number
|
||||
openExternally: boolean
|
||||
openInternally: boolean
|
||||
hairpinning: boolean
|
||||
}
|
||||
8
sdk/base/lib/osBindings/CifsAddParams.ts
Normal file
8
sdk/base/lib/osBindings/CifsAddParams.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CifsAddParams = {
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string | null
|
||||
}
|
||||
10
sdk/base/lib/osBindings/CifsBackupTarget.ts
Normal file
10
sdk/base/lib/osBindings/CifsBackupTarget.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { StartOsRecoveryInfo } from './StartOsRecoveryInfo'
|
||||
|
||||
export type CifsBackupTarget = {
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
mountable: boolean
|
||||
startOs: { [key: string]: StartOsRecoveryInfo }
|
||||
}
|
||||
4
sdk/base/lib/osBindings/CifsRemoveParams.ts
Normal file
4
sdk/base/lib/osBindings/CifsRemoveParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BackupTargetId } from './BackupTargetId'
|
||||
|
||||
export type CifsRemoveParams = { id: BackupTargetId }
|
||||
10
sdk/base/lib/osBindings/CifsUpdateParams.ts
Normal file
10
sdk/base/lib/osBindings/CifsUpdateParams.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BackupTargetId } from './BackupTargetId'
|
||||
|
||||
export type CifsUpdateParams = {
|
||||
id: BackupTargetId
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string | null
|
||||
}
|
||||
9
sdk/base/lib/osBindings/ClearTaskParams.ts
Normal file
9
sdk/base/lib/osBindings/ClearTaskParams.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PackageId } from './PackageId'
|
||||
import type { ReplayId } from './ReplayId'
|
||||
|
||||
export type ClearTaskParams = {
|
||||
packageId: PackageId
|
||||
replayId: ReplayId
|
||||
force: boolean
|
||||
}
|
||||
4
sdk/base/lib/osBindings/ControlParams.ts
Normal file
4
sdk/base/lib/osBindings/ControlParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type ControlParams = { id: PackageId }
|
||||
@@ -1,7 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LocaleString } from './LocaleString'
|
||||
import type { MetadataSrc } from './MetadataSrc'
|
||||
|
||||
export type DepInfo = {
|
||||
description: string | null
|
||||
description: LocaleString | null
|
||||
optional: boolean
|
||||
} & MetadataSrc
|
||||
|
||||
17
sdk/base/lib/osBindings/DerivedAddressInfo.ts
Normal file
17
sdk/base/lib/osBindings/DerivedAddressInfo.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HostnameInfo } from './HostnameInfo'
|
||||
|
||||
export type DerivedAddressInfo = {
|
||||
/**
|
||||
* User override: enable these addresses (only for public IP & port)
|
||||
*/
|
||||
enabled: Array<string>
|
||||
/**
|
||||
* User override: disable these addresses (only for domains and private IP & port)
|
||||
*/
|
||||
disabled: Array<[string, number]>
|
||||
/**
|
||||
* COMPUTED: NetServiceData::update — all possible addresses for this binding
|
||||
*/
|
||||
available: Array<HostnameInfo>
|
||||
}
|
||||
9
sdk/base/lib/osBindings/EffectsRunActionParams.ts
Normal file
9
sdk/base/lib/osBindings/EffectsRunActionParams.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ActionId } from './ActionId'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type EffectsRunActionParams = {
|
||||
packageId?: PackageId
|
||||
actionId: ActionId
|
||||
input: any
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ErrorData = { details: string; debug: string }
|
||||
export type ErrorData = { details: string; debug: string; info: unknown }
|
||||
|
||||
4
sdk/base/lib/osBindings/ForgetGatewayParams.ts
Normal file
4
sdk/base/lib/osBindings/ForgetGatewayParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type ForgetGatewayParams = { gateway: GatewayId }
|
||||
3
sdk/base/lib/osBindings/GatewayType.ts
Normal file
3
sdk/base/lib/osBindings/GatewayType.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type GatewayType = 'inbound-outbound' | 'outbound-only'
|
||||
@@ -2,4 +2,8 @@
|
||||
import type { ActionId } from './ActionId'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type GetActionInputParams = { packageId?: PackageId; actionId: ActionId }
|
||||
export type GetActionInputParams = {
|
||||
packageId?: PackageId
|
||||
actionId: ActionId
|
||||
prefill: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
4
sdk/base/lib/osBindings/GetOutboundGatewayParams.ts
Normal file
4
sdk/base/lib/osBindings/GetOutboundGatewayParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CallbackId } from './CallbackId'
|
||||
|
||||
export type GetOutboundGatewayParams = { callback?: CallbackId }
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type GigaBytes = number
|
||||
export type GigaBytes = { value: string; unit: string }
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BindInfo } from './BindInfo'
|
||||
import type { HostnameInfo } from './HostnameInfo'
|
||||
import type { Bindings } from './Bindings'
|
||||
import type { GatewayId } from './GatewayId'
|
||||
import type { PortForward } from './PortForward'
|
||||
import type { PublicDomainConfig } from './PublicDomainConfig'
|
||||
|
||||
export type Host = {
|
||||
bindings: { [key: number]: BindInfo }
|
||||
onions: string[]
|
||||
bindings: Bindings
|
||||
publicDomains: { [key: string]: PublicDomainConfig }
|
||||
privateDomains: Array<string>
|
||||
privateDomains: { [key: string]: Array<GatewayId> }
|
||||
/**
|
||||
* COMPUTED: NetService::update
|
||||
* COMPUTED: port forwarding rules needed on gateways for public addresses to work.
|
||||
*/
|
||||
hostnameInfo: { [key: number]: Array<HostnameInfo> }
|
||||
portForwards: Array<PortForward>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayInfo } from './GatewayInfo'
|
||||
import type { IpHostname } from './IpHostname'
|
||||
import type { OnionHostname } from './OnionHostname'
|
||||
import type { HostnameMetadata } from './HostnameMetadata'
|
||||
|
||||
export type HostnameInfo =
|
||||
| { kind: 'ip'; gateway: GatewayInfo; public: boolean; hostname: IpHostname }
|
||||
| { kind: 'onion'; hostname: OnionHostname }
|
||||
export type HostnameInfo = {
|
||||
ssl: boolean
|
||||
public: boolean
|
||||
hostname: string
|
||||
port: number | null
|
||||
metadata: HostnameMetadata
|
||||
}
|
||||
|
||||
18
sdk/base/lib/osBindings/HostnameMetadata.ts
Normal file
18
sdk/base/lib/osBindings/HostnameMetadata.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ActionId } from './ActionId'
|
||||
import type { GatewayId } from './GatewayId'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type HostnameMetadata =
|
||||
| { kind: 'ipv4'; gateway: GatewayId }
|
||||
| { kind: 'ipv6'; gateway: GatewayId; scopeId: number }
|
||||
| { kind: 'mdns'; gateways: Array<GatewayId> }
|
||||
| { kind: 'private-domain'; gateways: Array<GatewayId> }
|
||||
| { kind: 'public-domain'; gateway: GatewayId }
|
||||
| {
|
||||
kind: 'plugin'
|
||||
packageId: PackageId
|
||||
removeAction: ActionId | null
|
||||
overflowActions: Array<ActionId>
|
||||
info: unknown
|
||||
}
|
||||
8
sdk/base/lib/osBindings/InfoParams.ts
Normal file
8
sdk/base/lib/osBindings/InfoParams.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BackupTargetId } from './BackupTargetId'
|
||||
|
||||
export type InfoParams = {
|
||||
targetId: BackupTargetId
|
||||
serverId: string
|
||||
password: string
|
||||
}
|
||||
4
sdk/base/lib/osBindings/InitAcmeParams.ts
Normal file
4
sdk/base/lib/osBindings/InitAcmeParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AcmeProvider } from './AcmeProvider'
|
||||
|
||||
export type InitAcmeParams = { provider: AcmeProvider; contact: Array<string> }
|
||||
@@ -1,23 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type IpHostname =
|
||||
| { kind: 'ipv4'; value: string; port: number | null; sslPort: number | null }
|
||||
| {
|
||||
kind: 'ipv6'
|
||||
value: string
|
||||
scopeId: number
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
| {
|
||||
kind: 'local'
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
| {
|
||||
kind: 'domain'
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
3
sdk/base/lib/osBindings/KillParams.ts
Normal file
3
sdk/base/lib/osBindings/KillParams.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type KillParams = { ids: Array<string> }
|
||||
@@ -1,7 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type OnionHostname = {
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
export type ListNotificationParams = {
|
||||
before: number | null
|
||||
limit: number | null
|
||||
}
|
||||
3
sdk/base/lib/osBindings/LogEntry.ts
Normal file
3
sdk/base/lib/osBindings/LogEntry.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LogEntry = { timestamp: string; message: string; bootId: string }
|
||||
4
sdk/base/lib/osBindings/LogFollowResponse.ts
Normal file
4
sdk/base/lib/osBindings/LogFollowResponse.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Guid } from './Guid'
|
||||
|
||||
export type LogFollowResponse = { startCursor: string | null; guid: Guid }
|
||||
8
sdk/base/lib/osBindings/LogResponse.ts
Normal file
8
sdk/base/lib/osBindings/LogResponse.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LogEntry } from './LogEntry'
|
||||
|
||||
export type LogResponse = {
|
||||
entries: Array<LogEntry>
|
||||
startCursor: string | null
|
||||
endCursor: string | null
|
||||
}
|
||||
8
sdk/base/lib/osBindings/LogsParams.ts
Normal file
8
sdk/base/lib/osBindings/LogsParams.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LogsParams = {
|
||||
limit?: number
|
||||
cursor?: string
|
||||
boot?: number | string
|
||||
before: boolean
|
||||
}
|
||||
@@ -8,32 +8,33 @@ import type { ImageConfig } from './ImageConfig'
|
||||
import type { ImageId } from './ImageId'
|
||||
import type { LocaleString } from './LocaleString'
|
||||
import type { PackageId } from './PackageId'
|
||||
import type { PluginId } from './PluginId'
|
||||
import type { Version } from './Version'
|
||||
import type { VolumeId } from './VolumeId'
|
||||
|
||||
export type Manifest = {
|
||||
id: PackageId
|
||||
title: string
|
||||
version: Version
|
||||
satisfies: Array<Version>
|
||||
releaseNotes: LocaleString
|
||||
canMigrateTo: string
|
||||
canMigrateFrom: string
|
||||
license: string
|
||||
wrapperRepo: string
|
||||
upstreamRepo: string
|
||||
supportSite: string
|
||||
marketingSite: string
|
||||
donationUrl: string | null
|
||||
docsUrl: string | null
|
||||
description: Description
|
||||
images: { [key: ImageId]: ImageConfig }
|
||||
volumes: Array<VolumeId>
|
||||
alerts: Alerts
|
||||
dependencies: Dependencies
|
||||
hardwareRequirements: HardwareRequirements
|
||||
hardwareAcceleration: boolean
|
||||
title: string
|
||||
description: Description
|
||||
releaseNotes: LocaleString
|
||||
gitHash: GitHash | null
|
||||
license: string
|
||||
packageRepo: string
|
||||
upstreamRepo: string
|
||||
marketingUrl: string
|
||||
donationUrl: string | null
|
||||
docsUrls: string[]
|
||||
alerts: Alerts
|
||||
osVersion: string
|
||||
sdkVersion: string | null
|
||||
hardwareAcceleration: boolean
|
||||
plugins: Array<PluginId>
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type MebiBytes = number
|
||||
export type MebiBytes = { value: string; unit: string }
|
||||
|
||||
5
sdk/base/lib/osBindings/MetricsFollowResponse.ts
Normal file
5
sdk/base/lib/osBindings/MetricsFollowResponse.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Guid } from './Guid'
|
||||
import type { Metrics } from './Metrics'
|
||||
|
||||
export type MetricsFollowResponse = { guid: Guid; metrics: Metrics }
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ModifyNotificationBeforeParams = { before: number }
|
||||
3
sdk/base/lib/osBindings/ModifyNotificationParams.ts
Normal file
3
sdk/base/lib/osBindings/ModifyNotificationParams.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ModifyNotificationParams = { ids: number[] }
|
||||
@@ -1,9 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type NetInfo = {
|
||||
privateDisabled: Array<GatewayId>
|
||||
publicEnabled: Array<GatewayId>
|
||||
assignedPort: number | null
|
||||
assignedSslPort: number | null
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@ export type NetworkInfo = {
|
||||
gateways: { [key: GatewayId]: NetworkInterfaceInfo }
|
||||
acme: { [key: AcmeProvider]: AcmeSettings }
|
||||
dns: DnsSettings
|
||||
defaultOutbound: string | null
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayType } from './GatewayType'
|
||||
import type { IpInfo } from './IpInfo'
|
||||
|
||||
export type NetworkInterfaceInfo = {
|
||||
name: string | null
|
||||
public: boolean | null
|
||||
secure: boolean | null
|
||||
ipInfo: IpInfo | null
|
||||
type: GatewayType | null
|
||||
}
|
||||
|
||||
14
sdk/base/lib/osBindings/Notification.ts
Normal file
14
sdk/base/lib/osBindings/Notification.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { NotificationLevel } from './NotificationLevel'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type Notification = {
|
||||
packageId: PackageId | null
|
||||
createdAt: string
|
||||
code: number
|
||||
level: NotificationLevel
|
||||
title: string
|
||||
message: string
|
||||
data: any
|
||||
seen: boolean
|
||||
}
|
||||
3
sdk/base/lib/osBindings/NotificationLevel.ts
Normal file
3
sdk/base/lib/osBindings/NotificationLevel.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type NotificationLevel = 'success' | 'info' | 'warning' | 'error'
|
||||
15
sdk/base/lib/osBindings/NotificationWithId.ts
Normal file
15
sdk/base/lib/osBindings/NotificationWithId.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { NotificationLevel } from './NotificationLevel'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type NotificationWithId = {
|
||||
id: number
|
||||
packageId: PackageId | null
|
||||
createdAt: string
|
||||
code: number
|
||||
level: NotificationLevel
|
||||
title: string
|
||||
message: string
|
||||
data: any
|
||||
seen: boolean
|
||||
}
|
||||
9
sdk/base/lib/osBindings/PackageBackupInfo.ts
Normal file
9
sdk/base/lib/osBindings/PackageBackupInfo.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Version } from './Version'
|
||||
|
||||
export type PackageBackupInfo = {
|
||||
title: string
|
||||
version: Version
|
||||
osVersion: string
|
||||
timestamp: string
|
||||
}
|
||||
3
sdk/base/lib/osBindings/PackageBackupReport.ts
Normal file
3
sdk/base/lib/osBindings/PackageBackupReport.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PackageBackupReport = { error: string | null }
|
||||
@@ -4,6 +4,7 @@ import type { ActionMetadata } from './ActionMetadata'
|
||||
import type { CurrentDependencies } from './CurrentDependencies'
|
||||
import type { DataUrl } from './DataUrl'
|
||||
import type { Hosts } from './Hosts'
|
||||
import type { PackagePlugin } from './PackagePlugin'
|
||||
import type { PackageState } from './PackageState'
|
||||
import type { ReplayId } from './ReplayId'
|
||||
import type { ServiceInterface } from './ServiceInterface'
|
||||
@@ -25,4 +26,6 @@ export type PackageDataEntry = {
|
||||
serviceInterfaces: { [key: ServiceInterfaceId]: ServiceInterface }
|
||||
hosts: Hosts
|
||||
storeExposedDependents: string[]
|
||||
outboundGateway: string | null
|
||||
plugin: PackagePlugin
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LocaleString } from './LocaleString'
|
||||
|
||||
export type PackageInfoShort = { releaseNotes: string }
|
||||
export type PackageInfoShort = { releaseNotes: LocaleString }
|
||||
|
||||
4
sdk/base/lib/osBindings/PackagePlugin.ts
Normal file
4
sdk/base/lib/osBindings/PackagePlugin.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { UrlPluginRegistration } from './UrlPluginRegistration'
|
||||
|
||||
export type PackagePlugin = { url: UrlPluginRegistration | null }
|
||||
@@ -8,26 +8,27 @@ import type { HardwareRequirements } from './HardwareRequirements'
|
||||
import type { LocaleString } from './LocaleString'
|
||||
import type { MerkleArchiveCommitment } from './MerkleArchiveCommitment'
|
||||
import type { PackageId } from './PackageId'
|
||||
import type { PluginId } from './PluginId'
|
||||
import type { RegistryAsset } from './RegistryAsset'
|
||||
|
||||
export type PackageVersionInfo = {
|
||||
icon: DataUrl
|
||||
dependencyMetadata: { [key: PackageId]: DependencyMetadata }
|
||||
sourceVersion: string | null
|
||||
s9pks: Array<[HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>]>
|
||||
title: string
|
||||
icon: DataUrl
|
||||
description: Description
|
||||
releaseNotes: LocaleString
|
||||
gitHash: GitHash | null
|
||||
license: string
|
||||
wrapperRepo: string
|
||||
packageRepo: string
|
||||
upstreamRepo: string
|
||||
supportSite: string
|
||||
marketingSite: string
|
||||
marketingUrl: string
|
||||
donationUrl: string | null
|
||||
docsUrl: string | null
|
||||
docsUrls: string[]
|
||||
alerts: Alerts
|
||||
dependencyMetadata: { [key: PackageId]: DependencyMetadata }
|
||||
osVersion: string
|
||||
sdkVersion: string | null
|
||||
hardwareAcceleration: boolean
|
||||
plugins: Array<PluginId>
|
||||
}
|
||||
|
||||
11
sdk/base/lib/osBindings/PartitionInfo.ts
Normal file
11
sdk/base/lib/osBindings/PartitionInfo.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { StartOsRecoveryInfo } from './StartOsRecoveryInfo'
|
||||
|
||||
export type PartitionInfo = {
|
||||
logicalname: string
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
startOs: { [key: string]: StartOsRecoveryInfo }
|
||||
guid: string | null
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Percentage = number
|
||||
export type Percentage = { value: string; unit: string }
|
||||
|
||||
14
sdk/base/lib/osBindings/PluginHostnameInfo.ts
Normal file
14
sdk/base/lib/osBindings/PluginHostnameInfo.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HostId } from './HostId'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type PluginHostnameInfo = {
|
||||
packageId: PackageId | null
|
||||
hostId: HostId
|
||||
internalPort: number
|
||||
ssl: boolean
|
||||
public: boolean
|
||||
hostname: string
|
||||
port: number | null
|
||||
info: unknown
|
||||
}
|
||||
3
sdk/base/lib/osBindings/PluginId.ts
Normal file
3
sdk/base/lib/osBindings/PluginId.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginId = 'url-v0'
|
||||
4
sdk/base/lib/osBindings/PortForward.ts
Normal file
4
sdk/base/lib/osBindings/PortForward.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type PortForward = { src: string; dst: string; gateway: GatewayId }
|
||||
3
sdk/base/lib/osBindings/QueryDnsParams.ts
Normal file
3
sdk/base/lib/osBindings/QueryDnsParams.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type QueryDnsParams = { fqdn: string }
|
||||
4
sdk/base/lib/osBindings/RebuildParams.ts
Normal file
4
sdk/base/lib/osBindings/RebuildParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type RebuildParams = { id: PackageId }
|
||||
4
sdk/base/lib/osBindings/RemoveAcmeParams.ts
Normal file
4
sdk/base/lib/osBindings/RemoveAcmeParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AcmeProvider } from './AcmeProvider'
|
||||
|
||||
export type RemoveAcmeParams = { provider: AcmeProvider }
|
||||
3
sdk/base/lib/osBindings/RemoveDomainParams.ts
Normal file
3
sdk/base/lib/osBindings/RemoveDomainParams.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RemoveDomainParams = { fqdn: string }
|
||||
4
sdk/base/lib/osBindings/RenameGatewayParams.ts
Normal file
4
sdk/base/lib/osBindings/RenameGatewayParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type RenameGatewayParams = { id: GatewayId; name: string }
|
||||
7
sdk/base/lib/osBindings/ResetPasswordParams.ts
Normal file
7
sdk/base/lib/osBindings/ResetPasswordParams.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PasswordType } from './PasswordType'
|
||||
|
||||
export type ResetPasswordParams = {
|
||||
oldPassword: PasswordType | null
|
||||
newPassword: PasswordType | null
|
||||
}
|
||||
9
sdk/base/lib/osBindings/RestorePackageParams.ts
Normal file
9
sdk/base/lib/osBindings/RestorePackageParams.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BackupTargetId } from './BackupTargetId'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type RestorePackageParams = {
|
||||
ids: Array<PackageId>
|
||||
targetId: BackupTargetId
|
||||
password: string
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ActionId } from './ActionId'
|
||||
import type { Guid } from './Guid'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type RunActionParams = {
|
||||
packageId?: PackageId
|
||||
packageId: PackageId
|
||||
eventId: Guid | null
|
||||
actionId: ActionId
|
||||
input: any
|
||||
input?: any
|
||||
}
|
||||
|
||||
3
sdk/base/lib/osBindings/ServerBackupReport.ts
Normal file
3
sdk/base/lib/osBindings/ServerBackupReport.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ServerBackupReport = { attempted: boolean; error: string | null }
|
||||
3
sdk/base/lib/osBindings/ServerHostname.ts
Normal file
3
sdk/base/lib/osBindings/ServerHostname.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ServerHostname = string
|
||||
@@ -10,6 +10,7 @@ export type ServerInfo = {
|
||||
arch: string
|
||||
platform: string
|
||||
id: string
|
||||
name: string
|
||||
hostname: string
|
||||
version: string
|
||||
packageVersionCompat: string
|
||||
@@ -25,6 +26,7 @@ export type ServerInfo = {
|
||||
zram: boolean
|
||||
governor: Governor | null
|
||||
smtp: SmtpValue | null
|
||||
ifconfigUrl: string
|
||||
ram: number
|
||||
devices: Array<LshwDevice>
|
||||
kiosk: boolean | null
|
||||
|
||||
3
sdk/base/lib/osBindings/SetCountryParams.ts
Normal file
3
sdk/base/lib/osBindings/SetCountryParams.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SetCountryParams = { country: string }
|
||||
4
sdk/base/lib/osBindings/SetDefaultOutboundParams.ts
Normal file
4
sdk/base/lib/osBindings/SetDefaultOutboundParams.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type SetDefaultOutboundParams = { gateway: GatewayId | null }
|
||||
3
sdk/base/lib/osBindings/SetLanguageParams.ts
Normal file
3
sdk/base/lib/osBindings/SetLanguageParams.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SetLanguageParams = { language: string }
|
||||
8
sdk/base/lib/osBindings/SetOutboundGatewayParams.ts
Normal file
8
sdk/base/lib/osBindings/SetOutboundGatewayParams.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
import type { PackageId } from './PackageId'
|
||||
|
||||
export type SetOutboundGatewayParams = {
|
||||
package: PackageId
|
||||
gateway: GatewayId | null
|
||||
}
|
||||
6
sdk/base/lib/osBindings/SetServerHostnameParams.ts
Normal file
6
sdk/base/lib/osBindings/SetServerHostnameParams.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SetServerHostnameParams = {
|
||||
name: string | null
|
||||
hostname: string | null
|
||||
}
|
||||
3
sdk/base/lib/osBindings/SetStaticDnsParams.ts
Normal file
3
sdk/base/lib/osBindings/SetStaticDnsParams.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SetStaticDnsParams = { servers: Array<string> | null }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user