Release v0.1.1:2 with certrelay, nacho, and prebuilt subspaces.
Build Service / BuildPackage (push) Has been cancelled
Build Service / BuildPackage (push) Has been cancelled
Remove the embedded explorer/indexer stack, switch subspaces to prebuilt images, add certrelay and nacho as always-on services, expose Spaces and Subs APIs, and wire optional HTTP basic auth for subs and subs-prover with the correct SUBS_BASIC_AUTH_PASSWORD env var. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import {
|
||||
dataDir,
|
||||
NACHO_DEFAULT_IGNORE_NAMES,
|
||||
NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT,
|
||||
NACHO_DIR,
|
||||
} from '../utils'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
const IGNORE_NAMES_PATH = `${NACHO_DIR}/ignore_names.txt`
|
||||
|
||||
const mainMount = sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: dataDir,
|
||||
readonly: false,
|
||||
})
|
||||
|
||||
// Reads /data/nacho/ignore_names.txt off the main volume via a temp
|
||||
// subcontainer. Returns null if the file is missing/empty/unreadable so the
|
||||
// prefill falls back to the package default.
|
||||
async function readIgnoreNamesFile(effects: any): Promise<string | null> {
|
||||
try {
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'spaces' },
|
||||
mainMount,
|
||||
'nacho-read-ignore-names',
|
||||
(subc) =>
|
||||
subc.exec(
|
||||
[
|
||||
'sh',
|
||||
'-c',
|
||||
`if [ -f ${IGNORE_NAMES_PATH} ]; then cat ${IGNORE_NAMES_PATH}; fi`,
|
||||
],
|
||||
{ user: 'root' },
|
||||
),
|
||||
)
|
||||
if (res.exitCode !== 0) return null
|
||||
const value = (res.stdout ?? '').toString().trim()
|
||||
return value.length > 0 ? value : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
ignoreNames: Value.text({
|
||||
name: i18n('Ignore Names'),
|
||||
description: i18n(
|
||||
'Comma-separated list of names for the nacho UI to ignore (EXPO_PUBLIC_IGNORE_NAMES).',
|
||||
),
|
||||
warning: null,
|
||||
footnote: null,
|
||||
default: NACHO_DEFAULT_IGNORE_NAMES,
|
||||
required: true,
|
||||
masked: false,
|
||||
placeholder: 'fold,swifty',
|
||||
minLength: 0,
|
||||
maxLength: null,
|
||||
}),
|
||||
workshopPdfLinkText: Value.text({
|
||||
name: i18n('Workshop PDF Link Text'),
|
||||
description: i18n(
|
||||
'Display text for the workshop PDF link in the nacho UI (EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT). Leave empty to suppress the label.',
|
||||
),
|
||||
warning: null,
|
||||
footnote: null,
|
||||
default: NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT,
|
||||
required: false,
|
||||
masked: false,
|
||||
placeholder: 'Workshop PDF',
|
||||
minLength: 0,
|
||||
maxLength: null,
|
||||
}),
|
||||
})
|
||||
|
||||
export const configureNacho = sdk.Action.withInput(
|
||||
// id
|
||||
'configure-nacho',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Configure Nacho'),
|
||||
description: i18n(
|
||||
'Set the user-tunable nacho settings. The Ignore Names list is written to /data/nacho/ignore_names.txt (read by nacho at runtime). The Workshop PDF link text is set via EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT. Saving restarts the service. To upload a new Workshop PDF, use the separate "Upload Support PDF" action.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// input
|
||||
inputSpec,
|
||||
|
||||
// prefill — current values. Ignore Names is sourced from the file on disk
|
||||
// (the source of truth nacho actually reads), with the default as a fallback.
|
||||
async ({ effects }) => {
|
||||
const [store, fileValue] = await Promise.all([
|
||||
storeJson.read().once(),
|
||||
readIgnoreNamesFile(effects),
|
||||
])
|
||||
return {
|
||||
ignoreNames: fileValue ?? NACHO_DEFAULT_IGNORE_NAMES,
|
||||
workshopPdfLinkText:
|
||||
store?.nachoWorkshopPdfLinkText ??
|
||||
NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT,
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects, input }) => {
|
||||
// Persist the workshop PDF link text in the store (so it stays an
|
||||
// EXPO_PUBLIC_* env var) and write the ignore names to its file on the
|
||||
// main volume. mkdir -p first in case /data/nacho doesn't yet exist
|
||||
// (e.g., the user runs this action before nacho-setup has executed).
|
||||
await storeJson.merge(effects, {
|
||||
nachoWorkshopPdfLinkText: input.workshopPdfLinkText ?? '',
|
||||
})
|
||||
|
||||
const ignoreNames = input.ignoreNames
|
||||
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'spaces' },
|
||||
mainMount,
|
||||
'nacho-write-ignore-names',
|
||||
(subc) =>
|
||||
subc.exec(
|
||||
[
|
||||
'sh',
|
||||
'-c',
|
||||
`set -eu; mkdir -p ${NACHO_DIR}; cat > ${IGNORE_NAMES_PATH} && chmod 644 ${IGNORE_NAMES_PATH}`,
|
||||
],
|
||||
{ input: ignoreNames, user: 'root' },
|
||||
),
|
||||
)
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not write ignore_names.txt: ${error}', {
|
||||
error:
|
||||
((res.stderr ?? '').toString() || `exit ${res.exitCode}`).slice(
|
||||
0,
|
||||
300,
|
||||
),
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Nacho configuration saved. Ignore Names written to /data/nacho/ignore_names.txt; the service is restarting to apply the new values.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1,37 +0,0 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const disableExplorer = sdk.Action.withoutInput(
|
||||
// id
|
||||
'disable-explorer',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => {
|
||||
const current = await storeJson.read((s) => s.enableExplorer).once()
|
||||
return {
|
||||
name: i18n('Disable Embedded Explorer'),
|
||||
description: i18n(
|
||||
'Stop the embedded PostgreSQL + Go indexer. Indexed data on disk (/data/postgres, /data/explorer-indexer) is preserved and will be reused if the explorer is re-enabled later. Service restarts automatically.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: current === true ? 'enabled' : 'hidden',
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
await storeJson.merge(effects, { enableExplorer: false })
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Embedded explorer disabled. The service is restarting in spaces-only mode (spaced + web terminal). Run "Enable Embedded Explorer" to turn it back on.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const disableSubsAuth = sdk.Action.withoutInput(
|
||||
// id
|
||||
'disable-subs-auth',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => {
|
||||
const enabled = await storeJson.read((s) => s.subsAuthEnabled).once()
|
||||
return {
|
||||
name: i18n('Disable Subspaces Auth'),
|
||||
description: i18n(
|
||||
'Turn off HTTP basic auth in front of the Subspaces Web UI / Subs API. Stored credentials are preserved so re-enabling does not generate new ones; use "Set Subspaces Auth Credentials" to rotate. Service restarts.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: enabled === true ? 'enabled' : 'hidden',
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
await storeJson.merge(effects, { subsAuthEnabled: false })
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Subspaces Auth disabled. The service is restarting; subs will serve unauthenticated again on port 7777. Stored credentials are kept for the next enable.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const disableSubsProverAuth = sdk.Action.withoutInput(
|
||||
// id
|
||||
'disable-subs-prover-auth',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => {
|
||||
const enabled = await storeJson.read((s) => s.subsProverAuthEnabled).once()
|
||||
return {
|
||||
name: i18n('Disable Subspaces Prover Auth'),
|
||||
description: i18n(
|
||||
'Turn off HTTP basic auth in front of the Subspaces Prover. Stored credentials are preserved so re-enabling does not generate new ones; use "Set Subspaces Prover Auth Credentials" to rotate. Service restarts.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: enabled === true ? 'enabled' : 'hidden',
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
await storeJson.merge(effects, { subsProverAuthEnabled: false })
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Subspaces Prover Auth disabled. The service is restarting; subs-prover will serve unauthenticated again on port 8888. Stored credentials are kept for the next enable.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1,37 +0,0 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const enableExplorer = sdk.Action.withoutInput(
|
||||
// id
|
||||
'enable-explorer',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => {
|
||||
const current = await storeJson.read((s) => s.enableExplorer).once()
|
||||
return {
|
||||
name: i18n('Enable Embedded Explorer'),
|
||||
description: i18n(
|
||||
'Turn on the embedded PostgreSQL + Go indexer. Spaces protocol data (blocks, transactions, spaces, rollouts) will be indexed locally for use by the future explorer web UI. Service restarts automatically.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: current === true ? 'hidden' : 'enabled',
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
await storeJson.merge(effects, { enableExplorer: true })
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Embedded explorer enabled. The service is restarting; the indexer will begin fetching blocks shortly. First-time start can take several minutes for the Go build.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { randomPassword } from '../utils'
|
||||
|
||||
const DEFAULT_USERNAME = 'spaces'
|
||||
|
||||
export const enableSubsAuth = sdk.Action.withoutInput(
|
||||
// id
|
||||
'enable-subs-auth',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => {
|
||||
const enabled = await storeJson.read((s) => s.subsAuthEnabled).once()
|
||||
return {
|
||||
name: i18n('Enable Subspaces Auth'),
|
||||
description: i18n(
|
||||
'Turn on HTTP basic auth in front of the Subspaces Web UI and the Subs API (both on port 7777). If no credentials have been set yet, a random password is generated (username defaults to "spaces"). Use "Show Subspaces Auth Credentials" afterwards to retrieve them. Service restarts so subs picks up the auth env vars.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: enabled === true ? 'hidden' : 'enabled',
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const existing = await storeJson.read((s) => s.subsAuth).once()
|
||||
const subsAuth = existing ?? {
|
||||
username: DEFAULT_USERNAME,
|
||||
password: randomPassword(),
|
||||
}
|
||||
|
||||
await storeJson.merge(effects, {
|
||||
subsAuth,
|
||||
subsAuthEnabled: true,
|
||||
})
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: existing
|
||||
? i18n(
|
||||
'Subspaces Auth enabled with the existing stored credentials. The service is restarting; use "Show Subspaces Auth Credentials" to view them.',
|
||||
)
|
||||
: i18n(
|
||||
'Subspaces Auth enabled and a fresh credential pair was generated. The service is restarting; use "Show Subspaces Auth Credentials" to view them.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { randomPassword } from '../utils'
|
||||
|
||||
const DEFAULT_USERNAME = 'spaces'
|
||||
|
||||
export const enableSubsProverAuth = sdk.Action.withoutInput(
|
||||
// id
|
||||
'enable-subs-prover-auth',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => {
|
||||
const enabled = await storeJson.read((s) => s.subsProverAuthEnabled).once()
|
||||
return {
|
||||
name: i18n('Enable Subspaces Prover Auth'),
|
||||
description: i18n(
|
||||
'Turn on HTTP basic auth in front of the Subspaces Prover (port 8888). If no credentials have been set yet, a random password is generated (username defaults to "spaces"). Use "Show Subspaces Prover Auth Credentials" afterwards to retrieve them. Service restarts so subs-prover picks up the auth env vars.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: enabled === true ? 'hidden' : 'enabled',
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const existing = await storeJson.read((s) => s.subsProverAuth).once()
|
||||
const subsProverAuth = existing ?? {
|
||||
username: DEFAULT_USERNAME,
|
||||
password: randomPassword(),
|
||||
}
|
||||
|
||||
await storeJson.merge(effects, {
|
||||
subsProverAuth,
|
||||
subsProverAuthEnabled: true,
|
||||
})
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: existing
|
||||
? i18n(
|
||||
'Subspaces Prover Auth enabled with the existing stored credentials. The service is restarting; use "Show Subspaces Prover Auth Credentials" to view them.',
|
||||
)
|
||||
: i18n(
|
||||
'Subspaces Prover Auth enabled and a fresh credential pair was generated. The service is restarting; use "Show Subspaces Prover Auth Credentials" to view them.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
+22
-12
@@ -1,41 +1,51 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { configureCertrelay } from './configureCertrelay'
|
||||
import { disableExplorer } from './disableExplorer'
|
||||
import { configureNacho } from './configureNacho'
|
||||
import { disableSubsAuth } from './disableSubsAuth'
|
||||
import { disableSubsProverAuth } from './disableSubsProverAuth'
|
||||
import { disableSubspaces } from './disableSubspaces'
|
||||
import { enableExplorer } from './enableExplorer'
|
||||
import { enableSubsAuth } from './enableSubsAuth'
|
||||
import { enableSubsProverAuth } from './enableSubsProverAuth'
|
||||
import { enableSubspaces } from './enableSubspaces'
|
||||
import { exportWallet } from './exportWallet'
|
||||
import { importWallet } from './importWallet'
|
||||
import { resetDbState } from './resetDbState'
|
||||
import { resetExplorerState } from './resetExplorerState'
|
||||
import { resetIndexerState } from './resetIndexerState'
|
||||
import { resetPassword } from './resetPassword'
|
||||
import { resetSpacedState } from './resetSpacedState'
|
||||
import { resetSubspacesState } from './resetSubspacesState'
|
||||
import { setBitcoinRpc } from './setBitcoinRpc'
|
||||
import { setSubsCredentials } from './setSubsCredentials'
|
||||
import { setSubsProver } from './setSubsProver'
|
||||
import { setSubsProverCredentials } from './setSubsProverCredentials'
|
||||
import { showCredentials } from './showCredentials'
|
||||
import { showDbCredentials } from './showDbCredentials'
|
||||
import { showPassword } from './showPassword'
|
||||
import { showSpacedCredentials } from './showSpacedCredentials'
|
||||
import { showSubsCredentials } from './showSubsCredentials'
|
||||
import { showSubsProverCredentials } from './showSubsProverCredentials'
|
||||
import { syncStatus } from './syncStatus'
|
||||
import { uploadSupportPdf } from './uploadSupportPdf'
|
||||
|
||||
export const actions = sdk.Actions.of()
|
||||
.addAction(resetPassword)
|
||||
.addAction(showCredentials)
|
||||
.addAction(showPassword)
|
||||
.addAction(setBitcoinRpc)
|
||||
.addAction(showSpacedCredentials)
|
||||
.addAction(syncStatus)
|
||||
.addAction(resetSpacedState)
|
||||
.addAction(exportWallet)
|
||||
.addAction(importWallet)
|
||||
.addAction(enableExplorer)
|
||||
.addAction(disableExplorer)
|
||||
.addAction(showDbCredentials)
|
||||
.addAction(resetDbState)
|
||||
.addAction(resetIndexerState)
|
||||
.addAction(resetExplorerState)
|
||||
.addAction(enableSubspaces)
|
||||
.addAction(disableSubspaces)
|
||||
.addAction(setSubsProver)
|
||||
.addAction(enableSubsAuth)
|
||||
.addAction(disableSubsAuth)
|
||||
.addAction(showSubsCredentials)
|
||||
.addAction(setSubsCredentials)
|
||||
.addAction(enableSubsProverAuth)
|
||||
.addAction(disableSubsProverAuth)
|
||||
.addAction(showSubsProverCredentials)
|
||||
.addAction(setSubsProverCredentials)
|
||||
.addAction(resetSubspacesState)
|
||||
.addAction(configureCertrelay)
|
||||
.addAction(configureNacho)
|
||||
.addAction(uploadSupportPdf)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir, pgDataDir } from '../utils'
|
||||
|
||||
export const resetDbState = sdk.Action.withoutInput(
|
||||
// id
|
||||
'reset-db-state',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Reset Database State'),
|
||||
description: i18n(
|
||||
'Wipe /data/postgres so PostgreSQL re-initializes from scratch.',
|
||||
),
|
||||
warning: i18n(
|
||||
'This deletes all PostgreSQL data on disk. The next start will re-create an empty database. store.json (passwords + RPC credentials) is preserved.',
|
||||
),
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'spaces' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: dataDir,
|
||||
readonly: false,
|
||||
}),
|
||||
'spaces-reset-db',
|
||||
(subc) => subc.exec(['rm', '-rf', pgDataDir], { user: 'root' }),
|
||||
)
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not wipe PostgreSQL state: ${error}', {
|
||||
error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`,
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'PostgreSQL data has been wiped. Start (or restart) the service to re-initialize the database.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1,60 +0,0 @@
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir, EXPLORER_DIR } from '../utils'
|
||||
|
||||
export const resetExplorerState = sdk.Action.withoutInput(
|
||||
// id
|
||||
'reset-explorer-state',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Reset Explorer UI State'),
|
||||
description: i18n(
|
||||
'Wipe /data/explorer-ui so the next start re-fetches the explorer source and rebuilds it from scratch.',
|
||||
),
|
||||
warning: i18n(
|
||||
'This deletes the cached explorer source and the built SvelteKit bundle. The next start will need internet access to re-download from GitHub and to fetch npm dependencies. PostgreSQL data is preserved.',
|
||||
),
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
// TODO(prebuilt-explorer): switch back to the explorer image once it
|
||||
// exists. Any image with `rm` works for the wipe; `spaces` is always present.
|
||||
{ imageId: 'spaces' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: dataDir,
|
||||
readonly: false,
|
||||
}),
|
||||
'spaces-reset-explorer',
|
||||
(subc) => subc.exec(['rm', '-rf', EXPLORER_DIR], { user: 'root' }),
|
||||
)
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not wipe explorer state: ${error}', {
|
||||
error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`,
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Explorer cache has been wiped. Start (or restart) the service to re-fetch and rebuild from GitHub.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1,58 +0,0 @@
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir, INDEXER_DIR } from '../utils'
|
||||
|
||||
export const resetIndexerState = sdk.Action.withoutInput(
|
||||
// id
|
||||
'reset-indexer-state',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Reset Indexer State'),
|
||||
description: i18n(
|
||||
'Wipe /data/explorer-indexer so the next start re-fetches the indexer source and rebuilds the sync + goose binaries.',
|
||||
),
|
||||
warning: i18n(
|
||||
'This deletes the cached indexer source and compiled binaries. The next start will need internet access to re-download from GitHub and to fetch Go modules. PostgreSQL data (the indexed blocks themselves) is preserved — use Reset Database State if you also want to clear that.',
|
||||
),
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'spaces' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: dataDir,
|
||||
readonly: false,
|
||||
}),
|
||||
'spaces-reset-indexer',
|
||||
(subc) => subc.exec(['rm', '-rf', INDEXER_DIR], { user: 'root' }),
|
||||
)
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not wipe indexer state: ${error}', {
|
||||
error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`,
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Indexer cache has been wiped. Start (or restart) the service to re-fetch and rebuild from GitHub.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { randomPassword } from '../utils'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
const DEFAULT_USERNAME = 'spaces'
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
username: Value.text({
|
||||
name: i18n('Username'),
|
||||
description: i18n(
|
||||
'Username for HTTP basic auth in front of subs (SUBS_BASIC_AUTH_USER).',
|
||||
),
|
||||
warning: null,
|
||||
footnote: null,
|
||||
default: DEFAULT_USERNAME,
|
||||
required: true,
|
||||
masked: false,
|
||||
placeholder: 'spaces',
|
||||
minLength: 1,
|
||||
maxLength: null,
|
||||
}),
|
||||
password: Value.text({
|
||||
name: i18n('Password'),
|
||||
description: i18n(
|
||||
'Password for HTTP basic auth in front of subs (SUBS_BASIC_AUTH_PASSWORD). Leave blank to auto-generate a random one.',
|
||||
),
|
||||
warning: null,
|
||||
footnote: null,
|
||||
default: null,
|
||||
required: false,
|
||||
masked: true,
|
||||
placeholder: '(leave blank to auto-generate)',
|
||||
minLength: 0,
|
||||
maxLength: null,
|
||||
}),
|
||||
})
|
||||
|
||||
export const setSubsCredentials = sdk.Action.withInput(
|
||||
// id
|
||||
'set-subs-credentials',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Set Subspaces Auth Credentials'),
|
||||
description: i18n(
|
||||
'Set or rotate the HTTP basic auth credentials enforced in front of the Subspaces Web UI and Subs API. Auth is enabled automatically. Saving restarts the service.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// input
|
||||
inputSpec,
|
||||
|
||||
// prefill — current username, never the password (the user explicitly types
|
||||
// a new password or leaves blank to auto-generate)
|
||||
async ({ effects }) => {
|
||||
const subsAuth = await storeJson.read((s) => s.subsAuth).once()
|
||||
return {
|
||||
username: subsAuth?.username ?? DEFAULT_USERNAME,
|
||||
password: null,
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects, input }) => {
|
||||
const password =
|
||||
input.password && input.password.length > 0
|
||||
? input.password
|
||||
: randomPassword()
|
||||
|
||||
await storeJson.merge(effects, {
|
||||
subsAuth: { username: input.username, password },
|
||||
subsAuthEnabled: true,
|
||||
})
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Subspaces Auth credentials saved. The service is restarting so subs picks up the new credentials.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { randomPassword } from '../utils'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
const DEFAULT_USERNAME = 'spaces'
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
username: Value.text({
|
||||
name: i18n('Username'),
|
||||
description: i18n(
|
||||
'Username for HTTP basic auth in front of subs-prover (SUBS_PROVER_BASIC_AUTH_USER).',
|
||||
),
|
||||
warning: null,
|
||||
footnote: null,
|
||||
default: DEFAULT_USERNAME,
|
||||
required: true,
|
||||
masked: false,
|
||||
placeholder: 'spaces',
|
||||
minLength: 1,
|
||||
maxLength: null,
|
||||
}),
|
||||
password: Value.text({
|
||||
name: i18n('Password'),
|
||||
description: i18n(
|
||||
'Password for HTTP basic auth in front of subs-prover (SUBS_PROVER_BASIC_AUTH_PASSWORD). Leave blank to auto-generate a random one.',
|
||||
),
|
||||
warning: null,
|
||||
footnote: null,
|
||||
default: null,
|
||||
required: false,
|
||||
masked: true,
|
||||
placeholder: '(leave blank to auto-generate)',
|
||||
minLength: 0,
|
||||
maxLength: null,
|
||||
}),
|
||||
})
|
||||
|
||||
export const setSubsProverCredentials = sdk.Action.withInput(
|
||||
// id
|
||||
'set-subs-prover-credentials',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Set Subspaces Prover Auth Credentials'),
|
||||
description: i18n(
|
||||
'Set or rotate the HTTP basic auth credentials enforced in front of the Subspaces Prover. Saving restarts the service if auth is currently enabled.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// input
|
||||
inputSpec,
|
||||
|
||||
// prefill — current username, never the password (the user explicitly types
|
||||
// a new password or leaves blank to auto-generate)
|
||||
async ({ effects }) => {
|
||||
const subsProverAuth = await storeJson.read((s) => s.subsProverAuth).once()
|
||||
return {
|
||||
username: subsProverAuth?.username ?? DEFAULT_USERNAME,
|
||||
password: null,
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects, input }) => {
|
||||
const password =
|
||||
input.password && input.password.length > 0
|
||||
? input.password
|
||||
: randomPassword()
|
||||
|
||||
await storeJson.merge(effects, {
|
||||
subsProverAuth: { username: input.username, password },
|
||||
})
|
||||
|
||||
const enabled = await storeJson.read((s) => s.subsProverAuthEnabled).once()
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: enabled
|
||||
? i18n(
|
||||
'Subspaces Prover Auth credentials saved. The service is restarting so subs-prover picks up the new credentials.',
|
||||
)
|
||||
: i18n(
|
||||
'Subspaces Prover Auth credentials saved. Auth is currently DISABLED — enable it with "Enable Subspaces Prover Auth" to enforce these credentials.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1,17 +1,17 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { POSTGRES_PORT } from '../utils'
|
||||
import { spacedRpcPort } from '../utils'
|
||||
|
||||
export const showDbCredentials = sdk.Action.withoutInput(
|
||||
export const showSpacedCredentials = sdk.Action.withoutInput(
|
||||
// id
|
||||
'show-db-credentials',
|
||||
'show-spaced-credentials',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Show Database Credentials'),
|
||||
name: i18n('Show Spaces API Credentials'),
|
||||
description: i18n(
|
||||
'Display the PostgreSQL username, password, database, and connection URL.',
|
||||
'Display the spaced RPC username and password (SPACED_RPC_USER / SPACED_RPC_PASSWORD) used to authenticate against the Spaces API.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
@@ -21,26 +21,25 @@ export const showDbCredentials = sdk.Action.withoutInput(
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const dbAuth = await storeJson.read((s) => s.dbAuth).once()
|
||||
const username = dbAuth?.username ?? ''
|
||||
const password = dbAuth?.password ?? ''
|
||||
const database = dbAuth?.database ?? ''
|
||||
const url = dbAuth
|
||||
? `postgres://${dbAuth.username}:${dbAuth.password}@127.0.0.1:${POSTGRES_PORT}/${dbAuth.database}`
|
||||
const spacedAuth = await storeJson.read((s) => s.spacedAuth).once()
|
||||
const username = spacedAuth?.username ?? ''
|
||||
const password = spacedAuth?.password ?? ''
|
||||
const url = spacedAuth
|
||||
? `http://${spacedAuth.username}:${spacedAuth.password}@127.0.0.1:${spacedRpcPort}`
|
||||
: ''
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Show Database Credentials'),
|
||||
title: i18n('Show Spaces API Credentials'),
|
||||
message: i18n(
|
||||
'Use these credentials to connect to the Spaces PostgreSQL database (loopback only inside the container).',
|
||||
'Use these credentials to authenticate against the spaced JSON-RPC (the Spaces API). The connection URL shown uses the loopback address; for external access, substitute the host StartOS exposes for the Spaces API interface.',
|
||||
),
|
||||
result: {
|
||||
type: 'group',
|
||||
value: [
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Username'),
|
||||
name: i18n('SPACED_RPC_USER'),
|
||||
description: null,
|
||||
value: username,
|
||||
masked: false,
|
||||
@@ -49,22 +48,13 @@ export const showDbCredentials = sdk.Action.withoutInput(
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Password'),
|
||||
name: i18n('SPACED_RPC_PASSWORD'),
|
||||
description: null,
|
||||
value: password,
|
||||
masked: true,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Database'),
|
||||
description: null,
|
||||
value: database,
|
||||
masked: false,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Connection URL'),
|
||||
@@ -0,0 +1,79 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { SUBSPACES_UI_PORT } from '../utils'
|
||||
|
||||
export const showSubsCredentials = sdk.Action.withoutInput(
|
||||
// id
|
||||
'show-subs-credentials',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Show Subspaces Auth Credentials'),
|
||||
description: i18n(
|
||||
'Display the HTTP basic auth username and password used in front of the Subspaces Web UI and Subs API. Returns blanks if no credentials have been set yet.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const [subsAuth, enabled] = await Promise.all([
|
||||
storeJson.read((s) => s.subsAuth).once(),
|
||||
storeJson.read((s) => s.subsAuthEnabled).once(),
|
||||
])
|
||||
|
||||
const username = subsAuth?.username ?? ''
|
||||
const password = subsAuth?.password ?? ''
|
||||
const status =
|
||||
enabled === true
|
||||
? i18n('Auth is ENABLED — these credentials are enforced.')
|
||||
: i18n(
|
||||
'Auth is DISABLED — subs is serving unauthenticated. These credentials will take effect when enabled.',
|
||||
)
|
||||
const url = subsAuth
|
||||
? `http://${subsAuth.username}:${subsAuth.password}@127.0.0.1:${SUBSPACES_UI_PORT}`
|
||||
: ''
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Show Subspaces Auth Credentials'),
|
||||
message: status,
|
||||
result: {
|
||||
type: 'group',
|
||||
value: [
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('SUBS_BASIC_AUTH_USER'),
|
||||
description: null,
|
||||
value: username,
|
||||
masked: false,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('SUBS_BASIC_AUTH_PASSWORD'),
|
||||
description: null,
|
||||
value: password,
|
||||
masked: true,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Connection URL'),
|
||||
description: null,
|
||||
value: url,
|
||||
masked: true,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { SUBSPACES_PROVER_PORT } from '../utils'
|
||||
|
||||
export const showSubsProverCredentials = sdk.Action.withoutInput(
|
||||
// id
|
||||
'show-subs-prover-credentials',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Show Subspaces Prover Auth Credentials'),
|
||||
description: i18n(
|
||||
'Display the HTTP basic auth username and password used in front of the Subspaces Prover. Returns blanks if no credentials have been set yet.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const [subsProverAuth, enabled] = await Promise.all([
|
||||
storeJson.read((s) => s.subsProverAuth).once(),
|
||||
storeJson.read((s) => s.subsProverAuthEnabled).once(),
|
||||
])
|
||||
|
||||
const username = subsProverAuth?.username ?? ''
|
||||
const password = subsProverAuth?.password ?? ''
|
||||
const status =
|
||||
enabled === true
|
||||
? i18n('Auth is ENABLED — these credentials are enforced.')
|
||||
: i18n(
|
||||
'Auth is DISABLED — subs-prover is serving unauthenticated. These credentials will take effect when enabled.',
|
||||
)
|
||||
const url = subsProverAuth
|
||||
? `http://${subsProverAuth.username}:${subsProverAuth.password}@127.0.0.1:${SUBSPACES_PROVER_PORT}`
|
||||
: ''
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Show Subspaces Prover Auth Credentials'),
|
||||
message: status,
|
||||
result: {
|
||||
type: 'group',
|
||||
value: [
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('SUBS_PROVER_BASIC_AUTH_USER'),
|
||||
description: null,
|
||||
value: username,
|
||||
masked: false,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('SUBS_PROVER_BASIC_AUTH_PASSWORD'),
|
||||
description: null,
|
||||
value: password,
|
||||
masked: true,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Connection URL'),
|
||||
description: null,
|
||||
value: url,
|
||||
masked: true,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,108 @@
|
||||
import { readFile } from 'fs/promises'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir } from '../utils'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
// Where to drop the uploaded PDF on the main volume. nacho (and any other
|
||||
// daemon that mounts /data) can read it from this path.
|
||||
const SUPPORT_PDF_PATH = `${dataDir}/support.pdf`
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
// `required: true` so the form refuses to submit without a file selected —
|
||||
// works around the StartOS quirk where an empty file picker submits `{}`
|
||||
// (which Value.file's nullable parser rejects).
|
||||
pdf: Value.file({
|
||||
name: i18n('Workshop PDF'),
|
||||
description: i18n(
|
||||
'PDF to write to /data/support.pdf (overwrites any existing file).',
|
||||
),
|
||||
warning: null,
|
||||
extensions: ['.pdf'],
|
||||
required: true,
|
||||
}),
|
||||
})
|
||||
|
||||
export const uploadSupportPdf = sdk.Action.withInput(
|
||||
// id
|
||||
'upload-support-pdf',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Upload Support PDF'),
|
||||
description: i18n(
|
||||
'Upload a PDF that replaces /data/support.pdf on the main volume (overwrites if it exists). Used by nacho as the workshop PDF.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// input
|
||||
inputSpec,
|
||||
|
||||
// prefill — file pickers can't be prefilled
|
||||
async ({ effects }) => {},
|
||||
|
||||
// run
|
||||
async ({ effects, input }) => {
|
||||
try {
|
||||
const pdfBuffer = await readFile(input.pdf.path)
|
||||
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'spaces' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: dataDir,
|
||||
readonly: false,
|
||||
}),
|
||||
'upload-support-pdf',
|
||||
(subc) =>
|
||||
subc.exec(
|
||||
[
|
||||
'sh',
|
||||
'-c',
|
||||
`cat > ${SUPPORT_PDF_PATH} && chmod 644 ${SUPPORT_PDF_PATH}`,
|
||||
],
|
||||
{ input: pdfBuffer, user: 'root' },
|
||||
),
|
||||
)
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not write support PDF: ${error}', {
|
||||
error:
|
||||
((res.stderr ?? '').toString() ||
|
||||
`exit ${res.exitCode}`).slice(0, 300),
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Workshop PDF written to /data/support.pdf (${size} bytes).',
|
||||
{ size: String(input.pdf.commitment.size) },
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not write support PDF: ${error}', {
|
||||
error: (e as Error)?.message ?? String(e),
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user