Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major

This commit is contained in:
Matt Hill
2024-11-25 19:02:07 -07:00
712 changed files with 83068 additions and 9240 deletions

9
sdk/.gitignore vendored
View File

@@ -1,5 +1,6 @@
.vscode
dist/
node_modules/
lib/coverage
lib/test/output.ts
baseDist/
base/lib/coverage
base/lib/node_modules
package/lib/coverage
package/lib/node_modules

View File

@@ -1 +1 @@
/lib/exver/exver.ts
/base/lib/exver/exver.ts

View File

@@ -1,47 +1,73 @@
TS_FILES := $(shell git ls-files lib) lib/test/output.ts
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
version = $(shell git tag --sort=committerdate | tail -1)
.PHONY: test clean bundle fmt buildOutput check
.PHONY: test base/test package/test clean bundle fmt buildOutput check
all: bundle
test: $(TS_FILES) lib/test/output.ts
npm test
package/test: $(PACKAGE_TS_FILES) package/lib/test/output.ts package/node_modules base/node_modules
cd package && npm test
base/test: $(BASE_TS_FILES) base/node_modules
cd base && npm test
test: base/test package/test
clean:
rm -rf base/node_modules
rm -rf dist
rm -f lib/test/output.ts
rm -rf node_modules
rm -rf baseDist
rm -f package/lib/test/output.ts
rm -rf package/node_modules
lib/test/output.ts: node_modules lib/test/makeOutput.ts scripts/oldSpecToBuilder.ts
npm run buildOutput
package/lib/test/output.ts: package/node_modules package/lib/test/makeOutput.ts package/scripts/oldSpecToBuilder.ts
cd package && npm run buildOutput
bundle: dist | test fmt
bundle: baseDist dist | test fmt
touch dist
lib/exver/exver.ts: node_modules lib/exver/exver.pegjs
npx peggy --allowed-start-rules '*' --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs
base/lib/exver/exver.ts: base/node_modules base/lib/exver/exver.pegjs
cd base && npm run peggy
dist: $(TS_FILES) package.json node_modules README.md LICENSE
npx tsc
npx tsc --project tsconfig-cjs.json
cp package.json dist/package.json
cp README.md dist/README.md
cp LICENSE dist/LICENSE
baseDist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) base/package.json base/node_modules base/README.md base/LICENSE
(cd base && npm run tsc)
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
(cd package && npm run tsc)
rsync -ac package/node_modules dist/
cp package/.npmignore dist/.npmignore
cp package/package.json dist/package.json
cp package/README.md dist/README.md
cp package/LICENSE dist/LICENSE
touch dist
full-bundle: bundle
check:
cd package
npm run check
cd ../base
npm run check
fmt: node_modules
fmt: package/node_modules base/node_modules
npx prettier . "**/*.ts" --write
node_modules: package.json
npm ci
publish: bundle package.json README.md LICENSE
package/node_modules: package/package.json
cd package && npm ci
base/node_modules: base/package.json
cd base && npm ci
node_modules: package/node_modules base/node_modules
publish: bundle package/package.json package/README.md package/LICENSE
cd dist && npm publish --access=public
link: bundle

5
sdk/base/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.vscode
dist/
node_modules/
lib/coverage
lib/test/output.ts

1
sdk/base/README.md Normal file
View File

@@ -0,0 +1 @@
# See ../package/README.md

197
sdk/base/lib/Effects.ts Normal file
View File

@@ -0,0 +1,197 @@
import {
ActionId,
ActionInput,
ActionMetadata,
SetMainStatus,
DependencyRequirement,
CheckDependenciesResult,
SetHealth,
BindParams,
HostId,
LanInfo,
Host,
ExportServiceInterfaceParams,
ServiceInterface,
ActionRequest,
RequestActionParams,
MainStatus,
} from "./osBindings"
import { StorePath } from "./util/PathBuilder"
import {
PackageId,
Dependencies,
ServiceInterfaceId,
SmtpValue,
ActionResult,
} from "./types"
import { UrlString } from "./util/getServiceInterface"
/** Used to reach out from the pure js runtime */
export type Effects = {
constRetry: () => void
clearCallbacks: (
options: { only: number[] } | { except: number[] },
) => Promise<null>
// action
action: {
/** Define an action that can be invoked by a user or service */
export(options: { id: ActionId; metadata: ActionMetadata }): Promise<null>
/** Remove all exported actions */
clear(options: { except: ActionId[] }): Promise<null>
getInput(options: {
packageId?: PackageId
actionId: ActionId
}): Promise<ActionInput | null>
run<Input extends Record<string, unknown>>(options: {
packageId?: PackageId
actionId: ActionId
input?: Input
}): Promise<ActionResult | null>
request<Input extends Record<string, unknown>>(
options: RequestActionParams,
): Promise<null>
clearRequests(
options: { only: string[] } | { except: string[] },
): Promise<null>
}
// control
/** restart this service's main function */
restart(): Promise<null>
/** stop this service's main function */
shutdown(): Promise<null>
/** ask the host os what the service's current status is */
getStatus(options: {
packageId?: PackageId
callback?: () => void
}): Promise<MainStatus>
/** indicate to the host os what runstate the service is in */
setMainStatus(options: SetMainStatus): Promise<null>
// dependency
/** Set the dependencies of what the service needs, usually run during the inputSpec action as a best practice */
setDependencies(options: { dependencies: Dependencies }): Promise<null>
/** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */
getDependencies(): Promise<DependencyRequirement[]>
/** Test whether current dependency requirements are satisfied */
checkDependencies(options: {
packageIds?: PackageId[]
}): Promise<CheckDependenciesResult[]>
/** mount a volume of a dependency */
mount(options: {
location: string
target: {
packageId: string
volumeId: string
subpath: string | null
readonly: boolean
}
}): Promise<string>
/** Returns a list of the ids of all installed packages */
getInstalledPackages(): Promise<string[]>
/** grants access to certain paths in the store to dependents */
exposeForDependents(options: { paths: string[] }): Promise<null>
// health
/** sets the result of a health check */
setHealth(o: SetHealth): Promise<null>
// subcontainer
subcontainer: {
/** A low level api used by SubContainer */
createFs(options: {
imageId: string
name: string | null
}): Promise<[string, string]>
/** A low level api used by SubContainer */
destroyFs(options: { guid: string }): Promise<null>
}
// net
// bind
/** Creates a host connected to the specified port with the provided options */
bind(options: BindParams): Promise<null>
/** Get the port address for a service */
getServicePortForward(options: {
packageId?: PackageId
hostId: HostId
internalPort: number
}): Promise<LanInfo>
/** Removes all network bindings, called in the setupInputSpec */
clearBindings(options: {
except: { id: HostId; internalPort: number }[]
}): Promise<null>
// host
/** Returns information about the specified host, if it exists */
getHostInfo(options: {
packageId?: PackageId
hostId: HostId
callback?: () => void
}): Promise<Host | null>
/** Returns the primary url that a user has selected for a host, if it exists */
getPrimaryUrl(options: {
packageId?: PackageId
hostId: HostId
callback?: () => void
}): Promise<UrlString | null>
/** Returns the IP address of the container */
getContainerIp(): Promise<string>
// interface
/** Creates an interface bound to a specific host and port to show to the user */
exportServiceInterface(options: ExportServiceInterfaceParams): Promise<null>
/** Returns an exported service interface */
getServiceInterface(options: {
packageId?: PackageId
serviceInterfaceId: ServiceInterfaceId
callback?: () => void
}): Promise<ServiceInterface | null>
/** Returns all exported service interfaces for a package */
listServiceInterfaces(options: {
packageId?: PackageId
callback?: () => void
}): Promise<Record<ServiceInterfaceId, ServiceInterface>>
/** Removes all service interfaces */
clearServiceInterfaces(options: {
except: ServiceInterfaceId[]
}): Promise<null>
// ssl
/** Returns a PEM encoded fullchain for the hostnames specified */
getSslCertificate: (options: {
hostnames: string[]
algorithm?: "ecdsa" | "ed25519"
callback?: () => void
}) => Promise<[string, string, string]>
/** Returns a PEM encoded private key corresponding to the certificate for the hostnames specified */
getSslKey: (options: {
hostnames: string[]
algorithm?: "ecdsa" | "ed25519"
}) => Promise<string>
// store
store: {
/** Get a value in a json like data, can be observed and subscribed */
get<Store = never, ExtractStore = unknown>(options: {
/** If there is no packageId it is assumed the current package */
packageId?: string
/** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */
path: StorePath
callback?: () => void
}): Promise<ExtractStore>
/** Used to store values that can be accessed and subscribed to */
set<Store = never, ExtractStore = unknown>(options: {
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
path: StorePath
value: ExtractStore
}): Promise<null>
}
/** sets the version that this service's data has been migrated to */
setDataVersion(options: { version: string }): Promise<null>
/** returns the version that this service's data has been migrated to */
getDataVersion(): Promise<string | null>
// system
/** Returns globally configured SMTP settings, if they exist */
getSystemSmtp(options: { callback?: () => void }): Promise<SmtpValue | null>
}

View File

