mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major
This commit is contained in:
12
container-runtime/package-lock.json
generated
12
container-runtime/package-lock.json
generated
@@ -20,6 +20,7 @@
|
||||
"node-fetch": "^3.1.0",
|
||||
"ts-matches": "^5.5.1",
|
||||
"tslib": "^2.5.3",
|
||||
"tslog": "^4.9.3",
|
||||
"typescript": "^5.1.3",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
@@ -5527,6 +5528,17 @@
|
||||
"version": "2.6.3",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tslog": {
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz",
|
||||
"integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fullstack-build/tslog?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import { types as T, utils } from "@start9labs/start-sdk"
|
||||
import * as net from "net"
|
||||
import { object, string, number, literals, some, unknown } from "ts-matches"
|
||||
import { Effects } from "../Models/Effects"
|
||||
@@ -40,7 +40,7 @@ export type EffectContext = {
|
||||
|
||||
const rpcRoundFor =
|
||||
(procedureId: string | null) =>
|
||||
<K extends keyof Effects | "getStore" | "setStore" | "clearCallbacks">(
|
||||
<K extends T.EffectMethod | "clearCallbacks">(
|
||||
method: K,
|
||||
params: Record<string, unknown>,
|
||||
) => {
|
||||
@@ -65,7 +65,10 @@ const rpcRoundFor =
|
||||
)
|
||||
if (testRpcError(res)) {
|
||||
let message = res.error.message
|
||||
console.error("Error in host RPC:", { method, params })
|
||||
console.error(
|
||||
"Error in host RPC:",
|
||||
utils.asError({ method, params }),
|
||||
)
|
||||
if (string.test(res.error.data)) {
|
||||
message += ": " + res.error.data
|
||||
console.error(`Details: ${res.error.data}`)
|
||||
@@ -107,65 +110,65 @@ function makeEffects(context: EffectContext): Effects {
|
||||
}) as ReturnType<T.Effects["bind"]>
|
||||
},
|
||||
clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
|
||||
return rpcRound("clearBindings", {}) as ReturnType<
|
||||
return rpcRound("clear-bindings", {}) as ReturnType<
|
||||
T.Effects["clearBindings"]
|
||||
>
|
||||
},
|
||||
clearServiceInterfaces(
|
||||
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
|
||||
) {
|
||||
return rpcRound("clearServiceInterfaces", {}) as ReturnType<
|
||||
return rpcRound("clear-service-interfaces", {}) as ReturnType<
|
||||
T.Effects["clearServiceInterfaces"]
|
||||
>
|
||||
},
|
||||
getInstalledPackages(...[]: Parameters<T.Effects["getInstalledPackages"]>) {
|
||||
return rpcRound("getInstalledPackages", {}) as ReturnType<
|
||||
return rpcRound("get-installed-packages", {}) as ReturnType<
|
||||
T.Effects["getInstalledPackages"]
|
||||
>
|
||||
},
|
||||
createOverlayedImage(options: {
|
||||
imageId: string
|
||||
}): Promise<[string, string]> {
|
||||
return rpcRound("createOverlayedImage", options) as ReturnType<
|
||||
T.Effects["createOverlayedImage"]
|
||||
>
|
||||
},
|
||||
destroyOverlayedImage(options: { guid: string }): Promise<void> {
|
||||
return rpcRound("destroyOverlayedImage", options) as ReturnType<
|
||||
T.Effects["destroyOverlayedImage"]
|
||||
>
|
||||
subcontainer: {
|
||||
createFs(options: { imageId: string }) {
|
||||
return rpcRound("subcontainer.create-fs", options) as ReturnType<
|
||||
T.Effects["subcontainer"]["createFs"]
|
||||
>
|
||||
},
|
||||
destroyFs(options: { guid: string }): Promise<void> {
|
||||
return rpcRound("subcontainer.destroy-fs", options) as ReturnType<
|
||||
T.Effects["subcontainer"]["destroyFs"]
|
||||
>
|
||||
},
|
||||
},
|
||||
executeAction(...[options]: Parameters<T.Effects["executeAction"]>) {
|
||||
return rpcRound("executeAction", options) as ReturnType<
|
||||
return rpcRound("execute-action", options) as ReturnType<
|
||||
T.Effects["executeAction"]
|
||||
>
|
||||
},
|
||||
exportAction(...[options]: Parameters<T.Effects["exportAction"]>) {
|
||||
return rpcRound("exportAction", options) as ReturnType<
|
||||
return rpcRound("export-action", options) as ReturnType<
|
||||
T.Effects["exportAction"]
|
||||
>
|
||||
},
|
||||
exportServiceInterface: ((
|
||||
...[options]: Parameters<Effects["exportServiceInterface"]>
|
||||
) => {
|
||||
return rpcRound("exportServiceInterface", options) as ReturnType<
|
||||
return rpcRound("export-service-interface", options) as ReturnType<
|
||||
T.Effects["exportServiceInterface"]
|
||||
>
|
||||
}) as Effects["exportServiceInterface"],
|
||||
exposeForDependents(
|
||||
...[options]: Parameters<T.Effects["exposeForDependents"]>
|
||||
) {
|
||||
return rpcRound("exposeForDependents", options) as ReturnType<
|
||||
return rpcRound("expose-for-dependents", options) as ReturnType<
|
||||
T.Effects["exposeForDependents"]
|
||||
>
|
||||
},
|
||||
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
|
||||
return rpcRound("getConfigured", {}) as ReturnType<
|
||||
return rpcRound("get-configured", {}) as ReturnType<
|
||||
T.Effects["getConfigured"]
|
||||
>
|
||||
},
|
||||
getContainerIp(...[]: Parameters<T.Effects["getContainerIp"]>) {
|
||||
return rpcRound("getContainerIp", {}) as ReturnType<
|
||||
return rpcRound("get-container-ip", {}) as ReturnType<
|
||||
T.Effects["getContainerIp"]
|
||||
>
|
||||
},
|
||||
@@ -174,21 +177,21 @@ function makeEffects(context: EffectContext): Effects {
|
||||
...allOptions,
|
||||
callback: context.callbacks?.addCallback(allOptions.callback) || null,
|
||||
}
|
||||
return rpcRound("getHostInfo", options) as ReturnType<
|
||||
return rpcRound("get-host-info", options) as ReturnType<
|
||||
T.Effects["getHostInfo"]
|
||||
> as any
|
||||
}) as Effects["getHostInfo"],
|
||||
getServiceInterface(
|
||||
...[options]: Parameters<T.Effects["getServiceInterface"]>
|
||||
) {
|
||||
return rpcRound("getServiceInterface", {
|
||||
return rpcRound("get-service-interface", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["getServiceInterface"]>
|
||||
},
|
||||
|
||||
getPrimaryUrl(...[options]: Parameters<T.Effects["getPrimaryUrl"]>) {
|
||||
return rpcRound("getPrimaryUrl", {
|
||||
return rpcRound("get-primary-url", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["getPrimaryUrl"]>
|
||||
@@ -196,22 +199,22 @@ function makeEffects(context: EffectContext): Effects {
|
||||
getServicePortForward(
|
||||
...[options]: Parameters<T.Effects["getServicePortForward"]>
|
||||
) {
|
||||
return rpcRound("getServicePortForward", options) as ReturnType<
|
||||
return rpcRound("get-service-port-forward", options) as ReturnType<
|
||||
T.Effects["getServicePortForward"]
|
||||
>
|
||||
},
|
||||
getSslCertificate(options: Parameters<T.Effects["getSslCertificate"]>[0]) {
|
||||
return rpcRound("getSslCertificate", options) as ReturnType<
|
||||
return rpcRound("get-ssl-certificate", options) as ReturnType<
|
||||
T.Effects["getSslCertificate"]
|
||||
>
|
||||
},
|
||||
getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) {
|
||||
return rpcRound("getSslKey", options) as ReturnType<
|
||||
return rpcRound("get-ssl-key", options) as ReturnType<
|
||||
T.Effects["getSslKey"]
|
||||
>
|
||||
},
|
||||
getSystemSmtp(...[options]: Parameters<T.Effects["getSystemSmtp"]>) {
|
||||
return rpcRound("getSystemSmtp", {
|
||||
return rpcRound("get-system-smtp", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["getSystemSmtp"]>
|
||||
@@ -219,7 +222,7 @@ function makeEffects(context: EffectContext): Effects {
|
||||
listServiceInterfaces(
|
||||
...[options]: Parameters<T.Effects["listServiceInterfaces"]>
|
||||
) {
|
||||
return rpcRound("listServiceInterfaces", {
|
||||
return rpcRound("list-service-interfaces", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["listServiceInterfaces"]>
|
||||
@@ -228,7 +231,7 @@ function makeEffects(context: EffectContext): Effects {
|
||||
return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
||||
},
|
||||
clearActions(...[]: Parameters<T.Effects["clearActions"]>) {
|
||||
return rpcRound("clearActions", {}) as ReturnType<
|
||||
return rpcRound("clear-actions", {}) as ReturnType<
|
||||
T.Effects["clearActions"]
|
||||
>
|
||||
},
|
||||
@@ -236,37 +239,39 @@ function makeEffects(context: EffectContext): Effects {
|
||||
return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
|
||||
},
|
||||
setConfigured(...[configured]: Parameters<T.Effects["setConfigured"]>) {
|
||||
return rpcRound("setConfigured", { configured }) as ReturnType<
|
||||
return rpcRound("set-configured", { configured }) as ReturnType<
|
||||
T.Effects["setConfigured"]
|
||||
>
|
||||
},
|
||||
setDependencies(
|
||||
dependencies: Parameters<T.Effects["setDependencies"]>[0],
|
||||
): ReturnType<T.Effects["setDependencies"]> {
|
||||
return rpcRound("setDependencies", dependencies) as ReturnType<
|
||||
return rpcRound("set-dependencies", dependencies) as ReturnType<
|
||||
T.Effects["setDependencies"]
|
||||
>
|
||||
},
|
||||
checkDependencies(
|
||||
options: Parameters<T.Effects["checkDependencies"]>[0],
|
||||
): ReturnType<T.Effects["checkDependencies"]> {
|
||||
return rpcRound("checkDependencies", options) as ReturnType<
|
||||
return rpcRound("check-dependencies", options) as ReturnType<
|
||||
T.Effects["checkDependencies"]
|
||||
>
|
||||
},
|
||||
getDependencies(): ReturnType<T.Effects["getDependencies"]> {
|
||||
return rpcRound("getDependencies", {}) as ReturnType<
|
||||
return rpcRound("get-dependencies", {}) as ReturnType<
|
||||
T.Effects["getDependencies"]
|
||||
>
|
||||
},
|
||||
setHealth(...[options]: Parameters<T.Effects["setHealth"]>) {
|
||||
return rpcRound("setHealth", options) as ReturnType<
|
||||
return rpcRound("set-health", options) as ReturnType<
|
||||
T.Effects["setHealth"]
|
||||
>
|
||||
},
|
||||
|
||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<void> {
|
||||
return rpcRound("setMainStatus", o) as ReturnType<T.Effects["setHealth"]>
|
||||
return rpcRound("set-main-status", o) as ReturnType<
|
||||
T.Effects["setHealth"]
|
||||
>
|
||||
},
|
||||
|
||||
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
|
||||
@@ -274,13 +279,23 @@ function makeEffects(context: EffectContext): Effects {
|
||||
},
|
||||
store: {
|
||||
get: async (options: any) =>
|
||||
rpcRound("getStore", {
|
||||
rpcRound("store.get", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as any,
|
||||
set: async (options: any) =>
|
||||
rpcRound("setStore", options) as ReturnType<T.Effects["store"]["set"]>,
|
||||
rpcRound("store.set", options) as ReturnType<T.Effects["store"]["set"]>,
|
||||
} as T.Effects["store"],
|
||||
getDataVersion() {
|
||||
return rpcRound("get-data-version", {}) as ReturnType<
|
||||
T.Effects["getDataVersion"]
|
||||
>
|
||||
},
|
||||
setDataVersion(...[options]: Parameters<T.Effects["setDataVersion"]>) {
|
||||
return rpcRound("set-data-version", options) as ReturnType<
|
||||
T.Effects["setDataVersion"]
|
||||
>
|
||||
},
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import * as fs from "fs"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { AllGetDependencies } from "../Interfaces/AllGetDependencies"
|
||||
import { jsonPath } from "../Models/JsonPath"
|
||||
import { jsonPath, unNestPath } from "../Models/JsonPath"
|
||||
import { RunningMain, System } from "../Interfaces/System"
|
||||
import {
|
||||
MakeMainEffects,
|
||||
@@ -52,6 +52,8 @@ const SOCKET_PARENT = "/media/startos/rpc"
|
||||
const SOCKET_PATH = "/media/startos/rpc/service.sock"
|
||||
const jsonrpc = "2.0" as const
|
||||
|
||||
const isResult = object({ result: any }).test
|
||||
|
||||
const idType = some(string, number, literal(null))
|
||||
type IdType = null | string | number
|
||||
const runType = object({
|
||||
@@ -64,7 +66,7 @@ const runType = object({
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout", "input"],
|
||||
["timeout"],
|
||||
),
|
||||
})
|
||||
const sandboxRunType = object({
|
||||
@@ -77,7 +79,7 @@ const sandboxRunType = object({
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout", "input"],
|
||||
["timeout"],
|
||||
),
|
||||
})
|
||||
const callbackType = object({
|
||||
@@ -226,27 +228,25 @@ export class RpcListener {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
const effects = this.getDependencies.makeProcedureEffects()(params.id)
|
||||
return handleRpc(
|
||||
id,
|
||||
system.execute(effects, {
|
||||
procedure,
|
||||
input: params.input,
|
||||
timeout: params.timeout,
|
||||
}),
|
||||
)
|
||||
const input = params.input
|
||||
const timeout = params.timeout
|
||||
const result = getResult(procedure, system, effects, timeout, input)
|
||||
|
||||
return handleRpc(id, result)
|
||||
})
|
||||
.when(sandboxRunType, async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
const effects = this.makeProcedureEffects(params.id)
|
||||
return handleRpc(
|
||||
id,
|
||||
system.sandbox(effects, {
|
||||
procedure,
|
||||
input: params.input,
|
||||
timeout: params.timeout,
|
||||
}),
|
||||
const result = getResult(
|
||||
procedure,
|
||||
system,
|
||||
effects,
|
||||
params.input,
|
||||
params.input,
|
||||
)
|
||||
|
||||
return handleRpc(id, result)
|
||||
})
|
||||
.when(callbackType, async ({ params: { callback, args } }) => {
|
||||
this.system.callCallback(callback, args)
|
||||
@@ -280,7 +280,7 @@ export class RpcListener {
|
||||
(async () => {
|
||||
if (!this._system) {
|
||||
const system = await this.getDependencies.system()
|
||||
await system.init()
|
||||
await system.containerInit()
|
||||
this._system = system
|
||||
}
|
||||
})().then((result) => ({ result })),
|
||||
@@ -342,3 +342,97 @@ export class RpcListener {
|
||||
})
|
||||
}
|
||||
}
|
||||
function getResult(
|
||||
procedure: typeof jsonPath._TYPE,
|
||||
system: System,
|
||||
effects: T.Effects,
|
||||
timeout: number | undefined,
|
||||
input: any,
|
||||
) {
|
||||
const ensureResultTypeShape = (
|
||||
result:
|
||||
| void
|
||||
| T.ConfigRes
|
||||
| T.PropertiesReturn
|
||||
| T.ActionMetadata[]
|
||||
| T.ActionResult,
|
||||
): { result: any } => {
|
||||
if (isResult(result)) return result
|
||||
return { result }
|
||||
}
|
||||
return (async () => {
|
||||
switch (procedure) {
|
||||
case "/backup/create":
|
||||
return system.createBackup(effects, timeout || null)
|
||||
case "/backup/restore":
|
||||
return system.restoreBackup(effects, timeout || null)
|
||||
case "/config/get":
|
||||
return system.getConfig(effects, timeout || null)
|
||||
case "/config/set":
|
||||
return system.setConfig(effects, input, timeout || null)
|
||||
case "/properties":
|
||||
return system.properties(effects, timeout || null)
|
||||
case "/actions/metadata":
|
||||
return system.actionsMetadata(effects)
|
||||
case "/init":
|
||||
return system.packageInit(
|
||||
effects,
|
||||
string.optional().unsafeCast(input),
|
||||
timeout || null,
|
||||
)
|
||||
case "/uninit":
|
||||
return system.packageUninit(
|
||||
effects,
|
||||
string.optional().unsafeCast(input),
|
||||
timeout || null,
|
||||
)
|
||||
default:
|
||||
const procedures = unNestPath(procedure)
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "get":
|
||||
return system.action(effects, procedures[2], input, timeout || null)
|
||||
case procedures[1] === "actions" && procedures[3] === "run":
|
||||
return system.action(effects, procedures[2], input, timeout || null)
|
||||
case procedures[1] === "dependencies" && procedures[3] === "query":
|
||||
return system.dependenciesAutoconfig(
|
||||
effects,
|
||||
procedures[2],
|
||||
input,
|
||||
timeout || null,
|
||||
)
|
||||
|
||||
case procedures[1] === "dependencies" && procedures[3] === "update":
|
||||
return system.dependenciesAutoconfig(
|
||||
effects,
|
||||
procedures[2],
|
||||
input,
|
||||
timeout || null,
|
||||
)
|
||||
}
|
||||
}
|
||||
})().then(ensureResultTypeShape, (error) =>
|
||||
matches(error)
|
||||
.when(
|
||||
object(
|
||||
{
|
||||
error: string,
|
||||
code: number,
|
||||
},
|
||||
["code"],
|
||||
{ code: 0 },
|
||||
),
|
||||
(error) => ({
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.error,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.defaultToLazy(() => ({
|
||||
error: {
|
||||
code: 0,
|
||||
message: String(error),
|
||||
},
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,40 +1,60 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as cp from "child_process"
|
||||
import { Overlay, types as T } from "@start9labs/start-sdk"
|
||||
import { SubContainer, types as T } from "@start9labs/start-sdk"
|
||||
import { promisify } from "util"
|
||||
import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure"
|
||||
import { Volume } from "./matchVolume"
|
||||
import {
|
||||
CommandOptions,
|
||||
ExecOptions,
|
||||
ExecSpawnable,
|
||||
} from "@start9labs/start-sdk/cjs/lib/util/SubContainer"
|
||||
export const exec = promisify(cp.exec)
|
||||
export const execFile = promisify(cp.execFile)
|
||||
|
||||
export class DockerProcedureContainer {
|
||||
private constructor(readonly overlay: Overlay) {}
|
||||
// static async readonlyOf(data: DockerProcedure) {
|
||||
// return DockerProcedureContainer.of(data, ["-o", "ro"])
|
||||
// }
|
||||
private constructor(private readonly subcontainer: ExecSpawnable) {}
|
||||
|
||||
static async of(
|
||||
effects: T.Effects,
|
||||
packageId: string,
|
||||
data: DockerProcedure,
|
||||
volumes: { [id: VolumeId]: Volume },
|
||||
options: { subcontainer?: ExecSpawnable } = {},
|
||||
) {
|
||||
const overlay = await Overlay.of(effects, { id: data.image })
|
||||
const subcontainer =
|
||||
options?.subcontainer ??
|
||||
(await DockerProcedureContainer.createSubContainer(
|
||||
effects,
|
||||
packageId,
|
||||
data,
|
||||
volumes,
|
||||
))
|
||||
return new DockerProcedureContainer(subcontainer)
|
||||
}
|
||||
static async createSubContainer(
|
||||
effects: T.Effects,
|
||||
packageId: string,
|
||||
data: DockerProcedure,
|
||||
volumes: { [id: VolumeId]: Volume },
|
||||
) {
|
||||
const subcontainer = await SubContainer.of(effects, { id: data.image })
|
||||
|
||||
if (data.mounts) {
|
||||
const mounts = data.mounts
|
||||
for (const mount in mounts) {
|
||||
const path = mounts[mount].startsWith("/")
|
||||
? `${overlay.rootfs}${mounts[mount]}`
|
||||
: `${overlay.rootfs}/${mounts[mount]}`
|
||||
? `${subcontainer.rootfs}${mounts[mount]}`
|
||||
: `${subcontainer.rootfs}/${mounts[mount]}`
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
const volumeMount = volumes[mount]
|
||||
if (volumeMount.type === "data") {
|
||||
await overlay.mount(
|
||||
await subcontainer.mount(
|
||||
{ type: "volume", id: mount, subpath: null, readonly: false },
|
||||
mounts[mount],
|
||||
)
|
||||
} else if (volumeMount.type === "assets") {
|
||||
await overlay.mount(
|
||||
await subcontainer.mount(
|
||||
{ type: "assets", id: mount, subpath: null },
|
||||
mounts[mount],
|
||||
)
|
||||
@@ -80,25 +100,35 @@ export class DockerProcedureContainer {
|
||||
})
|
||||
.catch(console.warn)
|
||||
} else if (volumeMount.type === "backup") {
|
||||
await overlay.mount({ type: "backup", subpath: null }, mounts[mount])
|
||||
await subcontainer.mount(
|
||||
{ type: "backup", subpath: null },
|
||||
mounts[mount],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DockerProcedureContainer(overlay)
|
||||
return subcontainer
|
||||
}
|
||||
|
||||
async exec(commands: string[]) {
|
||||
async exec(
|
||||
commands: string[],
|
||||
options?: CommandOptions & ExecOptions,
|
||||
timeoutMs?: number | null,
|
||||
) {
|
||||
try {
|
||||
return await this.overlay.exec(commands)
|
||||
return await this.subcontainer.exec(commands, options, timeoutMs)
|
||||
} finally {
|
||||
await this.overlay.destroy()
|
||||
await this.subcontainer.destroy?.()
|
||||
}
|
||||
}
|
||||
|
||||
async execFail(commands: string[], timeoutMs: number | null) {
|
||||
async execFail(
|
||||
commands: string[],
|
||||
timeoutMs: number | null,
|
||||
options?: CommandOptions & ExecOptions,
|
||||
) {
|
||||
try {
|
||||
const res = await this.overlay.exec(commands, {}, timeoutMs)
|
||||
const res = await this.subcontainer.exec(commands, options, timeoutMs)
|
||||
if (res.exitCode !== 0) {
|
||||
const codeOrSignal =
|
||||
res.exitCode !== null
|
||||
@@ -110,11 +140,11 @@ export class DockerProcedureContainer {
|
||||
}
|
||||
return res
|
||||
} finally {
|
||||
await this.overlay.destroy()
|
||||
await this.subcontainer.destroy?.()
|
||||
}
|
||||
}
|
||||
|
||||
async spawn(commands: string[]): Promise<cp.ChildProcessWithoutNullStreams> {
|
||||
return await this.overlay.spawn(commands)
|
||||
return await this.subcontainer.spawn(commands)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { T, utils } from "@start9labs/start-sdk"
|
||||
import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon"
|
||||
import { Effects } from "../../../Models/Effects"
|
||||
import { off } from "node:process"
|
||||
import { CommandController } from "@start9labs/start-sdk/cjs/lib/mainFn/CommandController"
|
||||
import { asError } from "@start9labs/start-sdk/cjs/lib/util"
|
||||
|
||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||
@@ -14,6 +16,9 @@ const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||
* Also, this has an ability to clean itself up too if need be.
|
||||
*/
|
||||
export class MainLoop {
|
||||
get mainSubContainerHandle() {
|
||||
return this.mainEvent?.daemon?.subContainerHandle
|
||||
}
|
||||
private healthLoops?: {
|
||||
name: string
|
||||
interval: NodeJS.Timeout
|
||||
@@ -48,26 +53,32 @@ export class MainLoop {
|
||||
await this.setupInterfaces(effects)
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
const jsMain = (this.system.moduleCode as any)?.jsMain
|
||||
const dockerProcedureContainer = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
this.system.manifest.id,
|
||||
this.system.manifest.main,
|
||||
this.system.manifest.volumes,
|
||||
)
|
||||
if (jsMain) {
|
||||
throw new Error("Unreachable")
|
||||
}
|
||||
const daemon = await Daemon.of()(
|
||||
this.effects,
|
||||
{ id: this.system.manifest.main.image },
|
||||
currentCommand,
|
||||
{
|
||||
overlay: dockerProcedureContainer.overlay,
|
||||
sigtermTimeout: utils.inMs(
|
||||
this.system.manifest.main["sigterm-timeout"],
|
||||
),
|
||||
},
|
||||
)
|
||||
const daemon = new Daemon(async () => {
|
||||
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
||||
effects,
|
||||
this.system.manifest.id,
|
||||
this.system.manifest.main,
|
||||
this.system.manifest.volumes,
|
||||
)
|
||||
return CommandController.of()(
|
||||
this.effects,
|
||||
subcontainer,
|
||||
currentCommand,
|
||||
{
|
||||
runAsInit: true,
|
||||
env: {
|
||||
TINI_SUBREAPER: "true",
|
||||
},
|
||||
sigtermTimeout: utils.inMs(
|
||||
this.system.manifest.main["sigterm-timeout"],
|
||||
),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
daemon.start()
|
||||
return {
|
||||
daemon,
|
||||
@@ -123,7 +134,9 @@ export class MainLoop {
|
||||
const main = await mainEvent
|
||||
delete this.mainEvent
|
||||
delete this.healthLoops
|
||||
await main?.daemon.stop().catch((e) => console.error(e))
|
||||
await main?.daemon
|
||||
.stop()
|
||||
.catch((e) => console.error(`Main loop error`, utils.asError(e)))
|
||||
this.effects.setMainStatus({ status: "stopped" })
|
||||
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
||||
}
|
||||
@@ -134,27 +147,42 @@ export class MainLoop {
|
||||
const start = Date.now()
|
||||
return Object.entries(manifest["health-checks"]).map(
|
||||
([healthId, value]) => {
|
||||
effects
|
||||
.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "starting",
|
||||
message: null,
|
||||
})
|
||||
.catch((e) => console.error(asError(e)))
|
||||
const interval = setInterval(async () => {
|
||||
const actionProcedure = value
|
||||
const timeChanged = Date.now() - start
|
||||
if (actionProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
manifest.id,
|
||||
actionProcedure,
|
||||
manifest.volumes,
|
||||
const subcontainer = actionProcedure.inject
|
||||
? this.mainSubContainerHandle
|
||||
: undefined
|
||||
// prettier-ignore
|
||||
const container =
|
||||
await DockerProcedureContainer.of(
|
||||
effects,
|
||||
manifest.id,
|
||||
actionProcedure,
|
||||
manifest.volumes,
|
||||
{
|
||||
subcontainer,
|
||||
}
|
||||
)
|
||||
const executed = await container.exec(
|
||||
[actionProcedure.entrypoint, ...actionProcedure.args],
|
||||
{ input: JSON.stringify(timeChanged) },
|
||||
)
|
||||
const executed = await container.exec([
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(timeChanged),
|
||||
])
|
||||
if (executed.exitCode === 0) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "success",
|
||||
message: actionProcedure["success-message"],
|
||||
message: actionProcedure["success-message"] ?? null,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
export default {
|
||||
"eos-version": "0.3.5.1",
|
||||
id: "gitea",
|
||||
"git-hash": "91fada3edf30357a2e75c281d32f8888c87fcc2d\n",
|
||||
title: "Gitea",
|
||||
version: "1.22.0",
|
||||
description: {
|
||||
short: "A painless self-hosted Git service.",
|
||||
long: "Gitea is a community managed lightweight code hosting solution written in Go. It is published under the MIT license.\n",
|
||||
},
|
||||
assets: {
|
||||
license: "LICENSE",
|
||||
instructions: "instructions.md",
|
||||
icon: "icon.png",
|
||||
"docker-images": null,
|
||||
assets: null,
|
||||
scripts: null,
|
||||
},
|
||||
build: ["make"],
|
||||
"release-notes":
|
||||
"* Upstream code update\n* Fix deprecated config options\n* Full list of upstream changes available [here](https://github.com/go-gitea/gitea/compare/v1.21.8...v1.22.0)\n",
|
||||
license: "MIT",
|
||||
"wrapper-repo": "https://github.com/Start9Labs/gitea-startos",
|
||||
"upstream-repo": "https://github.com/go-gitea/gitea",
|
||||
"support-site": "https://docs.gitea.io/en-us/",
|
||||
"marketing-site": "https://gitea.io/en-us/",
|
||||
"donation-url": null,
|
||||
alerts: {
|
||||
install: null,
|
||||
uninstall: null,
|
||||
restore: null,
|
||||
start: null,
|
||||
stop: null,
|
||||
},
|
||||
main: {
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "/usr/local/bin/docker_entrypoint.sh",
|
||||
args: [],
|
||||
inject: false,
|
||||
mounts: { main: "/data" },
|
||||
"io-format": null,
|
||||
"sigterm-timeout": null,
|
||||
"shm-size-mb": null,
|
||||
"gpu-acceleration": false,
|
||||
},
|
||||
"health-checks": {
|
||||
"user-signups-off": {
|
||||
name: "User Signups Off",
|
||||
"success-message": null,
|
||||
type: "script",
|
||||
args: [],
|
||||
timeout: null,
|
||||
},
|
||||
web: {
|
||||
name: "Web & Git HTTP Tor Interfaces",
|
||||
"success-message":
|
||||
"Gitea is ready to be visited in a web browser and git can be used with SSH over TOR.",
|
||||
type: "script",
|
||||
args: [],
|
||||
timeout: null,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
get: { type: "script", args: [] },
|
||||
set: { type: "script", args: [] },
|
||||
},
|
||||
properties: { type: "script", args: [] },
|
||||
volumes: { main: { type: "data" } },
|
||||
interfaces: {
|
||||
main: {
|
||||
name: "Web UI / Git HTTPS/SSH",
|
||||
description:
|
||||
"Port 80: Browser Interface and HTTP Git Interface / Port 22: Git SSH Interface",
|
||||
"tor-config": { "port-mapping": { "22": "22", "80": "3000" } },
|
||||
"lan-config": { "443": { ssl: true, internal: 3000 } },
|
||||
ui: true,
|
||||
protocols: ["tcp", "http", "ssh", "git"],
|
||||
},
|
||||
},
|
||||
backup: {
|
||||
create: {
|
||||
type: "docker",
|
||||
image: "compat",
|
||||
system: true,
|
||||
entrypoint: "compat",
|
||||
args: ["duplicity", "create", "/mnt/backup", "/root/data"],
|
||||
inject: false,
|
||||
mounts: { BACKUP: "/mnt/backup", main: "/root/data" },
|
||||
"io-format": "yaml",
|
||||
"sigterm-timeout": null,
|
||||
"shm-size-mb": null,
|
||||
"gpu-acceleration": false,
|
||||
},
|
||||
restore: {
|
||||
type: "docker",
|
||||
image: "compat",
|
||||
system: true,
|
||||
entrypoint: "compat",
|
||||
args: ["duplicity", "restore", "/mnt/backup", "/root/data"],
|
||||
inject: false,
|
||||
mounts: { BACKUP: "/mnt/backup", main: "/root/data" },
|
||||
"io-format": "yaml",
|
||||
"sigterm-timeout": null,
|
||||
"shm-size-mb": null,
|
||||
"gpu-acceleration": false,
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
from: { "*": { type: "script", args: ["from"] } },
|
||||
to: { "*": { type: "script", args: ["to"] } },
|
||||
},
|
||||
actions: {},
|
||||
dependencies: {},
|
||||
containers: null,
|
||||
replaces: [],
|
||||
"hardware-requirements": {
|
||||
device: {},
|
||||
ram: null,
|
||||
arch: ["x86_64", "aarch64"],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
export default {
|
||||
"tor-address": {
|
||||
name: "Tor Address",
|
||||
description: "The Tor address for the websocket server.",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "nostr",
|
||||
target: "tor-address",
|
||||
interface: "websocket",
|
||||
},
|
||||
"lan-address": {
|
||||
name: "Tor Address",
|
||||
description: "The LAN address for the websocket server.",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "nostr",
|
||||
target: "lan-address",
|
||||
interface: "websocket",
|
||||
},
|
||||
"relay-type": {
|
||||
type: "union",
|
||||
name: "Relay Type",
|
||||
warning:
|
||||
"Running a public relay carries risk. Your relay can be spammed, resulting in large amounts of disk usage.",
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Relay Type",
|
||||
description:
|
||||
"Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.",
|
||||
"variant-names": { private: "Private", public: "Public" },
|
||||
},
|
||||
default: "private",
|
||||
variants: {
|
||||
private: {
|
||||
pubkey_whitelist: {
|
||||
name: "Pubkey Whitelist (hex)",
|
||||
description:
|
||||
"A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.",
|
||||
type: "list",
|
||||
range: "[1,*)",
|
||||
subtype: "string",
|
||||
spec: {
|
||||
placeholder: "hex (not npub) pubkey",
|
||||
pattern: "[0-9a-fA-F]{64}",
|
||||
"pattern-description":
|
||||
"Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
public: {
|
||||
info: {
|
||||
name: "Relay Info",
|
||||
description: "General public info about your relay",
|
||||
type: "object",
|
||||
spec: {
|
||||
name: {
|
||||
name: "Relay Name",
|
||||
description: "Your relay's human-readable identifier",
|
||||
type: "string",
|
||||
nullable: true,
|
||||
placeholder: "Bob's Public Relay",
|
||||
pattern: ".{3,32}",
|
||||
"pattern-description":
|
||||
"Must be at least 3 character and no more than 32 characters",
|
||||
masked: false,
|
||||
},
|
||||
description: {
|
||||
name: "Relay Description",
|
||||
description: "A more detailed description for your relay",
|
||||
type: "string",
|
||||
nullable: true,
|
||||
placeholder: "The best relay in town",
|
||||
pattern: ".{6,256}",
|
||||
"pattern-description":
|
||||
"Must be at least 6 character and no more than 256 characters",
|
||||
masked: false,
|
||||
},
|
||||
pubkey: {
|
||||
name: "Admin contact pubkey (hex)",
|
||||
description:
|
||||
"The Nostr hex (not npub) pubkey of the relay administrator",
|
||||
type: "string",
|
||||
nullable: true,
|
||||
placeholder: "hex (not npub) pubkey",
|
||||
pattern: "[0-9a-fA-F]{64}",
|
||||
"pattern-description":
|
||||
"Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||
masked: false,
|
||||
},
|
||||
contact: {
|
||||
name: "Admin contact email",
|
||||
description: "The email address of the relay administrator",
|
||||
type: "string",
|
||||
nullable: true,
|
||||
pattern: "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+",
|
||||
"pattern-description": "Must be a valid email address.",
|
||||
masked: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
name: "Limits",
|
||||
description:
|
||||
"Data limits to protect your relay from using too many resources",
|
||||
type: "object",
|
||||
spec: {
|
||||
messages_per_sec: {
|
||||
name: "Messages Per Second Limit",
|
||||
description:
|
||||
"Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[1,*)",
|
||||
integral: true,
|
||||
default: 2,
|
||||
units: "messages/sec",
|
||||
},
|
||||
subscriptions_per_min: {
|
||||
name: "Subscriptions Per Minute Limit",
|
||||
description:
|
||||
"Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[1,*)",
|
||||
integral: true,
|
||||
default: 10,
|
||||
units: "subscriptions",
|
||||
},
|
||||
max_blocking_threads: {
|
||||
name: "Max Blocking Threads",
|
||||
description:
|
||||
"Maximum number of blocking threads used for database connections.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[0,*)",
|
||||
integral: true,
|
||||
units: "threads",
|
||||
default: 16,
|
||||
},
|
||||
max_event_bytes: {
|
||||
name: "Max Event Size",
|
||||
description:
|
||||
"Limit the maximum size of an EVENT message. Set to 0 for unlimited",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[0,*)",
|
||||
integral: true,
|
||||
units: "bytes",
|
||||
default: 131072,
|
||||
},
|
||||
max_ws_message_bytes: {
|
||||
name: "Max Websocket Message Size",
|
||||
description: "Maximum WebSocket message in bytes.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[0,*)",
|
||||
integral: true,
|
||||
units: "bytes",
|
||||
default: 131072,
|
||||
},
|
||||
max_ws_frame_bytes: {
|
||||
name: "Max Websocket Frame Size",
|
||||
description: "Maximum WebSocket frame size in bytes.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[0,*)",
|
||||
integral: true,
|
||||
units: "bytes",
|
||||
default: 131072,
|
||||
},
|
||||
event_kind_blacklist: {
|
||||
name: "Event Kind Blacklist",
|
||||
description:
|
||||
"Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds",
|
||||
type: "list",
|
||||
range: "[0,*)",
|
||||
subtype: "number",
|
||||
spec: { integral: true, placeholder: 30023, range: "(0,100000]" },
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
export default {
|
||||
id: "synapse",
|
||||
title: "Synapse",
|
||||
version: "1.98.0",
|
||||
"release-notes":
|
||||
"* Upstream code update\n* Synapse Admin updated to the latest version - ([full changelog](https://github.com/Awesome-Technologies/synapse-admin/compare/0.8.7...0.9.1))\n* Instructions update\n* Updated package and upstream repositories links\n* Full list of upstream changes available [here](https://github.com/element-hq/synapse/compare/v1.95.1...v1.98.0)\n",
|
||||
license: "Apache-2.0",
|
||||
"wrapper-repo": "https://github.com/Start9Labs/synapse-startos",
|
||||
"upstream-repo": "https://github.com/element-hq/synapse",
|
||||
"support-site": "https://github.com/element-hq/synapse/issues",
|
||||
"marketing-site": "https://matrix.org/",
|
||||
build: ["make"],
|
||||
description: {
|
||||
short:
|
||||
"Synapse is a battle-tested implementation of the Matrix protocol, the killer of all messaging apps.",
|
||||
long: "Synapse is the battle-tested, reference implementation of the Matrix protocol. Matrix is a next-generation, federated, full-featured, encrypted, independent messaging system. There are no trusted third parties involved. (see matrix.org for details).",
|
||||
},
|
||||
assets: {
|
||||
license: "LICENSE",
|
||||
icon: "icon.png",
|
||||
instructions: "instructions.md",
|
||||
},
|
||||
main: {
|
||||
type: "docker",
|
||||
image: "main",
|
||||
entrypoint: "docker_entrypoint.sh",
|
||||
args: [],
|
||||
mounts: {
|
||||
main: "/data",
|
||||
cert: "/mnt/cert",
|
||||
"admin-cert": "/mnt/admin-cert",
|
||||
},
|
||||
},
|
||||
"health-checks": {
|
||||
federation: {
|
||||
name: "Federation",
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "check-federation.sh",
|
||||
args: [],
|
||||
mounts: {},
|
||||
"io-format": "json",
|
||||
inject: true,
|
||||
},
|
||||
"synapse-admin": {
|
||||
name: "Admin interface",
|
||||
"success-message":
|
||||
"Synapse Admin is ready to be visited in a web browser.",
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "check-ui.sh",
|
||||
args: [],
|
||||
mounts: {},
|
||||
"io-format": "yaml",
|
||||
inject: true,
|
||||
},
|
||||
"user-signups-off": {
|
||||
name: "User Signups Off",
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "user-signups-off.sh",
|
||||
args: [],
|
||||
mounts: {},
|
||||
"io-format": "yaml",
|
||||
inject: true,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
get: {
|
||||
type: "script",
|
||||
},
|
||||
set: {
|
||||
type: "script",
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
type: "script",
|
||||
},
|
||||
volumes: {
|
||||
main: {
|
||||
type: "data",
|
||||
},
|
||||
cert: {
|
||||
type: "certificate",
|
||||
"interface-id": "main",
|
||||
},
|
||||
"admin-cert": {
|
||||
type: "certificate",
|
||||
"interface-id": "admin",
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
start:
|
||||
"After your first run, Synapse needs a little time to establish a stable TOR connection over federation. We kindly ask for your patience during this process. Remember, great things take time! 🕒",
|
||||
},
|
||||
interfaces: {
|
||||
main: {
|
||||
name: "Homeserver Address",
|
||||
description:
|
||||
"Used by clients and other servers to connect with your homeserver",
|
||||
"tor-config": {
|
||||
"port-mapping": {
|
||||
"80": "80",
|
||||
"443": "443",
|
||||
"8448": "8448",
|
||||
},
|
||||
},
|
||||
ui: false,
|
||||
protocols: ["tcp", "http", "matrix"],
|
||||
},
|
||||
admin: {
|
||||
name: "Admin Portal",
|
||||
description: "A web application for administering your Synapse server",
|
||||
"tor-config": {
|
||||
"port-mapping": {
|
||||
"80": "8080",
|
||||
"443": "4433",
|
||||
},
|
||||
},
|
||||
"lan-config": {
|
||||
"443": {
|
||||
ssl: true,
|
||||
internal: 8080,
|
||||
},
|
||||
},
|
||||
ui: true,
|
||||
protocols: ["tcp", "http"],
|
||||
},
|
||||
},
|
||||
dependencies: {},
|
||||
backup: {
|
||||
create: {
|
||||
type: "docker",
|
||||
image: "compat",
|
||||
system: true,
|
||||
entrypoint: "compat",
|
||||
args: ["duplicity", "create", "/mnt/backup", "/data"],
|
||||
mounts: {
|
||||
BACKUP: "/mnt/backup",
|
||||
main: "/data",
|
||||
},
|
||||
},
|
||||
restore: {
|
||||
type: "docker",
|
||||
image: "compat",
|
||||
system: true,
|
||||
entrypoint: "compat",
|
||||
args: ["duplicity", "restore", "/mnt/backup", "/data"],
|
||||
mounts: {
|
||||
BACKUP: "/mnt/backup",
|
||||
main: "/data",
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
"reset-first-user": {
|
||||
name: "Reset First User",
|
||||
description:
|
||||
"This action will reset the password of the first user in your database to a random value.",
|
||||
"allowed-statuses": ["stopped"],
|
||||
implementation: {
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "docker_entrypoint.sh",
|
||||
args: ["reset-first-user"],
|
||||
mounts: {
|
||||
main: "/data",
|
||||
},
|
||||
"io-format": "json",
|
||||
},
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
from: {
|
||||
"*": {
|
||||
type: "script",
|
||||
args: ["from"],
|
||||
},
|
||||
},
|
||||
to: {
|
||||
"*": {
|
||||
type: "script",
|
||||
args: ["to"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -750,6 +750,283 @@ exports[`transformConfigSpec transformConfigSpec(nostr) 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transformConfigSpec transformConfigSpec(nostr2) 1`] = `
|
||||
{
|
||||
"relay-type": {
|
||||
"default": "private",
|
||||
"description": "Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Relay Type",
|
||||
"required": true,
|
||||
"type": "union",
|
||||
"variants": {
|
||||
"private": {
|
||||
"name": "Private",
|
||||
"spec": {
|
||||
"pubkey_whitelist": {
|
||||
"default": [],
|
||||
"description": "A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.",
|
||||
"disabled": false,
|
||||
"maxLength": null,
|
||||
"minLength": 1,
|
||||
"name": "Pubkey Whitelist (hex)",
|
||||
"spec": {
|
||||
"generate": null,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||
"regex": "[0-9a-fA-F]{64}",
|
||||
},
|
||||
],
|
||||
"placeholder": "hex (not npub) pubkey",
|
||||
"type": "text",
|
||||
},
|
||||
"type": "list",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
},
|
||||
"public": {
|
||||
"name": "Public",
|
||||
"spec": {
|
||||
"info": {
|
||||
"description": "General public info about your relay",
|
||||
"name": "Relay Info",
|
||||
"spec": {
|
||||
"contact": {
|
||||
"default": null,
|
||||
"description": "The email address of the relay administrator",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Admin contact email",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be a valid email address.",
|
||||
"regex": "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+",
|
||||
},
|
||||
],
|
||||
"placeholder": null,
|
||||
"required": false,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
"description": {
|
||||
"default": null,
|
||||
"description": "A more detailed description for your relay",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Relay Description",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be at least 6 character and no more than 256 characters",
|
||||
"regex": ".{6,256}",
|
||||
},
|
||||
],
|
||||
"placeholder": "The best relay in town",
|
||||
"required": false,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
"name": {
|
||||
"default": null,
|
||||
"description": "Your relay's human-readable identifier",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Relay Name",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be at least 3 character and no more than 32 characters",
|
||||
"regex": ".{3,32}",
|
||||
},
|
||||
],
|
||||
"placeholder": "Bob's Public Relay",
|
||||
"required": false,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
"pubkey": {
|
||||
"default": null,
|
||||
"description": "The Nostr hex (not npub) pubkey of the relay administrator",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Admin contact pubkey (hex)",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||
"regex": "[0-9a-fA-F]{64}",
|
||||
},
|
||||
],
|
||||
"placeholder": "hex (not npub) pubkey",
|
||||
"required": false,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
"limits": {
|
||||
"description": "Data limits to protect your relay from using too many resources",
|
||||
"name": "Limits",
|
||||
"spec": {
|
||||
"event_kind_blacklist": {
|
||||
"default": [],
|
||||
"description": "Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds",
|
||||
"disabled": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Event Kind Blacklist",
|
||||
"spec": {
|
||||
"generate": null,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Integral number type",
|
||||
"regex": "[0-9]+",
|
||||
},
|
||||
],
|
||||
"placeholder": "30023",
|
||||
"type": "text",
|
||||
},
|
||||
"type": "list",
|
||||
"warning": null,
|
||||
},
|
||||
"max_blocking_threads": {
|
||||
"default": 16,
|
||||
"description": "Maximum number of blocking threads used for database connections.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "Max Blocking Threads",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "threads",
|
||||
"warning": null,
|
||||
},
|
||||
"max_event_bytes": {
|
||||
"default": 131072,
|
||||
"description": "Limit the maximum size of an EVENT message. Set to 0 for unlimited",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "Max Event Size",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "bytes",
|
||||
"warning": null,
|
||||
},
|
||||
"max_ws_frame_bytes": {
|
||||
"default": 131072,
|
||||
"description": "Maximum WebSocket frame size in bytes.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "Max Websocket Frame Size",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "bytes",
|
||||
"warning": null,
|
||||
},
|
||||
"max_ws_message_bytes": {
|
||||
"default": 131072,
|
||||
"description": "Maximum WebSocket message in bytes.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "Max Websocket Message Size",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "bytes",
|
||||
"warning": null,
|
||||
},
|
||||
"messages_per_sec": {
|
||||
"default": 2,
|
||||
"description": "Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": null,
|
||||
"min": 1,
|
||||
"name": "Messages Per Second Limit",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "messages/sec",
|
||||
"warning": null,
|
||||
},
|
||||
"subscriptions_per_min": {
|
||||
"default": 10,
|
||||
"description": "Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": null,
|
||||
"min": 1,
|
||||
"name": "Subscriptions Per Minute Limit",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "subscriptions",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"warning": null,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transformConfigSpec transformConfigSpec(searNXG) 1`] = `
|
||||
{
|
||||
"enable-metrics": {
|
||||
|
||||
@@ -61,6 +61,42 @@ const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
|
||||
export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
|
||||
const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as StorePath
|
||||
|
||||
const matchResult = object({
|
||||
result: any,
|
||||
})
|
||||
const matchError = object({
|
||||
error: string,
|
||||
})
|
||||
const matchErrorCode = object<{
|
||||
"error-code": [number, string] | readonly [number, string]
|
||||
}>({
|
||||
"error-code": tuple(number, string),
|
||||
})
|
||||
|
||||
const assertNever = (
|
||||
x: never,
|
||||
message = "Not expecting to get here: ",
|
||||
): never => {
|
||||
throw new Error(message + JSON.stringify(x))
|
||||
}
|
||||
/**
|
||||
Should be changing the type for specific properties, and this is mostly a transformation for the old return types to the newer one.
|
||||
*/
|
||||
const fromReturnType = <A>(a: U.ResultType<A>): A => {
|
||||
if (matchResult.test(a)) {
|
||||
return a.result
|
||||
}
|
||||
if (matchError.test(a)) {
|
||||
console.info({ passedErrorStack: new Error().stack, error: a.error })
|
||||
throw { error: a.error }
|
||||
}
|
||||
if (matchErrorCode.test(a)) {
|
||||
const [code, message] = a["error-code"]
|
||||
throw { error: message, code }
|
||||
}
|
||||
return assertNever(a)
|
||||
}
|
||||
|
||||
const matchSetResult = object(
|
||||
{
|
||||
"depends-on": dictionary([string, array(string)]),
|
||||
@@ -194,7 +230,7 @@ export class SystemForEmbassy implements System {
|
||||
const moduleCode = await import(EMBASSY_JS_LOCATION)
|
||||
.catch((_) => require(EMBASSY_JS_LOCATION))
|
||||
.catch(async (_) => {
|
||||
console.error("Could not load the js")
|
||||
console.error(utils.asError("Could not load the js"))
|
||||
console.error({
|
||||
exists: await fs.stat(EMBASSY_JS_LOCATION),
|
||||
})
|
||||
@@ -206,12 +242,49 @@ export class SystemForEmbassy implements System {
|
||||
moduleCode,
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly manifest: Manifest,
|
||||
readonly moduleCode: Partial<U.ExpectedExports>,
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {}
|
||||
async actionsMetadata(effects: T.Effects): Promise<T.ActionMetadata[]> {
|
||||
const actions = Object.entries(this.manifest.actions ?? {})
|
||||
return Promise.all(
|
||||
actions.map(async ([actionId, action]): Promise<T.ActionMetadata> => {
|
||||
const name = action.name ?? actionId
|
||||
const description = action.description
|
||||
const warning = action.warning ?? null
|
||||
const disabled = false
|
||||
const input = (await convertToNewConfig(action["input-spec"] as any))
|
||||
.spec
|
||||
const hasRunning = !!action["allowed-statuses"].find(
|
||||
(x) => x === "running",
|
||||
)
|
||||
const hasStopped = !!action["allowed-statuses"].find(
|
||||
(x) => x === "stopped",
|
||||
)
|
||||
// prettier-ignore
|
||||
const allowedStatuses =
|
||||
hasRunning && hasStopped ? "any":
|
||||
hasRunning ? "onlyRunning" :
|
||||
"onlyStopped"
|
||||
|
||||
const group = null
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
warning,
|
||||
disabled,
|
||||
allowedStatuses,
|
||||
group,
|
||||
input,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async containerInit(): Promise<void> {}
|
||||
|
||||
async exit(): Promise<void> {
|
||||
if (this.currentRunning) await this.currentRunning.clean()
|
||||
@@ -235,141 +308,7 @@ export class SystemForEmbassy implements System {
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure: JsonPath
|
||||
input?: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<RpcResult> {
|
||||
return this._execute(effects, options)
|
||||
.then((x) =>
|
||||
matches(x)
|
||||
.when(
|
||||
object({
|
||||
result: any,
|
||||
}),
|
||||
(x) => x,
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
error: string,
|
||||
}),
|
||||
(x) => ({
|
||||
error: {
|
||||
code: 0,
|
||||
message: x.error,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
"error-code": tuple(number, string),
|
||||
}),
|
||||
({ "error-code": [code, message] }) => ({
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.defaultTo({ result: x }),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
if (error instanceof Error)
|
||||
return {
|
||||
error: {
|
||||
code: 0,
|
||||
message: error.name,
|
||||
data: {
|
||||
details: error.message,
|
||||
debug: `${error?.cause ?? "[noCause]"}:${error?.stack ?? "[noStack]"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
if (matchRpcResult.test(error)) return error
|
||||
return {
|
||||
error: {
|
||||
code: 0,
|
||||
message: String(error),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
async _execute(
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure: JsonPath
|
||||
input?: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<unknown> {
|
||||
const input = options.input
|
||||
switch (options.procedure) {
|
||||
case "/backup/create":
|
||||
return this.createBackup(effects, options.timeout || null)
|
||||
case "/backup/restore":
|
||||
return this.restoreBackup(effects, options.timeout || null)
|
||||
case "/config/get":
|
||||
return this.getConfig(effects, options.timeout || null)
|
||||
case "/config/set":
|
||||
return this.setConfig(effects, input, options.timeout || null)
|
||||
case "/properties":
|
||||
return this.properties(effects, options.timeout || null)
|
||||
case "/actions/metadata":
|
||||
return todo()
|
||||
case "/init":
|
||||
return this.initProcedure(
|
||||
effects,
|
||||
string.optional().unsafeCast(input),
|
||||
options.timeout || null,
|
||||
)
|
||||
case "/uninit":
|
||||
return this.uninit(
|
||||
effects,
|
||||
string.optional().unsafeCast(input),
|
||||
options.timeout || null,
|
||||
)
|
||||
default:
|
||||
const procedures = unNestPath(options.procedure)
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "get":
|
||||
return this.action(
|
||||
effects,
|
||||
procedures[2],
|
||||
input,
|
||||
options.timeout || null,
|
||||
)
|
||||
case procedures[1] === "actions" && procedures[3] === "run":
|
||||
return this.action(
|
||||
effects,
|
||||
procedures[2],
|
||||
input,
|
||||
options.timeout || null,
|
||||
)
|
||||
case procedures[1] === "dependencies" && procedures[3] === "query":
|
||||
return null
|
||||
|
||||
case procedures[1] === "dependencies" && procedures[3] === "update":
|
||||
return this.dependenciesAutoconfig(
|
||||
effects,
|
||||
procedures[2],
|
||||
input,
|
||||
options.timeout || null,
|
||||
)
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find the path for ${options.procedure}`)
|
||||
}
|
||||
async sandbox(
|
||||
effects: Effects,
|
||||
options: { procedure: Procedure; input: unknown; timeout?: number },
|
||||
): Promise<RpcResult> {
|
||||
return this.execute(effects, options)
|
||||
}
|
||||
|
||||
private async initProcedure(
|
||||
async packageInit(
|
||||
effects: Effects,
|
||||
previousVersion: Optional<string>,
|
||||
timeoutMs: number | null,
|
||||
@@ -445,7 +384,6 @@ export class SystemForEmbassy implements System {
|
||||
id: `${id}-${internal}`,
|
||||
description: interfaceValue.description,
|
||||
hasPrimary: false,
|
||||
disabled: false,
|
||||
type:
|
||||
interfaceValue.ui &&
|
||||
(origin.scheme === "http" || origin.sslScheme === "https")
|
||||
@@ -490,7 +428,7 @@ export class SystemForEmbassy implements System {
|
||||
})
|
||||
}
|
||||
}
|
||||
private async uninit(
|
||||
async packageUninit(
|
||||
effects: Effects,
|
||||
nextVersion: Optional<string>,
|
||||
timeoutMs: number | null,
|
||||
@@ -499,7 +437,7 @@ export class SystemForEmbassy implements System {
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
}
|
||||
|
||||
private async createBackup(
|
||||
async createBackup(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void> {
|
||||
@@ -520,7 +458,7 @@ export class SystemForEmbassy implements System {
|
||||
await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest))
|
||||
}
|
||||
}
|
||||
private async restoreBackup(
|
||||
async restoreBackup(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void> {
|
||||
@@ -544,7 +482,7 @@ export class SystemForEmbassy implements System {
|
||||
await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest))
|
||||
}
|
||||
}
|
||||
private async getConfig(
|
||||
async getConfig(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ConfigRes> {
|
||||
@@ -585,7 +523,7 @@ export class SystemForEmbassy implements System {
|
||||
)) as any
|
||||
}
|
||||
}
|
||||
private async setConfig(
|
||||
async setConfig(
|
||||
effects: Effects,
|
||||
newConfigWithoutPointers: unknown,
|
||||
timeoutMs: number | null,
|
||||
@@ -677,7 +615,7 @@ export class SystemForEmbassy implements System {
|
||||
})
|
||||
}
|
||||
|
||||
private async migration(
|
||||
async migration(
|
||||
effects: Effects,
|
||||
fromVersion: string,
|
||||
timeoutMs: number | null,
|
||||
@@ -749,10 +687,10 @@ export class SystemForEmbassy implements System {
|
||||
}
|
||||
return { configured: true }
|
||||
}
|
||||
private async properties(
|
||||
async properties(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<ReturnType<T.ExpectedExports.properties>> {
|
||||
): Promise<T.PropertiesReturn> {
|
||||
// TODO BLU-J set the properties ever so often
|
||||
const setConfigValue = this.manifest.properties
|
||||
if (!setConfigValue) throw new Error("There is no properties")
|
||||
@@ -780,23 +718,80 @@ export class SystemForEmbassy implements System {
|
||||
if (!method)
|
||||
throw new Error("Expecting that the method properties exists")
|
||||
const properties = matchProperties.unsafeCast(
|
||||
await method(polyfillEffects(effects, this.manifest)).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
}),
|
||||
await method(polyfillEffects(effects, this.manifest)).then(
|
||||
fromReturnType,
|
||||
),
|
||||
)
|
||||
return asProperty(properties.data)
|
||||
}
|
||||
throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`)
|
||||
}
|
||||
private async action(
|
||||
async action(
|
||||
effects: Effects,
|
||||
actionId: string,
|
||||
formData: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionResult> {
|
||||
const actionProcedure = this.manifest.actions?.[actionId]?.implementation
|
||||
const toActionResult = ({
|
||||
message,
|
||||
value = "",
|
||||
copyable,
|
||||
qr,
|
||||
}: U.ActionResult): T.ActionResult => ({
|
||||
version: "0",
|
||||
message,
|
||||
value,
|
||||
copyable,
|
||||
qr,
|
||||
})
|
||||
if (!actionProcedure) throw Error("Action not found")
|
||||
if (actionProcedure.type === "docker") {
|
||||
const subcontainer = actionProcedure.inject
|
||||
? this.currentRunning?.mainSubContainerHandle
|
||||
: undefined
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
this.manifest.id,
|
||||
actionProcedure,
|
||||
this.manifest.volumes,
|
||||
{
|
||||
subcontainer,
|
||||
},
|
||||
)
|
||||
return toActionResult(
|
||||
JSON.parse(
|
||||
(
|
||||
await container.execFail(
|
||||
[
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(formData),
|
||||
],
|
||||
timeoutMs,
|
||||
)
|
||||
).stdout.toString(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.action?.[actionId]
|
||||
if (!method) throw new Error("Expecting that the method action exists")
|
||||
return await method(
|
||||
polyfillEffects(effects, this.manifest),
|
||||
formData as any,
|
||||
)
|
||||
.then(fromReturnType)
|
||||
.then(toActionResult)
|
||||
}
|
||||
}
|
||||
async dependenciesCheck(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<object> {
|
||||
const actionProcedure = this.manifest.dependencies?.[id]?.config?.check
|
||||
if (!actionProcedure) return { message: "Action not found", value: null }
|
||||
if (actionProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
@@ -811,27 +806,32 @@ export class SystemForEmbassy implements System {
|
||||
[
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(formData),
|
||||
JSON.stringify(oldConfig),
|
||||
],
|
||||
timeoutMs,
|
||||
)
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else {
|
||||
} else if (actionProcedure.type === "script") {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.action?.[actionId]
|
||||
if (!method) throw new Error("Expecting that the method action exists")
|
||||
const method = moduleCode.dependencies?.[id]?.check
|
||||
if (!method)
|
||||
throw new Error(
|
||||
`Expecting that the method dependency check ${id} exists`,
|
||||
)
|
||||
return (await method(
|
||||
polyfillEffects(effects, this.manifest),
|
||||
formData as any,
|
||||
oldConfig as any,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
private async dependenciesAutoconfig(
|
||||
async dependenciesAutoconfig(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
input: unknown,
|
||||
@@ -982,7 +982,10 @@ async function updateConfig(
|
||||
})
|
||||
.once()
|
||||
.catch((x) => {
|
||||
console.error("Could not get the service interface", x)
|
||||
console.error(
|
||||
"Could not get the service interface",
|
||||
utils.asError(x),
|
||||
)
|
||||
return null
|
||||
})
|
||||
const catchFn = <X>(fn: () => X) => {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { matchManifest } from "./matchManifest"
|
||||
import giteaManifest from "./__fixtures__/giteaManifest"
|
||||
import synapseManifest from "./__fixtures__/synapseManifest"
|
||||
|
||||
describe("matchManifest", () => {
|
||||
test("gittea", () => {
|
||||
matchManifest.unsafeCast(giteaManifest)
|
||||
})
|
||||
test("synapse", () => {
|
||||
matchManifest.unsafeCast(synapseManifest)
|
||||
})
|
||||
})
|
||||
@@ -55,10 +55,13 @@ export const matchManifest = object(
|
||||
string,
|
||||
every(
|
||||
matchProcedure,
|
||||
object({
|
||||
name: string,
|
||||
["success-message"]: string,
|
||||
}),
|
||||
object(
|
||||
{
|
||||
name: string,
|
||||
["success-message"]: string,
|
||||
},
|
||||
["success-message"],
|
||||
),
|
||||
),
|
||||
]),
|
||||
config: object({
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as oet from "./oldEmbassyTypes"
|
||||
import { Volume } from "../../../Models/Volume"
|
||||
import * as child_process from "child_process"
|
||||
import { promisify } from "util"
|
||||
import { daemons, startSdk, T } from "@start9labs/start-sdk"
|
||||
import { daemons, startSdk, T, utils } from "@start9labs/start-sdk"
|
||||
import "isomorphic-fetch"
|
||||
import { Manifest } from "./matchManifest"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
@@ -124,20 +124,18 @@ export const polyfillEffects = (
|
||||
wait(): Promise<oet.ResultType<string>>
|
||||
term(): Promise<void>
|
||||
} {
|
||||
const dockerProcedureContainer = DockerProcedureContainer.of(
|
||||
const promiseSubcontainer = DockerProcedureContainer.createSubContainer(
|
||||
effects,
|
||||
manifest.id,
|
||||
manifest.main,
|
||||
manifest.volumes,
|
||||
)
|
||||
const daemon = dockerProcedureContainer.then((dockerProcedureContainer) =>
|
||||
const daemon = promiseSubcontainer.then((subcontainer) =>
|
||||
daemons.runCommand()(
|
||||
effects,
|
||||
{ id: manifest.main.image },
|
||||
subcontainer,
|
||||
[input.command, ...(input.args || [])],
|
||||
{
|
||||
overlay: dockerProcedureContainer.overlay,
|
||||
},
|
||||
{},
|
||||
),
|
||||
)
|
||||
return {
|
||||
@@ -224,16 +222,16 @@ export const polyfillEffects = (
|
||||
return new Promise((resolve) => setTimeout(resolve, timeMs))
|
||||
},
|
||||
trace(whatToPrint: string): void {
|
||||
console.trace(whatToPrint)
|
||||
console.trace(utils.asError(whatToPrint))
|
||||
},
|
||||
warn(whatToPrint: string): void {
|
||||
console.warn(whatToPrint)
|
||||
console.warn(utils.asError(whatToPrint))
|
||||
},
|
||||
error(whatToPrint: string): void {
|
||||
console.error(whatToPrint)
|
||||
console.error(utils.asError(whatToPrint))
|
||||
},
|
||||
debug(whatToPrint: string): void {
|
||||
console.debug(whatToPrint)
|
||||
console.debug(utils.asError(whatToPrint))
|
||||
},
|
||||
info(whatToPrint: string): void {
|
||||
console.log(false)
|
||||
@@ -357,7 +355,7 @@ export const polyfillEffects = (
|
||||
})
|
||||
|
||||
spawned.stderr.on("data", (data: unknown) => {
|
||||
console.error(String(data))
|
||||
console.error(`polyfill.runAsync`, utils.asError(data))
|
||||
})
|
||||
|
||||
const id = async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig"
|
||||
import searNXG from "./__fixtures__/searNXG"
|
||||
import bitcoind from "./__fixtures__/bitcoind"
|
||||
import nostr from "./__fixtures__/nostr"
|
||||
import nostrConfig2 from "./__fixtures__/nostrConfig2"
|
||||
|
||||
describe("transformConfigSpec", () => {
|
||||
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
|
||||
@@ -30,4 +31,8 @@ describe("transformConfigSpec", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(nostr)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
test("transformConfigSpec(nostr2)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(nostrConfig2)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -74,7 +74,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec {
|
||||
integer: oldVal.integral,
|
||||
step: null,
|
||||
units: oldVal.units || null,
|
||||
placeholder: oldVal.placeholder || null,
|
||||
placeholder: oldVal.placeholder ? String(oldVal.placeholder) : null,
|
||||
}
|
||||
} else if (oldVal.type === "object") {
|
||||
newVal = {
|
||||
@@ -267,6 +267,31 @@ function getListSpec(
|
||||
{},
|
||||
),
|
||||
}
|
||||
} else if (isNumberList(oldVal)) {
|
||||
return {
|
||||
...partial,
|
||||
type: "list",
|
||||
default: oldVal.default.map(String) as string[],
|
||||
spec: {
|
||||
type: "text",
|
||||
patterns: oldVal.spec.integral
|
||||
? [{ regex: "[0-9]+", description: "Integral number type" }]
|
||||
: [
|
||||
{
|
||||
regex: "[-+]?[0-9]*\\.?[0-9]+",
|
||||
description: "Number type",
|
||||
},
|
||||
],
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
masked: false,
|
||||
generate: null,
|
||||
inputmode: "text",
|
||||
placeholder: oldVal.spec.placeholder
|
||||
? String(oldVal.spec.placeholder)
|
||||
: null,
|
||||
},
|
||||
}
|
||||
} else if (isStringList(oldVal)) {
|
||||
return {
|
||||
...partial,
|
||||
@@ -337,11 +362,16 @@ function isStringList(
|
||||
): val is OldValueSpecList & { subtype: "string" } {
|
||||
return val.subtype === "string"
|
||||
}
|
||||
function isNumberList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "number" } {
|
||||
return val.subtype === "number"
|
||||
}
|
||||
|
||||
function isObjectList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "object" } {
|
||||
if (["number", "union"].includes(val.subtype)) {
|
||||
if (["union"].includes(val.subtype)) {
|
||||
throw new Error("Invalid list subtype. enum, string, and object permitted.")
|
||||
}
|
||||
return val.subtype === "object"
|
||||
@@ -398,7 +428,7 @@ export const matchOldValueSpecNumber = object(
|
||||
description: string,
|
||||
warning: string,
|
||||
units: string,
|
||||
placeholder: string,
|
||||
placeholder: anyOf(number, string),
|
||||
},
|
||||
["default", "description", "warning", "units", "placeholder"],
|
||||
)
|
||||
@@ -499,6 +529,15 @@ const matchOldListValueSpecEnum = object({
|
||||
values: array(string),
|
||||
"value-names": dictionary([string, string]),
|
||||
})
|
||||
const matchOldListValueSpecNumber = object(
|
||||
{
|
||||
range: string,
|
||||
integral: boolean,
|
||||
units: string,
|
||||
placeholder: anyOf(number, string),
|
||||
},
|
||||
["units", "placeholder"],
|
||||
)
|
||||
|
||||
// represents a spec for a list
|
||||
const matchOldValueSpecList = every(
|
||||
@@ -531,6 +570,10 @@ const matchOldValueSpecList = every(
|
||||
subtype: literals("object"),
|
||||
spec: matchOldListValueSpecObject,
|
||||
}),
|
||||
object({
|
||||
subtype: literals("number"),
|
||||
spec: matchOldListValueSpecNumber,
|
||||
}),
|
||||
),
|
||||
)
|
||||
type OldValueSpecList = typeof matchOldValueSpecList._TYPE
|
||||
|
||||
@@ -4,10 +4,11 @@ import matches, { any, number, object, string, tuple } from "ts-matches"
|
||||
import { Effects } from "../../Models/Effects"
|
||||
import { RpcResult, matchRpcResult } from "../RpcListener"
|
||||
import { duration } from "../../Models/Duration"
|
||||
import { T } from "@start9labs/start-sdk"
|
||||
import { T, utils } from "@start9labs/start-sdk"
|
||||
import { Volume } from "../../Models/Volume"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
import { CallbackHolder } from "../../Models/CallbackHolder"
|
||||
import { Optional } from "ts-matches/lib/parsers/interfaces"
|
||||
|
||||
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
|
||||
|
||||
@@ -25,6 +26,107 @@ export class SystemForStartOs implements System {
|
||||
}
|
||||
|
||||
constructor(readonly abi: T.ABI) {}
|
||||
containerInit(): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async packageInit(
|
||||
effects: Effects,
|
||||
previousVersion: Optional<string> = null,
|
||||
timeoutMs: number | null = null,
|
||||
): Promise<void> {
|
||||
return void (await this.abi.init({ effects }))
|
||||
}
|
||||
async packageUninit(
|
||||
effects: Effects,
|
||||
nextVersion: Optional<string> = null,
|
||||
timeoutMs: number | null = null,
|
||||
): Promise<void> {
|
||||
return void (await this.abi.uninit({ effects, nextVersion }))
|
||||
}
|
||||
async createBackup(
|
||||
effects: T.Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void> {
|
||||
return void (await this.abi.createBackup({
|
||||
effects,
|
||||
pathMaker: ((options) =>
|
||||
new Volume(options.volume, options.path).path) as T.PathMaker,
|
||||
}))
|
||||
}
|
||||
async restoreBackup(
|
||||
effects: T.Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void> {
|
||||
return void (await this.abi.restoreBackup({
|
||||
effects,
|
||||
pathMaker: ((options) =>
|
||||
new Volume(options.volume, options.path).path) as T.PathMaker,
|
||||
}))
|
||||
}
|
||||
getConfig(
|
||||
effects: T.Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ConfigRes> {
|
||||
return this.abi.getConfig({ effects })
|
||||
}
|
||||
async setConfig(
|
||||
effects: Effects,
|
||||
input: { effects: Effects; input: Record<string, unknown> },
|
||||
timeoutMs: number | null,
|
||||
): Promise<void> {
|
||||
const _: unknown = await this.abi.setConfig({ effects, input })
|
||||
return
|
||||
}
|
||||
migration(
|
||||
effects: Effects,
|
||||
fromVersion: string,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.MigrationRes> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
properties(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.PropertiesReturn> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async action(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
formData: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionResult> {
|
||||
const action = (await this.abi.actions({ effects }))[id]
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.run({ effects })
|
||||
}
|
||||
dependenciesCheck(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<any> {
|
||||
const dependencyConfig = this.abi.dependencyConfig[id]
|
||||
if (!dependencyConfig) throw new Error(`dependencyConfig ${id} not found`)
|
||||
return dependencyConfig.query({ effects })
|
||||
}
|
||||
async dependenciesAutoconfig(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
remoteConfig: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void> {
|
||||
const dependencyConfig = this.abi.dependencyConfig[id]
|
||||
if (!dependencyConfig) throw new Error(`dependencyConfig ${id} not found`)
|
||||
const queryResults = await this.getConfig(effects, timeoutMs)
|
||||
return void (await dependencyConfig.update({
|
||||
queryResults,
|
||||
remoteConfig,
|
||||
})) // TODO
|
||||
}
|
||||
async actionsMetadata(effects: T.Effects): Promise<T.ActionMetadata[]> {
|
||||
return this.abi.actionsMetadata({ effects })
|
||||
}
|
||||
|
||||
async init(): Promise<void> {}
|
||||
|
||||
@@ -57,7 +159,9 @@ export class SystemForStartOs implements System {
|
||||
if (this.runningMain) {
|
||||
this.runningMain.callbacks
|
||||
.callCallback(callback, args)
|
||||
.catch((error) => console.error(`callback ${callback} failed`, error))
|
||||
.catch((error) =>
|
||||
console.error(`callback ${callback} failed`, utils.asError(error)),
|
||||
)
|
||||
} else {
|
||||
console.warn(`callback ${callback} ignored because system is not running`)
|
||||
}
|
||||
@@ -70,157 +174,4 @@ export class SystemForStartOs implements System {
|
||||
this.runningMain = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure: Procedure
|
||||
input?: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<RpcResult> {
|
||||
return this._execute(effects, options)
|
||||
.then((x) =>
|
||||
matches(x)
|
||||
.when(
|
||||
object({
|
||||
result: any,
|
||||
}),
|
||||
(x) => x,
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
error: string,
|
||||
}),
|
||||
(x) => ({
|
||||
error: {
|
||||
code: 0,
|
||||
message: x.error,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
"error-code": tuple(number, string),
|
||||
}),
|
||||
({ "error-code": [code, message] }) => ({
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.defaultTo({ result: x }),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
if (error instanceof Error)
|
||||
return {
|
||||
error: {
|
||||
code: 0,
|
||||
message: error.name,
|
||||
data: {
|
||||
details: error.message,
|
||||
debug: `${error?.cause ?? "[noCause]"}:${error?.stack ?? "[noStack]"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
if (matchRpcResult.test(error)) return error
|
||||
return {
|
||||
error: {
|
||||
code: 0,
|
||||
message: String(error),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
async _execute(
|
||||
effects: Effects | MainEffects,
|
||||
options: {
|
||||
procedure: Procedure
|
||||
input?: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<unknown> {
|
||||
switch (options.procedure) {
|
||||
case "/init": {
|
||||
const previousVersion =
|
||||
string.optional().unsafeCast(options.input) || null
|
||||
return this.abi.init({ effects, previousVersion })
|
||||
}
|
||||
case "/uninit": {
|
||||
const nextVersion = string.optional().unsafeCast(options.input) || null
|
||||
return this.abi.uninit({ effects, nextVersion })
|
||||
}
|
||||
// case "/main/start": {
|
||||
//
|
||||
// }
|
||||
// case "/main/stop": {
|
||||
// if (this.onTerm) await this.onTerm()
|
||||
// await effects.setMainStatus({ status: "stopped" })
|
||||
// delete this.onTerm
|
||||
// return duration(30, "s")
|
||||
// }
|
||||
case "/config/set": {
|
||||
const input = options.input as any // TODO
|
||||
return this.abi.setConfig({ effects, input })
|
||||
}
|
||||
case "/config/get": {
|
||||
return this.abi.getConfig({ effects })
|
||||
}
|
||||
case "/backup/create":
|
||||
return this.abi.createBackup({
|
||||
effects,
|
||||
pathMaker: ((options) =>
|
||||
new Volume(options.volume, options.path).path) as T.PathMaker,
|
||||
})
|
||||
case "/backup/restore":
|
||||
return this.abi.restoreBackup({
|
||||
effects,
|
||||
pathMaker: ((options) =>
|
||||
new Volume(options.volume, options.path).path) as T.PathMaker,
|
||||
})
|
||||
case "/actions/metadata": {
|
||||
return this.abi.actionsMetadata({ effects })
|
||||
}
|
||||
case "/properties": {
|
||||
throw new Error("TODO")
|
||||
}
|
||||
default:
|
||||
const procedures = unNestPath(options.procedure)
|
||||
const id = procedures[2]
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "get": {
|
||||
const action = (await this.abi.actions({ effects }))[id]
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.getConfig({ effects })
|
||||
}
|
||||
case procedures[1] === "actions" && procedures[3] === "run": {
|
||||
const action = (await this.abi.actions({ effects }))[id]
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.run({ effects, input: options.input as any }) // TODO
|
||||
}
|
||||
case procedures[1] === "dependencies" && procedures[3] === "query": {
|
||||
const dependencyConfig = this.abi.dependencyConfig[id]
|
||||
if (!dependencyConfig)
|
||||
throw new Error(`dependencyConfig ${id} not found`)
|
||||
const localConfig = options.input
|
||||
return dependencyConfig.query({ effects })
|
||||
}
|
||||
case procedures[1] === "dependencies" && procedures[3] === "update": {
|
||||
const dependencyConfig = this.abi.dependencyConfig[id]
|
||||
if (!dependencyConfig)
|
||||
throw new Error(`dependencyConfig ${id} not found`)
|
||||
return dependencyConfig.update(options.input as any) // TODO
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async sandbox(
|
||||
effects: Effects,
|
||||
options: { procedure: Procedure; input?: unknown; timeout?: number },
|
||||
): Promise<RpcResult> {
|
||||
return this.execute(effects, options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RpcResult } from "../Adapters/RpcListener"
|
||||
import { Effects } from "../Models/Effects"
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
import { Optional } from "ts-matches/lib/parsers/interfaces"
|
||||
|
||||
export type Procedure =
|
||||
| "/init"
|
||||
@@ -22,28 +23,60 @@ export type ExecuteResult =
|
||||
| { ok: unknown }
|
||||
| { err: { code: number; message: string } }
|
||||
export type System = {
|
||||
init(): Promise<void>
|
||||
containerInit(): Promise<void>
|
||||
|
||||
start(effects: MainEffects): Promise<void>
|
||||
callCallback(callback: number, args: any[]): void
|
||||
stop(): Promise<void>
|
||||
|
||||
execute(
|
||||
packageInit(
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure: Procedure
|
||||
input: unknown
|
||||
timeout?: number
|
||||
},
|
||||
): Promise<RpcResult>
|
||||
sandbox(
|
||||
previousVersion: Optional<string>,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void>
|
||||
packageUninit(
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure: Procedure
|
||||
input: unknown
|
||||
timeout?: number
|
||||
},
|
||||
): Promise<RpcResult>
|
||||
nextVersion: Optional<string>,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void>
|
||||
|
||||
createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
||||
restoreBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
||||
getConfig(effects: T.Effects, timeoutMs: number | null): Promise<T.ConfigRes>
|
||||
setConfig(
|
||||
effects: Effects,
|
||||
input: { effects: Effects; input: Record<string, unknown> },
|
||||
timeoutMs: number | null,
|
||||
): Promise<void>
|
||||
migration(
|
||||
effects: Effects,
|
||||
fromVersion: string,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.MigrationRes>
|
||||
properties(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.PropertiesReturn>
|
||||
action(
|
||||
effects: Effects,
|
||||
actionId: string,
|
||||
formData: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionResult>
|
||||
|
||||
dependenciesCheck(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<any>
|
||||
dependenciesAutoconfig(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void>
|
||||
actionsMetadata(effects: T.Effects): Promise<T.ActionMetadata[]>
|
||||
|
||||
exit(): Promise<void>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user