@@ -0,0 +1,100 @@
import * as T from "../types"
import * as IST from "../actions/input/inputSpecTypes"
import { Action } from "./setupActions"
import { ExtractInputSpecType } from "./input/builder/inputSpec"
export type RunActionInput<Input> =
| Input
| ((prev?: { spec: IST.InputSpec; value: Input | null }) => Input)
export const runAction = async <
Input extends Record<string, unknown>,
>(options: {
effects: T.Effects
// packageId?: T.PackageId
actionId: T.ActionId
input?: RunActionInput<Input>
}) => {
if (options.input) {
if (options.input instanceof Function) {
const prev = await options.effects.action.getInput({
// packageId: options.packageId,
actionId: options.actionId,
})
const input = options.input(
prev
? { spec: prev.spec as IST.InputSpec, value: prev.value as Input }
: undefined,
)
return options.effects.action.run({
// packageId: options.packageId,
actionId: options.actionId,
input,
})
} else {
return options.effects.action.run({
// packageId: options.packageId,
actionId: options.actionId,
input: options.input,
})
}
} else {
return options.effects.action.run({
// packageId: options.packageId,
actionId: options.actionId,
})
}
}
type GetActionInputType<A extends Action<T.ActionId, any, any>> =
A extends Action<T.ActionId, any, infer I> ? ExtractInputSpecType<I> : never
type ActionRequestBase = {
reason?: string
replayId?: string
}
type ActionRequestInput<T extends Action<T.ActionId, any, any>> = {
kind: "partial"
value: Partial<GetActionInputType<T>>
}
export type ActionRequestOptions<T extends Action<T.ActionId, any, any>> =
ActionRequestBase &
(
| {
when?: Exclude<
T.ActionRequestTrigger,
{ condition: "input-not-matches" }
>
input?: ActionRequestInput<T>
}
| {
when: T.ActionRequestTrigger & { condition: "input-not-matches" }
input: ActionRequestInput<T>
}
)
const _validate: T.ActionRequest = {} as ActionRequestOptions<any> & {
actionId: string
packageId: string
severity: T.ActionSeverity
}
export const requestAction = <T extends Action<T.ActionId, any, any>>(options: {
effects: T.Effects
packageId: T.PackageId
action: T
severity: T.ActionSeverity
options?: ActionRequestOptions<T>
}) => {
const request = options.options || {}
const actionId = options.action.id
const req = {
...request,
actionId,
packageId: options.packageId,
action: undefined,
severity: options.severity,
replayId: request.replayId || `${options.packageId}:${actionId}`,
}
delete req.action
return options.effects.action.request(req)
}

View File

@@ -1,6 +1,6 @@
import { Config } from "./config"
import { InputSpec } from "./inputSpec"
import { List } from "./list"
import { Value } from "./value"
import { Variants } from "./variants"
export { Config, List, Value, Variants }
export { InputSpec as InputSpec, List, Value, Variants }

View File

@@ -1,8 +1,9 @@
import { ValueSpec } from "../configTypes"
import { ValueSpec } from "../inputSpecTypes"
import { Value } from "./value"
import { _ } from "../../util"
import { Effects } from "../../types"
import { _ } from "../../../util"
import { Effects } from "../../../Effects"
import { Parser, object } from "ts-matches"
import { DeepPartial } from "../../../types"
export type LazyBuildOptions<Store> = {
effects: Effects
@@ -12,20 +13,29 @@ export type LazyBuild<Store, ExpectedOut> = (
) => Promise<ExpectedOut> | ExpectedOut
// prettier-ignore
export type ExtractConfigType<A extends Record<string, any> | Config<Record<string, any>, any> | Config<Record<string, any>, never>> =
A extends Config<infer B, any> | Config<infer B, never> ? B :
export type ExtractInputSpecType<A extends Record<string, any> | InputSpec<Record<string, any>, any> | InputSpec<Record<string, any>, never>> =
A extends InputSpec<infer B, any> | InputSpec<infer B, never> ? B :
A
export type ConfigSpecOf<A extends Record<string, any>, Store = never> = {
export type ExtractPartialInputSpecType<
A extends
| Record<string, any>
| InputSpec<Record<string, any>, any>
| InputSpec<Record<string, any>, never>,
> = A extends InputSpec<infer B, any> | InputSpec<infer B, never>
? DeepPartial<B>
: DeepPartial<A>
export type InputSpecOf<A extends Record<string, any>, Store = never> = {
[K in keyof A]: Value<A[K], Store>
}
export type MaybeLazyValues<A> = LazyBuild<any, A> | A
/**
* Configs are the specs that are used by the os configuration form for this service.
* Here is an example of a simple configuration
* 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
```ts
const smallConfig = Config.of({
const smallInputSpec = InputSpec.of({
test: Value.boolean({
name: "Test",
description: "This is the description for the test",
@@ -35,17 +45,17 @@ export type MaybeLazyValues<A> = LazyBuild<any, A> | A
});
```
The idea of a config is that now the form is going to ask for
The idea of an inputSpec is that now the form is going to ask for
Test: [ ] and the value is going to be checked as a boolean.
There are more complex values like selects, lists, and objects. See {@link Value}
Also, there is the ability to get a validator/parser from this config spec.
Also, there is the ability to get a validator/parser from this inputSpec spec.
```ts
const matchSmallConfig = smallConfig.validator();
type SmallConfig = typeof matchSmallConfig._TYPE;
const matchSmallInputSpec = smallInputSpec.validator();
type SmallInputSpec = typeof matchSmallInputSpec._TYPE;
```
Here is an example of a more complex configuration which came from a configuration for a service
Here is an example of a more complex input specification which came from an input specification for a service
that works with bitcoin, like c-lightning.
```ts
@@ -73,17 +83,19 @@ export const port = Value.number({
units: null,
placeholder: null,
});
export const addNodesSpec = Config.of({ hostname: hostname, port: port });
export const addNodesSpec = InputSpec.of({ hostname: hostname, port: port });
```
*/
export class Config<Type extends Record<string, any>, Store = never> {
export class InputSpec<Type extends Record<string, any>, Store = never> {
private constructor(
private readonly spec: {
[K in keyof Type]: Value<Type[K], Store> | Value<Type[K], never>
},
public validator: Parser<unknown, Type>,
) {}
_TYPE: Type = null as any as Type
_PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
async build(options: LazyBuildOptions<Store>) {
const answer = {} as {
[K in keyof Type]: ValueSpec
@@ -105,7 +117,7 @@ export class Config<Type extends Record<string, any>, Store = never> {
validatorObj[key] = spec[key].validator
}
const validator = object(validatorObj)
return new Config<
return new InputSpec<
{
[K in keyof Spec]: Spec[K] extends
| Value<infer T, Store>
@@ -119,19 +131,19 @@ export class Config<Type extends Record<string, any>, Store = never> {
/**
* Use this during the times that the input needs a more specific type.
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
* Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else.
```ts
const a = Config.text({
const a = InputSpec.text({
name: "a",
required: false,
})
return Config.of<Store>()({
return InputSpec.of<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Config<Type, NewStore>
return this as any as InputSpec<Type, NewStore>
}
}

View File

@@ -1,4 +1,4 @@
import { Config, LazyBuild } from "./config"
import { InputSpec, LazyBuild } from "./inputSpec"
import {
ListValueSpecText,
Pattern,
@@ -6,45 +6,55 @@ import {
UniqueBy,
ValueSpecList,
ValueSpecListOf,
} from "../configTypes"
import { Parser, arrayOf, number, string } from "ts-matches"
/**
* Used as a subtype of Value.list
```ts
export const authorizationList = List.string({
"name": "Authorization",
"range": "[0,*)",
"default": [],
"description": "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.",
"warning": null
}, {"masked":false,"placeholder":null,"pattern":"^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$","patternDescription":"Each item must be of the form \"<USERNAME>:<SALT>$<HASH>\"."});
export const auth = Value.list(authorizationList);
```
*/
} from "../inputSpecTypes"
import { Parser, arrayOf, string } from "ts-matches"
export class List<Type, Store> {
private constructor(
public build: LazyBuild<Store, ValueSpecList>,
public validator: Parser<unknown, Type>,
) {}
static text(
a: {
name: string
description?: string | null
warning?: string | null
/** Default = [] */
default?: string[]
minLength?: number | null
maxLength?: number | null
},
aSpec: {
/** Default = false */
/**
* @description Mask (aka camouflage) text input with dots:
* @default false
*/
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns: Pattern[]
/** Default = "text" */
/**
* @description A list of regular expressions to which the text must conform to pass validation. A human readable description is provided in case the validation fails.
* @default []
* @example
* ```
[
{
regex: "[a-z]",
description: "May only contain lower case letters from the English alphabet."
}
]
* ```
*/
patterns?: Pattern[]
/**
* @description Informs the browser how to behave and which keyboard to display on mobile
* @default "text"
*/
inputmode?: ListValueSpecText["inputmode"]
/**
* @description Displays a button that will generate a random string according to the provided charset and len attributes.
*/
generate?: null | RandomString
},
) {
@@ -57,6 +67,7 @@ export class List<Type, Store> {
masked: false,
inputmode: "text" as const,
generate: null,
patterns: aSpec.patterns || [],
...aSpec,
}
const built: ValueSpecListOf<"text"> = {
@@ -73,6 +84,7 @@ export class List<Type, Store> {
return built
}, arrayOf(string))
}
static dynamicText<Store = never>(
getA: LazyBuild<
Store,
@@ -80,20 +92,17 @@ export class List<Type, Store> {
name: string
description?: string | null
warning?: string | null
/** Default = [] */
default?: string[]
minLength?: number | null
maxLength?: number | null
disabled?: false | string
generate?: null | RandomString
spec: {
/** Default = false */
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns: Pattern[]
/** Default = "text" */
patterns?: Pattern[]
inputmode?: ListValueSpecText["inputmode"]
}
}
@@ -109,6 +118,7 @@ export class List<Type, Store> {
masked: false,
inputmode: "text" as const,
generate: null,
patterns: aSpec.patterns || [],
...aSpec,
}
const built: ValueSpecListOf<"text"> = {
@@ -125,18 +135,18 @@ export class List<Type, Store> {
return built
}, arrayOf(string))
}
static obj<Type extends Record<string, any>, Store>(
a: {
name: string
description?: string | null
warning?: string | null
/** Default [] */
default?: []
minLength?: number | null
maxLength?: number | null
},
aSpec: {
spec: Config<Type, Store>
spec: InputSpec<Type, Store>
displayAs?: null | string
uniqueBy?: null | UniqueBy
},
@@ -170,14 +180,14 @@ export class List<Type, Store> {
/**
* Use this during the times that the input needs a more specific type.
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
* Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else.
```ts
const a = Config.text({
const a = InputSpec.text({
name: "a",
required: false,
})
return Config.of<Store>()({
return InputSpec.of<Store>()({
myValue: a.withStore(),
})
```

View File

@@ -1,4 +1,4 @@
import { Config, LazyBuild, LazyBuildOptions } from "./config"
import { InputSpec, LazyBuild } from "./inputSpec"
import { List } from "./list"
import { Variants } from "./variants"
import {
@@ -7,13 +7,15 @@ import {
RandomString,
ValueSpec,
ValueSpecDatetime,
ValueSpecHidden,
ValueSpecText,
ValueSpecTextarea,
} from "../configTypes"
import { DefaultString } from "../configTypes"
import { _ } from "../../util"
} from "../inputSpecTypes"
import { DefaultString } from "../inputSpecTypes"
import { _, once } from "../../../util"
import {
Parser,
any,
anyOf,
arrayOf,
boolean,
@@ -24,77 +26,24 @@ import {
string,
unknown,
} from "ts-matches"
import { once } from "../../util/once"
import { DeepPartial } from "../../../types"
export type RequiredDefault<A> =
| false
| {
default: A | null
}
type AsRequired<T, Required extends boolean> = Required extends true
? T
: T | null
function requiredLikeToAbove<Input extends RequiredDefault<A>, A>(
requiredLike: Input,
) {
// prettier-ignore
return {
required: (typeof requiredLike === 'object' ? true : requiredLike) as (
Input extends { default: unknown} ? true:
Input extends true ? true :
false
),
default:(typeof requiredLike === 'object' ? requiredLike.default : null) as (
Input extends { default: infer Default } ? Default :
null
)
};
}
type AsRequired<Type, MaybeRequiredType> = MaybeRequiredType extends
| { default: unknown }
| never
? Type
: Type | null | undefined
type InputAsRequired<A, Type> = A extends
| { required: { default: any } | never }
| never
? Type
: Type | null | undefined
const testForAsRequiredParser = once(
() => object({ required: object({ default: unknown }) }).test,
() => object({ required: literal(true) }).test,
)
function asRequiredParser<
Type,
Input,
Return extends
| Parser<unknown, Type>
| Parser<unknown, Type | null | undefined>,
Return extends Parser<unknown, Type> | Parser<unknown, Type | null>,
>(parser: Parser<unknown, Type>, input: Input): Return {
if (testForAsRequiredParser()(input)) return parser as any
return parser.optional() as any
return parser.nullable() as any
}
/**
* A value is going to be part of the form in the FE of the OS.
* Something like a boolean, a string, a number, etc.
* in the fe it will ask for the name of value, and use the rest of the value to determine how to render it.
* While writing with a value, you will start with `Value.` then let the IDE suggest the rest.
* for things like string, the options are going to be in {}.
* Keep an eye out for another config builder types as params.
* Note, usually this is going to be used in a `Config` {@link Config} builder.
```ts
const username = Value.string({
name: "Username",
default: "bitcoin",
description: "The username for connecting to Bitcoin over RPC.",
warning: null,
required: true,
masked: true,
placeholder: null,
pattern: "^[a-zA-Z0-9_]+$",
patternDescription: "Must be alphanumeric (can contain underscore).",
});
```
*/
export class Value<Type, Store> {
protected constructor(
public build: LazyBuild<Store, ValueSpec>,
@@ -103,10 +52,13 @@ export class Value<Type, Store> {
static toggle(a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
default: boolean
/** Immutable means it can only be configed at the first config then never again
Default is false */
/**
* @description Once set, the value can never be changed.
* @default false
*/
immutable?: boolean
}) {
return new Value<boolean, never>(
@@ -145,25 +97,56 @@ export class Value<Type, Store> {
boolean,
)
}
static text<Required extends RequiredDefault<DefaultString>>(a: {
static text<Required extends boolean>(a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
/**
* provide a default value.
* @type { string | RandomString | null }
* @example default: null
* @example default: 'World'
* @example default: { charset: 'abcdefg', len: 16 }
*/
default: string | RandomString | null
required: Required
/** Default = false */
/**
* @description Mask (aka camouflage) text input with dots:
* @default false
*/
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
/**
* @description A list of regular expressions to which the text must conform to pass validation. A human readable description is provided in case the validation fails.
* @default []
* @example
* ```
[
{
regex: "[a-z]",
description: "May only contain lower case letters from the English alphabet."
}
]
* ```
*/
patterns?: Pattern[]
/** Default = 'text' */
/**
* @description Informs the browser how to behave and which keyboard to display on mobile
* @default "text"
*/
inputmode?: ValueSpecText["inputmode"]
/** Immutable means it can only be configured at the first config then never again
* Default is false
/**
* @description Once set, the value can never be changed.
* @default false
*/
immutable?: boolean
generate?: null | RandomString
/**
* @description Displays a button that will generate a random string according to the provided charset and len attributes.
*/
generate?: RandomString | null
}) {
return new Value<AsRequired<string, Required>, never>(
async () => ({
@@ -180,7 +163,6 @@ export class Value<Type, Store> {
immutable: a.immutable ?? false,
generate: a.generate ?? null,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
@@ -192,25 +174,20 @@ export class Value<Type, Store> {
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<DefaultString>
/** Default = false */
default: DefaultString | null
required: boolean
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns?: Pattern[]
/** Default = 'text' */
inputmode?: ValueSpecText["inputmode"]
disabled?: string | false
/** Immutable means it can only be configured at the first config then never again
* Default is false
*/
generate?: null | RandomString
}
>,
) {
return new Value<string | null | undefined, Store>(async (options) => {
return new Value<string | null, Store>(async (options) => {
const a = await getA(options)
return {
type: "text" as const,
@@ -226,36 +203,42 @@ export class Value<Type, Store> {
immutable: false,
generate: a.generate ?? null,
...a,
...requiredLikeToAbove(a.required),
}
}, string.optional())
}, string.nullable())
}
static textarea(a: {
static textarea<Required extends boolean>(a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
required: boolean
default: string | null
required: Required
minLength?: number | null
maxLength?: number | null
placeholder?: string | null
/** Immutable means it can only be configed at the first config then never again
Default is false */
/**
* @description Once set, the value can never be changed.
* @default false
*/
immutable?: boolean
}) {
return new Value<string, never>(async () => {
const built: ValueSpecTextarea = {
description: null,
warning: null,
minLength: null,
maxLength: null,
placeholder: null,
type: "textarea" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
}
return built
}, string)
return new Value<AsRequired<string, Required>, never>(
async () => {
const built: ValueSpecTextarea = {
description: null,
warning: null,
minLength: null,
maxLength: null,
placeholder: null,
type: "textarea" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
}
return built
},
asRequiredParser(string, a),
)
}
static dynamicTextarea<Store = never>(
getA: LazyBuild<
@@ -264,6 +247,7 @@ export class Value<Type, Store> {
name: string
description?: string | null
warning?: string | null
default: string | null
required: boolean
minLength?: number | null
maxLength?: number | null
@@ -272,7 +256,7 @@ export class Value<Type, Store> {
}
>,
) {
return new Value<string, Store>(async (options) => {
return new Value<string | null, Store>(async (options) => {
const a = await getA(options)
return {
description: null,
@@ -285,22 +269,41 @@ export class Value<Type, Store> {
immutable: false,
...a,
}
}, string)
}, string.nullable())
}
static number<Required extends RequiredDefault<number>>(a: {
static number<Required extends boolean>(a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
/**
* @description optionally provide a default value.
* @type { default: number | null }
* @example default: null
* @example default: 7
*/
default: number | null
required: Required
min?: number | null
max?: number | null
/** Default = '1' */
/**
* @description How much does the number increase/decrease when using the arrows provided by the browser.
* @default 1
*/
step?: number | null
/**
* @description Requires the number to be an integer.
*/
integer: boolean
/**
* @description Optionally display units to the right of the input box.
*/
units?: string | null
placeholder?: string | null
/** Immutable means it can only be configed at the first config then never again
Default is false */
/**
* @description Once set, the value can never be changed.
* @default false
*/
immutable?: boolean
}) {
return new Value<AsRequired<number, Required>, never>(
@@ -316,7 +319,6 @@ export class Value<Type, Store> {
disabled: false,
immutable: a.immutable ?? false,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(number, a),
)
@@ -328,10 +330,10 @@ export class Value<Type, Store> {
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<number>
default: number | null
required: boolean
min?: number | null
max?: number | null
/** Default = '1' */
step?: number | null
integer: boolean
units?: string | null
@@ -340,7 +342,7 @@ export class Value<Type, Store> {
}
>,
) {
return new Value<number | null | undefined, Store>(async (options) => {
return new Value<number | null, Store>(async (options) => {
const a = await getA(options)
return {
type: "number" as const,
@@ -354,17 +356,26 @@ export class Value<Type, Store> {
disabled: false,
immutable: false,
...a,
...requiredLikeToAbove(a.required),
}
}, number.optional())
}, number.nullable())
}
static color<Required extends RequiredDefault<string>>(a: {
static color<Required extends boolean>(a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
/**
* @description optionally provide a default value.
* @type { default: string | null }
* @example default: null
* @example default: 'ffffff'
*/
default: string | null
required: Required
/** Immutable means it can only be configed at the first config then never again
Default is false */
/**
* @description Once set, the value can never be changed.
* @default false
*/
immutable?: boolean
}) {
return new Value<AsRequired<string, Required>, never>(
@@ -375,9 +386,7 @@ export class Value<Type, Store> {
disabled: false,
immutable: a.immutable ?? false,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
}
@@ -389,12 +398,13 @@ export class Value<Type, Store> {
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
default: string | null
required: boolean
disabled?: false | string
}
>,
) {
return new Value<string | null | undefined, Store>(async (options) => {
return new Value<string | null, Store>(async (options) => {
const a = await getA(options)
return {
type: "color" as const,
@@ -403,21 +413,33 @@ export class Value<Type, Store> {
disabled: false,
immutable: false,
...a,
...requiredLikeToAbove(a.required),
}
}, string.optional())
}, string.nullable())
}
static datetime<Required extends RequiredDefault<string>>(a: {
static datetime<Required extends boolean>(a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
/**
* @description optionally provide a default value.
* @type { default: string | null }
* @example default: null
* @example default: '1985-12-16 18:00:00.000'
*/
default: string | null
required: Required
/** Default = 'datetime-local' */
/**
* @description Informs the browser how to behave and which date/time component to display.
* @default "datetime-local"
*/
inputmode?: ValueSpecDatetime["inputmode"]
min?: string | null
max?: string | null
/** Immutable means it can only be configed at the first config then never again
Default is false */
/**
* @description Once set, the value can never be changed.
* @default false
*/
immutable?: boolean
}) {
return new Value<AsRequired<string, Required>, never>(
@@ -432,7 +454,6 @@ export class Value<Type, Store> {
disabled: false,
immutable: a.immutable ?? false,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
@@ -444,8 +465,8 @@ export class Value<Type, Store> {
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
/** Default = 'datetime-local' */
default: string | null
required: boolean
inputmode?: ValueSpecDatetime["inputmode"]
min?: string | null
max?: string | null
@@ -453,7 +474,7 @@ export class Value<Type, Store> {
}
>,
) {
return new Value<string | null | undefined, Store>(async (options) => {
return new Value<string | null, Store>(async (options) => {
const a = await getA(options)
return {
type: "datetime" as const,
@@ -465,30 +486,40 @@ export class Value<Type, Store> {
disabled: false,
immutable: false,
...a,
...requiredLikeToAbove(a.required),
}
}, string.optional())
}, string.nullable())
}
static select<
Required extends RequiredDefault<string>,
B extends Record<string, string>,
>(a: {
static select<Values extends Record<string, string>>(a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
required: Required
values: B
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
* @description Determines if the field is required. If so, optionally provide a default value from the list of values.
* @type { (keyof Values & string) | null }
* @example default: null
* @example default: 'radio1'
*/
default: keyof Values & string
/**
* @description A mapping of unique radio options to their human readable display format.
* @example
* ```
{
radio1: "Radio 1"
radio2: "Radio 2"
radio3: "Radio 3"
}
* ```
*/
values: Values
/**
* @description Once set, the value can never be changed.
* @default false
*/
disabled?: false | string | (string & keyof B)[]
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
}) {
return new Value<AsRequired<keyof B, Required>, never>(
return new Value<keyof Values & string, never>(
() => ({
description: null,
warning: null,
@@ -496,14 +527,10 @@ export class Value<Type, Store> {
disabled: false,
immutable: a.immutable ?? false,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(
anyOf(
...Object.keys(a.values).map((x: keyof B & string) => literal(x)),
),
a,
) as any,
anyOf(
...Object.keys(a.values).map((x: keyof Values & string) => literal(x)),
),
)
}
static dynamicSelect<Store = never>(
@@ -513,18 +540,13 @@ export class Value<Type, Store> {
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
default: string
values: Record<string, string>
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled?: false | string | string[]
}
>,
) {
return new Value<string | null | undefined, Store>(async (options) => {
return new Value<string, Store>(async (options) => {
const a = await getA(options)
return {
description: null,
@@ -533,27 +555,37 @@ export class Value<Type, Store> {
disabled: false,
immutable: false,
...a,
...requiredLikeToAbove(a.required),
}
}, string.optional())
}, string)
}
static multiselect<Values extends Record<string, string>>(a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
default: string[]
/**
* @description A simple list of which options should be checked by default.
*/
default: (keyof Values & string)[]
/**
* @description A mapping of checkbox options to their human readable display format.
* @example
* ```
{
option1: "Option 1"
option2: "Option 2"
option3: "Option 3"
}
* ```
*/
values: Values
minLength?: number | null
maxLength?: number | null
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
* @description Once set, the value can never be changed.
* @default false
*/
disabled?: false | string | (string & keyof Values)[]
immutable?: boolean
}) {
return new Value<(keyof Values)[], never>(
() => ({
@@ -582,11 +614,6 @@ export class Value<Type, Store> {
values: Record<string, string>
minLength?: number | null
maxLength?: number | null
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled?: false | string | string[]
}
>,
@@ -609,9 +636,8 @@ export class Value<Type, Store> {
a: {
name: string
description?: string | null
warning?: string | null
},
spec: Config<Type, Store>,
spec: InputSpec<Type, Store>,
) {
return new Value<Type, Store>(async (options) => {
const built = await spec.build(options as any)
@@ -624,69 +650,76 @@ export class Value<Type, Store> {
}
}, spec.validator)
}
static file<Required extends RequiredDefault<string>, Store>(a: {
name: string
description?: string | null
warning?: string | null
extensions: string[]
required: Required
}) {
const buildValue = {
type: "file" as const,
description: null,
warning: null,
...a,
}
return new Value<AsRequired<FilePath, Required>, Store>(
() => ({
...buildValue,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(object({ filePath: string }), a),
)
}
static dynamicFile<Required extends boolean, Store>(
a: LazyBuild<
Store,
{
// static file<Store, Required extends boolean>(a: {
// name: string
// description?: string | null
// extensions: string[]
// required: Required
// }) {
// const buildValue = {
// type: "file" as const,
// description: null,
// warning: null,
// ...a,
// }
// return new Value<AsRequired<FilePath, Required>, Store>(
// () => ({
// ...buildValue,
// }),
// asRequiredParser(object({ filePath: string }), a),
// )
// }
// static dynamicFile<Store>(
// a: LazyBuild<
// Store,
// {
// name: string
// description?: string | null
// warning?: string | null
// extensions: string[]
// required: boolean
// }
// >,
// ) {
// return new Value<FilePath | null, Store>(
// async (options) => ({
// type: "file" as const,
// description: null,
// warning: null,
// ...(await a(options)),
// }),
// object({ filePath: string }).nullable(),
// )
// }
static union<
VariantValues extends {
[K in string]: {
name: string
description?: string | null
warning?: string | null
extensions: string[]
required: Required
spec: InputSpec<any, Store> | InputSpec<any, never>
}
>,
) {
return new Value<string | null | undefined, Store>(
async (options) => ({
type: "file" as const,
description: null,
warning: null,
...(await a(options)),
}),
string.optional(),
)
}
static union<Required extends RequiredDefault<string>, Type, Store>(
},
Store,
>(
a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
required: Required
/** Immutable means it can only be configed at the first config then never again
Default is false */
immutable?: boolean
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
* @description Provide a default value from the list of variants.
* @type { string }
* @example default: 'variant1'
*/
disabled?: false | string | string[]
default: keyof VariantValues & string
/**
* @description Once set, the value can never be changed.
* @default false
*/
immutable?: boolean
},
aVariants: Variants<Type, Store>,
aVariants: Variants<VariantValues, Store>,
) {
return new Value<AsRequired<Type, Required>, Store>(
return new Value<typeof aVariants.validator._TYPE, Store>(
async (options) => ({
type: "union" as const,
description: null,
@@ -694,85 +727,106 @@ export class Value<Type, Store> {
disabled: false,
...a,
variants: await aVariants.build(options as any),
...requiredLikeToAbove(a.required),
immutable: a.immutable ?? false,
}),
asRequiredParser(aVariants.validator, a),
aVariants.validator,
)
}
static filteredUnion<
Required extends RequiredDefault<string>,
Type extends Record<string, any>,
Store = never,
VariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
Store,
>(
getDisabledFn: LazyBuild<Store, string[] | false | string>,
a: {
name: string
description?: string | null
warning?: string | null
required: Required
default: keyof VariantValues & string
},
aVariants: Variants<Type, Store> | Variants<Type, never>,
aVariants: Variants<VariantValues, Store> | Variants<VariantValues, never>,
) {
return new Value<AsRequired<Type, Required>, Store>(
return new Value<typeof aVariants.validator._TYPE, Store>(
async (options) => ({
type: "union" as const,
description: null,
warning: null,
...a,
variants: await aVariants.build(options as any),
...requiredLikeToAbove(a.required),
disabled: (await getDisabledFn(options)) || false,
immutable: false,
}),
asRequiredParser(aVariants.validator, a),
aVariants.validator,
)
}
static dynamicUnion<
Required extends RequiredDefault<string>,
Type extends Record<string, any>,
Store = never,
VariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
Store,
>(
getA: LazyBuild<
Store,
{
disabled: string[] | false | string
name: string
description?: string | null
warning?: string | null
required: Required
default: keyof VariantValues & string
disabled: string[] | false | string
}
>,
aVariants: Variants<Type, Store> | Variants<Type, never>,
aVariants: Variants<VariantValues, Store> | Variants<VariantValues, never>,
) {
return new Value<Type | null | undefined, Store>(async (options) => {
const newValues = await getA(options)
return {
type: "union" as const,
description: null,
warning: null,
...newValues,
variants: await aVariants.build(options as any),
...requiredLikeToAbove(newValues.required),
immutable: false,
}
}, aVariants.validator.optional())
return new Value<typeof aVariants.validator._TYPE, Store>(
async (options) => {
const newValues = await getA(options)
return {
type: "union" as const,
description: null,
warning: null,
...newValues,
variants: await aVariants.build(options as any),
immutable: false,
}
},
aVariants.validator,
)
}
static list<Type, Store>(a: List<Type, Store>) {
return new Value<Type, Store>((options) => a.build(options), a.validator)
}
static hidden<T>(parser: Parser<unknown, T> = any) {
return new Value<T, never>(async () => {
const built: ValueSpecHidden = {
type: "hidden" as const,
}
return built
}, parser)
}
map<U>(fn: (value: Type) => U): Value<U, Store> {
return new Value(this.build, this.validator.map(fn))
}
/**
* Use this during the times that the input needs a more specific type.
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
* Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else.
```ts
const a = Config.text({
const a = InputSpec.text({
name: "a",
required: false,
})
return Config.of<Store>()({
return InputSpec.of<Store>()({
myValue: a.withStore(),
})
```

View File

@@ -1,6 +1,33 @@
import { InputSpec, ValueSpecUnion } from "../configTypes"
import { LazyBuild, Config } from "./config"
import { Parser, anyOf, literals, object } from "ts-matches"
import { DeepPartial } from "../../../types"
import { ValueSpec, ValueSpecUnion } from "../inputSpecTypes"
import {
LazyBuild,
InputSpec,
ExtractInputSpecType,
ExtractPartialInputSpecType,
} from "./inputSpec"
import { Parser, anyOf, literal, object } from "ts-matches"
export type UnionRes<
Store,
VariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
K extends keyof VariantValues & string = keyof VariantValues & string,
> = {
[key in keyof VariantValues]: {
selection: key
value: ExtractInputSpecType<VariantValues[key]["spec"]>
other?: {
[key2 in Exclude<keyof VariantValues & string, key>]?: DeepPartial<
ExtractInputSpecType<VariantValues[key2]["spec"]>
>
}
}
}[K]
/**
* Used in the the Value.select { @link './value.ts' }
@@ -8,7 +35,7 @@ import { Parser, anyOf, literals, object } from "ts-matches"
* key to the tag.id in the Value.select
```ts
export const disabled = Config.of({});
export const disabled = InputSpec.of({});
export const size = Value.number({
name: "Max Chain Size",
default: 550,
@@ -20,7 +47,7 @@ export const size = Value.number({
units: "MiB",
placeholder: null,
});
export const automatic = Config.of({ size: size });
export const automatic = InputSpec.of({ size: size });
export const size1 = Value.number({
name: "Failsafe Chain Size",
default: 65536,
@@ -32,7 +59,7 @@ export const size1 = Value.number({
units: "MiB",
placeholder: null,
});
export const manual = Config.of({ size: size1 });
export const manual = InputSpec.of({ size: size1 });
export const pruningSettingsVariants = Variants.of({
disabled: { name: "Disabled", spec: disabled },
automatic: { name: "Automatic", spec: automatic },
@@ -44,51 +71,49 @@ export const pruning = Value.union(
description:
'- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the "pruneblockchain" RPC\n',
warning: null,
required: true,
default: "disabled",
},
pruningSettingsVariants
);
```
*/
export class Variants<Type, Store> {
static text: any
export class Variants<
VariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
Store,
> {
private constructor(
public build: LazyBuild<Store, ValueSpecUnion["variants"]>,
public validator: Parser<unknown, Type>,
public validator: Parser<unknown, UnionRes<Store, VariantValues>>,
) {}
static of<
VariantValues extends {
[K in string]: {
name: string
spec: Config<any, Store> | Config<any, never>
spec: InputSpec<any, Store> | InputSpec<any, never>
}
},
Store = never,
>(a: VariantValues) {
const validator = anyOf(
...Object.entries(a).map(([name, { spec }]) =>
...Object.entries(a).map(([id, { spec }]) =>
object({
selection: literals(name),
selection: literal(id),
value: spec.validator,
}),
),
) as Parser<unknown, any>
return new Variants<
{
[K in keyof VariantValues]: {
selection: K
// prettier-ignore
value:
VariantValues[K]["spec"] extends (Config<infer B, Store> | Config<infer B, never>) ? B :
never
}
}[keyof VariantValues],
Store
>(async (options) => {
return new Variants<VariantValues, Store>(async (options) => {
const variants = {} as {
[K in keyof VariantValues]: { name: string; spec: InputSpec }
[K in keyof VariantValues]: {
name: string
spec: Record<string, ValueSpec>
}
}
for (const key in a) {
const value = a[key]
@@ -102,19 +127,19 @@ export class Variants<Type, Store> {
}
/**
* Use this during the times that the input needs a more specific type.
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
* Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else.
```ts
const a = Config.text({
const a = InputSpec.text({
name: "a",
required: false,
})
return Config.of<Store>()({
return InputSpec.of<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Variants<Type, NewStore>
return this as any as Variants<VariantValues, NewStore>
}
}

View File

@@ -0,0 +1,3 @@
export * as constants from "./inputSpecConstants"
export * as types from "./inputSpecTypes"
export * as builder from "./builder"

View File

@@ -1,53 +1,51 @@
import { SmtpValue } from "../types"
import { GetSystemSmtp } from "../util/GetSystemSmtp"
import { email } from "../util/patterns"
import { Config, ConfigSpecOf } from "./builder/config"
import { SmtpValue } from "../../types"
import { GetSystemSmtp, Patterns } from "../../util"
import { InputSpec, InputSpecOf } from "./builder/inputSpec"
import { Value } from "./builder/value"
import { Variants } from "./builder/variants"
/**
* Base SMTP settings, to be used by StartOS for system wide SMTP
*/
export const customSmtp = Config.of<ConfigSpecOf<SmtpValue>, never>({
export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>, never>({
server: Value.text({
name: "SMTP Server",
required: {
default: null,
},
required: true,
default: null,
}),
port: Value.number({
name: "Port",
required: { default: 587 },
required: true,
default: 587,
min: 1,
max: 65535,
integer: true,
}),
from: Value.text({
name: "From Address",
required: {
default: null,
},
required: true,
default: null,
placeholder: "<name>test@example.com",
inputmode: "email",
patterns: [email],
patterns: [Patterns.email],
}),
login: Value.text({
name: "Login",
required: {
default: null,
},
required: true,
default: null,
}),
password: Value.text({
name: "Password",
required: false,
default: null,
masked: true,
}),
})
/**
* For service config. 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
*/
export const smtpConfig = Value.filteredUnion(
export const smtpInputSpec = Value.filteredUnion(
async ({ effects }) => {
const smtp = await new GetSystemSmtp(effects).once()
return smtp ? [] : ["system"]
@@ -55,21 +53,22 @@ export const smtpConfig = Value.filteredUnion(
{
name: "SMTP",
description: "Optionally provide an SMTP server for sending emails",
required: { default: "disabled" },
default: "disabled",
},
Variants.of({
disabled: { name: "Disabled", spec: Config.of({}) },
disabled: { name: "Disabled", spec: InputSpec.of({}) },
system: {
name: "System Credentials",
spec: Config.of({
spec: InputSpec.of({
customFrom: Value.text({
name: "Custom From Address",
description:
"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: [email],
patterns: [Patterns.email],
}),
}),
},

View File

@@ -12,6 +12,7 @@ export type ValueType =
| "object"
| "file"
| "union"
| "hidden"
export type ValueSpec = ValueSpecOf<ValueType>
/** core spec types. These types provide the metadata for performing validations */
// prettier-ignore
@@ -28,6 +29,7 @@ export type ValueSpecOf<T extends ValueType> =
T extends "object" ? ValueSpecObject :
T extends "file" ? ValueSpecFile :
T extends "union" ? ValueSpecUnion :
T extends "hidden" ? ValueSpecHidden :
never
export type ValueSpecText = {
@@ -48,7 +50,6 @@ export type ValueSpecText = {
default: DefaultString | null
disabled: false | string
generate: null | RandomString
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecTextarea = {
@@ -62,7 +63,6 @@ export type ValueSpecTextarea = {
maxLength: number | null
required: boolean
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
@@ -83,7 +83,6 @@ export type ValueSpecNumber = {
required: boolean
default: number | null
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecColor = {
@@ -95,7 +94,6 @@ export type ValueSpecColor = {
required: boolean
default: string | null
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecDatetime = {
@@ -109,7 +107,6 @@ export type ValueSpecDatetime = {
max: string | null
default: string | null
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecSelect = {
@@ -118,15 +115,8 @@ export type ValueSpecSelect = {
description: string | null
warning: string | null
type: "select"
required: boolean
default: string | null
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled: false | string | string[]
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecMultiselect = {
@@ -139,14 +129,8 @@ export type ValueSpecMultiselect = {
type: "multiselect"
minLength: number | null
maxLength: number | null
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled: false | string | string[]
default: string[]
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecToggle = {
@@ -157,7 +141,6 @@ export type ValueSpecToggle = {
type: "toggle"
default: boolean | null
disabled: false | string
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecUnion = {
@@ -173,15 +156,8 @@ export type ValueSpecUnion = {
spec: InputSpec
}
>
/**
* Disabled: false means that there is nothing disabled, good to modify
* string means that this is the message displayed and the whole thing is disabled
* string[] means that the options are disabled
*/
disabled: false | string | string[]
required: boolean
default: string | null
/** Immutable means it can only be configured at the first config then never again */
immutable: boolean
}
export type ValueSpecFile = {
@@ -199,14 +175,15 @@ export type ValueSpecObject = {
type: "object"
spec: InputSpec
}
export type ValueSpecHidden = {
type: "hidden"
}
export type ListValueSpecType = "text" | "object"
/** represents a spec for the values of a list */
// prettier-ignore
export type ListValueSpecOf<T extends ListValueSpecType> =
T extends "text" ? ListValueSpecText :
T extends "object" ? ListValueSpecObject :
never
/** represents a spec for a list */
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
export type ValueSpecListOf<T extends ListValueSpecType> = {
name: string
@@ -242,13 +219,13 @@ export type ListValueSpecText = {
}
export type ListValueSpecObject = {
type: "object"
/** this is a mapped type of the config object at this level, replacing the object's values with specs on those values */
spec: InputSpec
/** indicates whether duplicates can be permitted in the list */
uniqueBy: UniqueBy
/** this should be a handlebars template which can make use of the entire config which corresponds to 'spec' */
displayAs: string | null
}
// TODO Aiden do we really want this expressivity? Why not the below. Also what's with the "readonly" portion?
// export type UniqueBy = null | string | { any: string[] } | { all: string[] }
export type UniqueBy =
| null
| string

View File

@@ -0,0 +1,155 @@
import { InputSpec } from "./input/builder"
import {
ExtractInputSpecType,
ExtractPartialInputSpecType,
} from "./input/builder/inputSpec"
import * as T from "../types"
import { once } from "../util"
export type Run<
A extends
| Record<string, any>
| InputSpec<Record<string, any>, any>
| InputSpec<Record<string, never>, never>,
> = (options: {
effects: T.Effects
input: ExtractInputSpecType<A> & Record<string, any>
}) => Promise<T.ActionResult | null | void | undefined>
export type GetInput<
A extends
| Record<string, any>
| InputSpec<Record<string, any>, any>
| InputSpec<Record<string, any>, never>,
> = (options: {
effects: T.Effects
}) => Promise<
| null
| void
| undefined
| (ExtractPartialInputSpecType<A> & Record<string, any>)
>
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
function callMaybeFn<T>(
maybeFn: MaybeFn<T>,
options: { effects: T.Effects },
): Promise<T> {
if (maybeFn instanceof Function) {
return maybeFn(options)
} else {
return Promise.resolve(maybeFn)
}
}
function mapMaybeFn<T, U>(
maybeFn: MaybeFn<T>,
map: (value: T) => U,
): MaybeFn<U> {
if (maybeFn instanceof Function) {
return async (...args) => map(await maybeFn(...args))
} else {
return map(maybeFn)
}
}
export class Action<
Id extends T.ActionId,
Store,
InputSpecType extends
| Record<string, any>
| InputSpec<any, Store>
| InputSpec<any, never>,
> {
private constructor(
readonly id: Id,
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
private readonly inputSpec: InputSpecType,
private readonly getInputFn: GetInput<ExtractInputSpecType<InputSpecType>>,
private readonly runFn: Run<ExtractInputSpecType<InputSpecType>>,
) {}
static withInput<
Id extends T.ActionId,
Store,
InputSpecType extends
| Record<string, any>
| InputSpec<any, Store>
| InputSpec<any, never>,
>(
id: Id,
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
inputSpec: InputSpecType,
getInput: GetInput<ExtractInputSpecType<InputSpecType>>,
run: Run<ExtractInputSpecType<InputSpecType>>,
): Action<Id, Store, InputSpecType> {
return new Action(
id,
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })),
inputSpec,
getInput,
run,
)
}
static withoutInput<Id extends T.ActionId, Store>(
id: Id,
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
run: Run<{}>,
): Action<Id, Store, {}> {
return new Action(
id,
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })),
{},
async () => null,
run,
)
}
async exportMetadata(options: {
effects: T.Effects
}): Promise<T.ActionMetadata> {
const metadata = await callMaybeFn(this.metadataFn, options)
await options.effects.action.export({ id: this.id, metadata })
return metadata
}
async getInput(options: { effects: T.Effects }): Promise<T.ActionInput> {
return {
spec: await this.inputSpec.build(options),
value: (await this.getInputFn(options)) || null,
}
}
async run(options: {
effects: T.Effects
input: ExtractInputSpecType<InputSpecType>
}): Promise<T.ActionResult | null> {
return (await this.runFn(options)) || null
}
}
export class Actions<
Store,
AllActions extends Record<T.ActionId, Action<T.ActionId, Store, any>>,
> {
private constructor(private readonly actions: AllActions) {}
static of<Store>(): Actions<Store, {}> {
return new Actions({})
}
addAction<A extends Action<T.ActionId, Store, any>>(
action: A,
): Actions<Store, AllActions & { [id in A["id"]]: A }> {
return new Actions({ ...this.actions, [action.id]: action })
}
async update(options: { effects: T.Effects }): Promise<null> {
options.effects = {
...options.effects,
constRetry: once(() => {
this.update(options) // yes, this reuses the options object, but the const retry function will be overwritten each time, so the once-ness is not a problem
}),
}
for (let action of Object.values(this.actions)) {
await action.exportMetadata(options)
}
await options.effects.action.clear({ except: Object.keys(this.actions) })
return null
}
get<Id extends T.ActionId>(actionId: Id): AllActions[Id] {
return this.actions[actionId]
}
}

View File

@@ -0,0 +1,208 @@
import * as T from "../types"
import * as child_process from "child_process"
import { asError } from "../util"
export const DEFAULT_OPTIONS: T.SyncOptions = {
delete: true,
exclude: [],
}
export type BackupSync<Volumes extends string> = {
dataPath: `/media/startos/volumes/${Volumes}/${string}`
backupPath: `/media/startos/backup/${string}`
options?: Partial<T.SyncOptions>
backupOptions?: Partial<T.SyncOptions>
restoreOptions?: Partial<T.SyncOptions>
}
/**
* This utility simplifies the volume backup process.
* ```ts
* export const { createBackup, restoreBackup } = Backups.volumes("main").build();
* ```
*
* Changing the options of the rsync, (ie excludes) use either
* ```ts
* Backups.volumes("main").set_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
* // or
* Backups.with_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
* ```
*
* Using the more fine control, using the addSets for more control
* ```ts
* Backups.addSets({
* srcVolume: 'main', srcPath:'smallData/', dstPath: 'main/smallData/', dstVolume: : Backups.BACKUP
* }, {
* srcVolume: 'main', srcPath:'bigData/', dstPath: 'main/bigData/', dstVolume: : Backups.BACKUP, options: {exclude:['bigData/excludeThis']}}
* ).build()q
* ```
*/
export class Backups<M extends T.Manifest> {
private constructor(
private options = DEFAULT_OPTIONS,
private restoreOptions: Partial<T.SyncOptions> = {},
private backupOptions: Partial<T.SyncOptions> = {},
private backupSet = [] as BackupSync<M["volumes"][number]>[],
) {}
static withVolumes<M extends T.Manifest = never>(
...volumeNames: Array<M["volumes"][number]>
): Backups<M> {
return Backups.withSyncs(
...volumeNames.map((srcVolume) => ({
dataPath: `/media/startos/volumes/${srcVolume}/` as const,
backupPath: `/media/startos/backup/${srcVolume}/` as const,
})),
)
}
static withSyncs<M extends T.Manifest = never>(
...syncs: BackupSync<M["volumes"][number]>[]
) {
return syncs.reduce((acc, x) => acc.addSync(x), new Backups<M>())
}
static withOptions<M extends T.Manifest = never>(
options?: Partial<T.SyncOptions>,
) {
return new Backups<M>({ ...DEFAULT_OPTIONS, ...options })
}
setOptions(options?: Partial<T.SyncOptions>) {
this.options = {
...this.options,
...options,
}
return this
}
setBackupOptions(options?: Partial<T.SyncOptions>) {
this.backupOptions = {
...this.backupOptions,
...options,
}
return this
}
setRestoreOptions(options?: Partial<T.SyncOptions>) {
this.restoreOptions = {
...this.restoreOptions,
...options,
}
return this
}
addVolume(
volume: M["volumes"][number],
options?: Partial<{
options: T.SyncOptions
backupOptions: T.SyncOptions
restoreOptions: T.SyncOptions
}>,
) {
return this.addSync({
dataPath: `/media/startos/volumes/${volume}/` as const,
backupPath: `/media/startos/backup/${volume}/` as const,
...options,
})
}
addSync(sync: BackupSync<M["volumes"][0]>) {
this.backupSet.push({
...sync,
options: { ...this.options, ...sync.options },
})
return this
}
async createBackup() {
for (const item of this.backupSet) {
const rsyncResults = await runRsync({
srcPath: item.dataPath,
dstPath: item.backupPath,
options: {
...this.options,
...this.backupOptions,
...item.options,
...item.backupOptions,
},
})
await rsyncResults.wait()
}
return
}
async restoreBackup() {
for (const item of this.backupSet) {
const rsyncResults = await runRsync({
srcPath: item.backupPath,
dstPath: item.dataPath,
options: {
...this.options,
...this.backupOptions,
...item.options,
...item.backupOptions,
},
})
await rsyncResults.wait()
}
return
}
}
async function runRsync(rsyncOptions: {
srcPath: string
dstPath: string
options: T.SyncOptions
}): Promise<{
id: () => Promise<string>
wait: () => Promise<null>
progress: () => Promise<number>
}> {
const { srcPath, dstPath, options } = rsyncOptions
const command = "rsync"
const args: string[] = []
if (options.delete) {
args.push("--delete")
}
for (const exclude of options.exclude) {
args.push(`--exclude=${exclude}`)
}
args.push("-actAXH")
args.push("--info=progress2")
args.push("--no-inc-recursive")
args.push(srcPath)
args.push(dstPath)
const spawned = child_process.spawn(command, args, { detached: true })
let percentage = 0.0
spawned.stdout.on("data", (data: unknown) => {
const lines = String(data).replace("\r", "\n").split("\n")
for (const line of lines) {
const parsed = /$([0-9.]+)%/.exec(line)?.[1]
if (!parsed) continue
percentage = Number.parseFloat(parsed)
}
})
spawned.stderr.on("data", (data: unknown) => {
console.error(`Backups.runAsync`, asError(data))
})
const id = async () => {
const pid = spawned.pid
if (pid === undefined) {
throw new Error("rsync process has no pid")
}
return String(pid)
}
const waitPromise = new Promise<null>((resolve, reject) => {
spawned.on("exit", (code: any) => {
if (code === 0) {
resolve(null)
} else {
reject(new Error(`rsync exited with code ${code}`))
}
})
})
const wait = () => waitPromise
const progress = () => Promise.resolve(percentage)
return { id, wait, progress }
}

View File

@@ -0,0 +1,39 @@
import { Backups } from "./Backups"
import * as T from "../types"
import { _ } from "../util"
export type SetupBackupsParams<M extends T.Manifest> =
| M["volumes"][number][]
| ((_: { effects: T.Effects }) => Promise<Backups<M>>)
type SetupBackupsRes = {
createBackup: T.ExpectedExports.createBackup
restoreBackup: T.ExpectedExports.restoreBackup
}
export function setupBackups<M extends T.Manifest>(
options: SetupBackupsParams<M>,
) {
let backupsFactory: (_: { effects: T.Effects }) => Promise<Backups<M>>
if (options instanceof Function) {
backupsFactory = options
} else {
backupsFactory = async () => Backups.withVolumes(...options)
}
const answer: {
createBackup: T.ExpectedExports.createBackup
restoreBackup: T.ExpectedExports.restoreBackup
} = {
get createBackup() {
return (async (options) => {
return (await backupsFactory(options)).createBackup()
}) as T.ExpectedExports.createBackup
},
get restoreBackup() {
return (async (options) => {
return (await backupsFactory(options)).restoreBackup()
}) as T.ExpectedExports.restoreBackup
},
}
return answer
}

View File

@@ -1,33 +1,27 @@
import { ExtendedVersion, VersionRange } from "../exver"
import {
Effects,
PackageId,
DependencyRequirement,
SetHealth,
CheckDependenciesResult,
HealthCheckId,
} from "../types"
import { PackageId, HealthCheckId } from "../types"
import { Effects } from "../Effects"
export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
installedSatisfied: (packageId: DependencyId) => boolean
installedVersionSatisfied: (packageId: DependencyId) => boolean
runningSatisfied: (packageId: DependencyId) => boolean
configSatisfied: (packageId: DependencyId) => boolean
actionsSatisfied: (packageId: DependencyId) => boolean
healthCheckSatisfied: (
packageId: DependencyId,
healthCheckId: HealthCheckId,
) => boolean
satisfied: () => boolean
throwIfInstalledNotSatisfied: (packageId: DependencyId) => void
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => void
throwIfRunningNotSatisfied: (packageId: DependencyId) => void
throwIfConfigNotSatisfied: (packageId: DependencyId) => void
throwIfInstalledNotSatisfied: (packageId: DependencyId) => null
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => null
throwIfRunningNotSatisfied: (packageId: DependencyId) => null
throwIfActionsNotSatisfied: (packageId: DependencyId) => null
throwIfHealthNotSatisfied: (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => void
throwIfNotSatisfied: (packageId?: DependencyId) => void
) => null
throwIfNotSatisfied: (packageId?: DependencyId) => null
}
export async function checkDependencies<
DependencyId extends PackageId = PackageId,
@@ -71,8 +65,8 @@ export async function checkDependencies<
const dep = find(packageId)
return dep.requirement.kind !== "running" || dep.result.isRunning
}
const configSatisfied = (packageId: DependencyId) =>
find(packageId).result.configSatisfied
const actionsSatisfied = (packageId: DependencyId) =>
Object.keys(find(packageId).result.requestedActions).length === 0
const healthCheckSatisfied = (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
@@ -94,7 +88,7 @@ export async function checkDependencies<
installedSatisfied(packageId) &&
installedVersionSatisfied(packageId) &&
runningSatisfied(packageId) &&
configSatisfied(packageId) &&
actionsSatisfied(packageId) &&
healthCheckSatisfied(packageId)
const satisfied = (packageId?: DependencyId) =>
packageId
@@ -106,6 +100,7 @@ export async function checkDependencies<
if (!dep.result.installedVersion) {
throw new Error(`${dep.result.title || packageId} is not installed`)
}
return null
}
const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
@@ -123,20 +118,24 @@ export async function checkDependencies<
`Installed version ${dep.result.installedVersion} of ${dep.result.title || packageId} does not match expected version range ${dep.requirement.versionRange}`,
)
}
return null
}
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
throw new Error(`${dep.result.title || packageId} is not running`)
}
return null
}
const throwIfConfigNotSatisfied = (packageId: DependencyId) => {
const throwIfActionsNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
if (!dep.result.configSatisfied) {
const reqs = Object.keys(dep.result.requestedActions)
if (reqs.length) {
throw new Error(
`${dep.result.title || packageId}'s configuration does not satisfy requirements`,
`The following action requests have not been fulfilled: ${reqs.join(", ")}`,
)
}
return null
}
const throwIfHealthNotSatisfied = (
packageId: DependencyId,
@@ -163,13 +162,15 @@ export async function checkDependencies<
.join("; "),
)
}
return null
}
const throwIfPkgNotSatisfied = (packageId: DependencyId) => {
throwIfInstalledNotSatisfied(packageId)
throwIfInstalledVersionNotSatisfied(packageId)
throwIfRunningNotSatisfied(packageId)
throwIfConfigNotSatisfied(packageId)
throwIfActionsNotSatisfied(packageId)
throwIfHealthNotSatisfied(packageId)
return null
}
const throwIfNotSatisfied = (packageId?: DependencyId) =>
packageId
@@ -187,19 +188,20 @@ export async function checkDependencies<
if (err.length) {
throw new Error(err.join("; "))
}
return null
})()
return {
installedSatisfied,
installedVersionSatisfied,
runningSatisfied,
configSatisfied,
actionsSatisfied,
healthCheckSatisfied,
satisfied,
throwIfInstalledNotSatisfied,
throwIfInstalledVersionNotSatisfied,
throwIfRunningNotSatisfied,
throwIfConfigNotSatisfied,
throwIfActionsNotSatisfied,
throwIfHealthNotSatisfied,
throwIfNotSatisfied,
}

View File

@@ -4,6 +4,3 @@ export type ReadonlyDeep<A> =
A extends {} ? { readonly [K in keyof A]: ReadonlyDeep<A[K]> } : A;
export type MaybePromise<A> = Promise<A> | A
export type Message = string
import "./DependencyConfig"
import "./setupDependencyConfig"

View File

@@ -0,0 +1,62 @@
import * as T from "../types"
import { once } from "../util"
export type RequiredDependenciesOf<Manifest extends T.SDKManifest> = {
[K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends false
? K
: never
}[keyof Manifest["dependencies"]]
export type OptionalDependenciesOf<Manifest extends T.SDKManifest> = Exclude<
keyof Manifest["dependencies"],
RequiredDependenciesOf<Manifest>
>
type DependencyRequirement =
| {
kind: "running"
healthChecks: Array<T.HealthCheckId>
versionRange: string
}
| {
kind: "exists"
versionRange: string
}
type Matches<T, U> = T extends U ? (U extends T ? null : never) : never
const _checkType: Matches<
DependencyRequirement & { id: T.PackageId },
T.DependencyRequirement
> = null
export type CurrentDependenciesResult<Manifest extends T.SDKManifest> = {
[K in RequiredDependenciesOf<Manifest>]: DependencyRequirement
} & {
[K in OptionalDependenciesOf<Manifest>]?: DependencyRequirement
} & Record<string, DependencyRequirement>
export function setupDependencies<Manifest extends T.SDKManifest>(
fn: (options: {
effects: T.Effects
}) => Promise<CurrentDependenciesResult<Manifest>>,
): (options: { effects: T.Effects }) => Promise<null> {
const cell = { updater: async (_: { effects: T.Effects }) => null }
cell.updater = async (options: { effects: T.Effects }) => {
options.effects = {
...options.effects,
constRetry: once(() => {
cell.updater(options)
}),
}
const dependencyType = await fn(options)
return await options.effects.setDependencies({
dependencies: Object.entries(dependencyType).map(
([id, { versionRange, ...x }, ,]) =>
({
// id,
...x,
versionRange: versionRange.toString(),
}) as T.DependencyRequirement,
),
})
}
return cell.updater
}

View File

@@ -1,13 +1,12 @@
export { S9pk } from "./s9pk"
export { VersionRange, ExtendedVersion, Version } from "./exver"
export * as config from "./config"
export * as CB from "./config/builder"
export * as CT from "./config/configTypes"
export * as dependencyConfig from "./dependencies"
export * as inputSpec from "./actions/input"
export * as ISB from "./actions/input/builder"
export * as IST from "./actions/input/inputSpecTypes"
export * as types from "./types"
export * as T from "./types"
export * as yaml from "yaml"
export * as matches from "ts-matches"
export * as utils from "./util/index.browser"
export * as utils from "./util"

View File

@@ -1,10 +1,10 @@
import { number, object, string } from "ts-matches"
import { Effects } from "../types"
import { object, string } from "ts-matches"
import { Effects } from "../Effects"
import { Origin } from "./Origin"
import { AddSslOptions, BindParams } from ".././osBindings"
import { Security } from ".././osBindings"
import { BindOptions } from ".././osBindings"
import { AlpnInfo } from ".././osBindings"
import { AddSslOptions, BindParams } from "../osBindings"
import { Security } from "../osBindings"
import { BindOptions } from "../osBindings"
import { AlpnInfo } from "../osBindings"
export { AddSslOptions, Security, BindOptions }
@@ -94,6 +94,22 @@ export class Host {
},
) {}
/**
* @description Use this function to bind the host to an internal port and configured options for protocol, security, and external port.
*
* @param internalPort - The internal port to be bound.
* @param options - The protocol options for this binding.
* @returns A multi-origin that is capable of exporting one or more service interfaces.
* @example
* In this example, we bind a previously created multi-host to port 80, then select the http protocol and request an external port of 8332.
*
* ```
const uiMultiOrigin = await uiMulti.bindPort(80, {
protocol: 'http',
preferredExternalPort: 8332,
})
* ```
*/
async bindPort(
internalPort: number,
options: BindOptionsByProtocol,

View File

@@ -1,6 +1,6 @@
import { AddressInfo } from "../types"
import { AddressReceipt } from "./AddressReceipt"
import { Host, BindOptions, Scheme } from "./Host"
import { Host, Scheme } from "./Host"
import { ServiceInterfaceBuilder } from "./ServiceInterfaceBuilder"
export class Origin<T extends Host> {
@@ -31,9 +31,9 @@ export class Origin<T extends Host> {
}
/**
* A function to register a group of origins (<PROTOCOL> :// <HOSTNAME> : <PORT>) with StartOS
* @description A function to register a group of origins (<PROTOCOL> :// <HOSTNAME> : <PORT>) with StartOS
*
* The returned addressReceipt serves as proof that the addresses were registered
* The returned addressReceipt serves as proof that the addresses were registered
*
* @param addressInfo
* @returns

View File

@@ -1,5 +1,5 @@
import { ServiceInterfaceType } from "../StartSdk"
import { Effects } from "../types"
import { ServiceInterfaceType } from "../types"
import { Effects } from "../Effects"
import { Scheme } from "./Host"
/**

View File

@@ -0,0 +1,57 @@
import * as T from "../types"
import { once } from "../util"
import { AddressReceipt } from "./AddressReceipt"
declare const UpdateServiceInterfacesProof: unique symbol
export type UpdateServiceInterfacesReceipt = {
[UpdateServiceInterfacesProof]: never
}
export type ServiceInterfacesReceipt = Array<T.AddressInfo[] & AddressReceipt>
export type SetServiceInterfaces<Output extends ServiceInterfacesReceipt> =
(opts: { effects: T.Effects }) => Promise<Output>
export type UpdateServiceInterfaces<Output extends ServiceInterfacesReceipt> =
(opts: {
effects: T.Effects
}) => Promise<Output & UpdateServiceInterfacesReceipt>
export type SetupServiceInterfaces = <Output extends ServiceInterfacesReceipt>(
fn: SetServiceInterfaces<Output>,
) => UpdateServiceInterfaces<Output>
export const NO_INTERFACE_CHANGES = {} as UpdateServiceInterfacesReceipt
export const setupServiceInterfaces: SetupServiceInterfaces = <
Output extends ServiceInterfacesReceipt,
>(
fn: SetServiceInterfaces<Output>,
) => {
const cell = {
updater: (async (options: { effects: T.Effects }) =>
[] as any as Output) as UpdateServiceInterfaces<Output>,
}
cell.updater = (async (options: { effects: T.Effects }) => {
options.effects = {
...options.effects,
constRetry: once(() => {
cell.updater(options)
}),
}
const bindings: T.BindId[] = []
const interfaces: T.ServiceInterfaceId[] = []
const res = await fn({
effects: {
...options.effects,
bind: (params: T.BindParams) => {
bindings.push({ id: params.id, internalPort: params.internalPort })
return options.effects.bind(params)
},
exportServiceInterface: (params: T.ExportServiceInterfaceParams) => {
interfaces.push(params.id)
return options.effects.exportServiceInterface(params)
},
},
})
await options.effects.clearBindings({ except: bindings })
await options.effects.clearServiceInterfaces({ except: interfaces })
return res
}) as UpdateServiceInterfaces<Output>
return cell.updater
}

View File

@@ -0,0 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AcmeSettings = {
provider: string
/**
* email addresses for letsencrypt
*/
contact: Array<string>
/**
* domains to get letsencrypt certs for
*/
domains: string[]
}

View 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 ActionInput = {
spec: Record<string, unknown>
value: Record<string, unknown> | null
}

View File

@@ -1,12 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionVisibility } from "./ActionVisibility"
import type { AllowedStatuses } from "./AllowedStatuses"
export type ActionMetadata = {
name: string
description: string
warning: string | null
input: any
disabled: boolean
visibility: ActionVisibility
allowedStatuses: AllowedStatuses
hasInput: boolean
group: string | null
}

View 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 { ActionId } from "./ActionId"
import type { ActionRequestInput } from "./ActionRequestInput"
import type { ActionRequestTrigger } from "./ActionRequestTrigger"
import type { ActionSeverity } from "./ActionSeverity"
import type { PackageId } from "./PackageId"
export type ActionRequest = {
packageId: PackageId
actionId: ActionId
severity: ActionSeverity
reason?: string
when?: ActionRequestTrigger
input?: ActionRequestInput
}

View File

@@ -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 AllowedStatuses = "onlyRunning" | "onlyStopped" | "any"
export type ActionRequestCondition = "input-not-matches"

View 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 { ActionRequest } from "./ActionRequest"
export type ActionRequestEntry = { request: ActionRequest; active: boolean }

View 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 ActionRequestInput = {
kind: "partial"
value: Record<string, unknown>
}

View 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 { ActionRequestCondition } from "./ActionRequestCondition"
export type ActionRequestTrigger = {
once: boolean
condition: ActionRequestCondition
}

View 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 { ActionResultV0 } from "./ActionResultV0"
import type { ActionResultV1 } from "./ActionResultV1"
export type ActionResult =
| ({ version: "0" } & ActionResultV0)
| ({ version: "1" } & ActionResultV1)

View 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.
export type ActionResultMember = {
name: string
description: string | null
} & (
| {
type: "single"
value: string
copyable: boolean
qr: boolean
masked: boolean
}
| { type: "group"; value: Array<ActionResultMember> }
)

View 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 ActionResultV0 = {
message: string
value: string | null
copyable: boolean
qr: boolean
}

View 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 { ActionResultValue } from "./ActionResultValue"
export type ActionResultV1 = {
title: string
message: string | null
result: ActionResultValue | null
}

View File

@@ -0,0 +1,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionResultMember } from "./ActionResultMember"
export type ActionResultValue =
| {
type: "single"
value: string
copyable: boolean
qr: boolean
masked: boolean
}
| { type: "group"; value: Array<ActionResultMember> }

View 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 ActionSeverity = "critical" | "important"

View 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 ActionVisibility = "hidden" | { disabled: string } | "enabled"

View 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 AddCategoryParams = {
id: string
name: string
short: string
long: string
}

View 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 AllowedStatuses = "only-running" | "only-stopped" | "any"

View 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 { HostId } from "./HostId"
export type BindId = { id: HostId; internalPort: number }

View File

@@ -2,4 +2,4 @@
import type { BindOptions } from "./BindOptions"
import type { LanInfo } from "./LanInfo"
export type BindInfo = { options: BindOptions; lan: LanInfo }
export type BindInfo = { enabled: boolean; options: BindOptions; lan: LanInfo }

View File

@@ -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 SetConfigured = { configured: boolean }
export type BuildArg = string | { env: string }

View File

@@ -1,14 +1,17 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionRequestEntry } from "./ActionRequestEntry"
import type { HealthCheckId } from "./HealthCheckId"
import type { NamedHealthCheckResult } from "./NamedHealthCheckResult"
import type { PackageId } from "./PackageId"
import type { ReplayId } from "./ReplayId"
import type { Version } from "./Version"
export type CheckDependenciesResult = {
packageId: PackageId
title: string | null
installedVersion: string | null
satisfies: string[]
installedVersion: Version | null
satisfies: Array<Version>
isRunning: boolean
configSatisfied: boolean
requestedActions: { [key: ReplayId]: ActionRequestEntry }
healthChecks: { [key: HealthCheckId]: NamedHealthCheckResult }
}

View 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.
export type ClearActionRequestsParams =
| { only: string[] }
| { except: string[] }

View 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 { ActionId } from "./ActionId"
export type ClearActionsParams = { except: Array<ActionId> }

View 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 { BindId } from "./BindId"
export type ClearBindingsParams = { except: Array<BindId> }

View 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 ClearCallbacksParams = { only: number[] } | { except: number[] }

View 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 { ServiceInterfaceId } from "./ServiceInterfaceId"
export type ClearServiceInterfacesParams = { except: Array<ServiceInterfaceId> }

View 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 CliSetIconParams = { icon: string }

View File

@@ -1,4 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ImageId } from "./ImageId"
export type CreateSubcontainerFsParams = { imageId: ImageId }
export type CreateSubcontainerFsParams = {
imageId: ImageId
name: string | null
}

View File

@@ -5,5 +5,4 @@ export type CurrentDependencyInfo = {
title: string | null
icon: DataUrl | null
versionRange: string
configSatisfied: boolean
} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] })

Some files were not shown because too many files have changed in this diff Show More