diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0a17f46 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git +.gitmodules \ No newline at end of file diff --git a/.github/Dockerfile.simplex b/.github/Dockerfile.simplex deleted file mode 100644 index db08ac5..0000000 --- a/.github/Dockerfile.simplex +++ /dev/null @@ -1,61 +0,0 @@ -ARG TAG=22.04 - -FROM ubuntu:${TAG} AS build - -### Build stage - -# Install curl and git and simplexmq dependencies -RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev - -# Specify bootstrap Haskell versions -ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3 -ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0 - -# Install ghcup -RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh - -# Adjust PATH -ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH" - -# Set both as default -RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \ - ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}" - -COPY ./simplex-chat /project -WORKDIR /project - -# Compile apps -RUN cabal update -RUN cabal build exe:smp-server -RUN cabal build exe:xftp-server - -# Create new path containing all files needed -RUN mkdir /final -WORKDIR /final - -# Strip the binaries from debug symbols to reduce size -RUN for app in smp-server xftp-server; do \ - bin=$(find / -name "$app" -type f -executable 2>/dev/null) && \ - if [ -n "$bin" ]; then \ - mv "$bin" ./ && \ - strip ./"$app" && \ - mv /project/scripts/docker/entrypoint-"$app" ./entrypoint; \ - else \ - echo "Binary $app not found"; \ - exit 1; \ - fi; \ -done - -### Final stage -FROM ubuntu:${TAG} - -# Install OpenSSL dependency -RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssl libnuma-dev netcat \ - && rm -rf /var/lib/apt/lists/* - -# Copy compiled apps from build stage -COPY --from=build /final /usr/local/bin/ - -# simplexmq requires using SIGINT to correctly preserve undelivered messages and restore them on restart -STOPSIGNAL SIGINT diff --git a/.github/workflows/build-simplex-server.yml b/.github/workflows/build-simplex-server.yml deleted file mode 100644 index f2a2241..0000000 --- a/.github/workflows/build-simplex-server.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Build and Publish SimpleX Docker Image - -on: - workflow_dispatch: - inputs: - simplex_version: - description: 'SimpleX Server version to build (e.g. v6.0.4 or v6.1.0-beta.1). Use "latest" to build the most recent version' - required: true - default: 'latest' - -jobs: - build: - runs-on: ubuntu-latest - - permissions: - contents: read - packages: write - id-token: write - - env: - REPO_URL: https://github.com/simplex-chat/simplex-chat.git - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: all - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - install: true - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Fetch latest tags from SimpleX repository - run: | - git clone --depth=1 --tags $REPO_URL - cd simplex-chat - # Find the latest tag, filtering out any non-version tags like 'v*' - LATEST_TAG=$(git tag -l --sort=-v:refname 'v*' | head -n 1) - echo "Latest tag found: $LATEST_TAG" - echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV - - - name: Determine version to build - run: | - if [ "${{ github.event.inputs.simplex_version }}" == "latest" ]; then - echo "Using latest tag: $LATEST_TAG" - echo "SIMPLEX_VERSION=$LATEST_TAG" >> $GITHUB_ENV - else - echo "Using specified version: ${{ github.event.inputs.simplex_version }}" - echo "SIMPLEX_VERSION=${{ github.event.inputs.simplex_version }}" >> $GITHUB_ENV - fi - - - name: Build and push Docker image for x86 and arm - run: | - echo "Building SimpleX version: $SIMPLEX_VERSION" - - REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') - - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --file .github/Dockerfile.simplex \ - --tag ghcr.io/${REPO_NAME}:$SIMPLEX_VERSION \ - --build-arg SIMPLEX_VERSION=$SIMPLEX_VERSION \ - --push \ - . - - - name: Image build complete - run: echo "Docker image has been built and published." diff --git a/.github/workflows/buildService.yml b/.github/workflows/buildService.yml index ab6e729..00ab89b 100644 --- a/.github/workflows/buildService.yml +++ b/.github/workflows/buildService.yml @@ -4,34 +4,20 @@ on: workflow_dispatch: pull_request: paths-ignore: ['*.md'] - branches: ['main', 'master'] + branches: ['master'] push: paths-ignore: ['*.md'] - branches: ['main', 'master'] + branches: ['master'] -jobs: - BuildPackage: - runs-on: ubuntu-latest - steps: - - name: Prepare StartOS SDK - uses: Start9Labs/sdk@v1 - - - name: Checkout services repository - uses: actions/checkout@v4 +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true - - name: Build the service package - id: build - run: | - git submodule update --init --recursive - start-sdk init - make - PACKAGE_ID=$(yq -oy ".id" manifest.*) - echo "package_id=$PACKAGE_ID" >> $GITHUB_ENV - printf "\n SHA256SUM: $(sha256sum ${PACKAGE_ID}.s9pk) \n" - shell: bash - - - name: Upload .s9pk - uses: actions/upload-artifact@v4 - with: - name: ${{ env.package_id }}.s9pk - path: ./${{ env.package_id }}.s9pk +jobs: + build: + if: github.event.pull_request.draft == false + uses: start9labs/shared-workflows/.github/workflows/buildService.yml@master + # with: + # FREE_DISK_SPACE: true + secrets: + DEV_KEY: ${{ secrets.DEV_KEY }} diff --git a/.github/workflows/releaseService.yml b/.github/workflows/releaseService.yml index 6cf91f2..17a0b91 100644 --- a/.github/workflows/releaseService.yml +++ b/.github/workflows/releaseService.yml @@ -6,67 +6,15 @@ on: - 'v*.*' jobs: - ReleasePackage: - runs-on: ubuntu-latest + release: + uses: start9labs/shared-workflows/.github/workflows/releaseService.yml@master + with: + # FREE_DISK_SPACE: true + REGISTRY: ${{ vars.REGISTRY }} + S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }} + secrets: + DEV_KEY: ${{ secrets.DEV_KEY }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} permissions: contents: write - steps: - - name: Prepare StartOS SDK - uses: Start9Labs/sdk@v1 - - - name: Checkout services repository - uses: actions/checkout@v4 - - - name: Build the service package - run: | - git submodule update --init --recursive - start-sdk init - make - - - name: Setting package ID and title from the manifest - id: package - run: | - echo "package_id=$(yq -oy ".id" manifest.*)" >> $GITHUB_ENV - echo "package_title=$(yq -oy ".title" manifest.*)" >> $GITHUB_ENV - shell: bash - - - name: Generate sha256 checksum - run: | - PACKAGE_ID=${{ env.package_id }} - printf "\n SHA256SUM: $(sha256sum ${PACKAGE_ID}.s9pk) \n" - sha256sum ${PACKAGE_ID}.s9pk > ${PACKAGE_ID}.s9pk.sha256 - shell: bash - - - name: Generate changelog - run: | - PACKAGE_ID=${{ env.package_id }} - echo "## What's Changed" > change-log.txt - yq -oy '.release-notes' manifest.* >> change-log.txt - echo "## SHA256 Hash" >> change-log.txt - echo '```' >> change-log.txt - sha256sum ${PACKAGE_ID}.s9pk >> change-log.txt - echo '```' >> change-log.txt - shell: bash - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - name: ${{ env.package_title }} ${{ github.ref_name }} - prerelease: true - body_path: change-log.txt - files: | - ./${{ env.package_id }}.s9pk - ./${{ env.package_id }}.s9pk.sha256 - - - name: Publish to Registry - env: - S9USER: ${{ secrets.S9USER }} - S9PASS: ${{ secrets.S9PASS }} - S9REGISTRY: ${{ secrets.S9REGISTRY }} - run: | - if [[ -z "$S9USER" || -z "$S9PASS" || -z "$S9REGISTRY" ]]; then - echo "Publish skipped: missing registry credentials." - else - start-sdk publish https://$S9USER:$S9PASS@$S9REGISTRY ${{ env.package_id }}.s9pk - fi diff --git a/.gitignore b/.gitignore index 5738189..4dad48c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,8 @@ -simplex.s9pk -image.tar -scripts/*.js +*.s9pk +startos/*.js +node_modules/ .DS_Store .vscode/ docker-images -/assets/.$explainer-source.drawio.dtmp -/assets/.$explainer-source.drawio.bkp -/_site +javascript +ncc-cache \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0b0940c..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "simplexmq"] - path = simplexmq - url = https://github.com/simplex-chat/simplexmq.git diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..023d761 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +## How the upstream version is pulled +- dockerTags in `startos/manifest/index.ts`: + - `simplexchat/smp-server:v` + - `simplexchat/xftp-server:v` +- Both must be updated together. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cbc38a7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# Contributing + +## Building and Development + +See the [StartOS Packaging Guide](https://docs.start9.com/packaging/) for complete environment setup and build instructions. + +### Quick Start + +```bash +# Install dependencies +npm ci + +# Build universal package +make +``` diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3a248f4..0000000 --- a/Dockerfile +++ /dev/null @@ -1,62 +0,0 @@ -ARG TAG=22.04 - -FROM ubuntu:${TAG} AS build - -### Build stage - -# Install curl and git and simplexmq dependencies -RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev - -# Specify bootstrap Haskell versions -ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3 -ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0 - -# Install ghcup -RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh - -# Adjust PATH -ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH" - -# Set both as default -RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \ - ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}" - -COPY ./simplexmq /project -WORKDIR /project - -# Compile apps -RUN cabal update -RUN cabal build exe:smp-server -RUN cabal build exe:xftp-server - -# Create new path containing all files needed -RUN mkdir /final -WORKDIR /final - -# Strip the binaries from debug symbols to reduce size -RUN for app in smp-server xftp-server; do \ - bin=$(find /project/dist-newstyle -name "$app" -type f -executable) && \ - mv "$bin" ./ && \ - strip ./"$app" &&\ - mv /project/scripts/docker/entrypoint-"$app" ./entrypoint; \ -done - -### Final stage -FROM ubuntu:${TAG} - -# Install OpenSSL dependency -RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssl libnuma-dev tini netcat \ - && rm -rf /var/lib/apt/lists/* - -# Copy compiled apps from build stage -COPY --from=build /final /usr/local/bin/ - -# simplexmq requires using SIGINT to correctly preserve undelivered messages and restore them on restart -STOPSIGNAL SIGINT - -ADD ./check-syn-ack.sh /usr/local/bin/check-syn-ack.sh -RUN chmod a+x /usr/local/bin/check-syn-ack.sh - -ADD ./docker_entrypoint.sh /usr/local/bin/docker_entrypoint.sh -RUN chmod a+x /usr/local/bin/docker_entrypoint.sh diff --git a/Makefile b/Makefile index 84d38ec..927e1fc 100644 --- a/Makefile +++ b/Makefile @@ -1,60 +1,3 @@ -PKG_ID := $(shell yq e ".id" manifest.yaml) -PKG_VERSION := $(shell yq e ".version" manifest.yaml) -TS_FILES := $(shell find ./ -name \*.ts) - -# delete the target of a rule if it has changed and its recipe exits with a nonzero exit status -.DELETE_ON_ERROR: - -all: verify - -verify: $(PKG_ID).s9pk - @start-sdk verify s9pk $(PKG_ID).s9pk - @echo " Done!" - @echo " Filesize: $(shell du -h $(PKG_ID).s9pk) is ready" - -install: -ifeq (,$(wildcard ~/.embassy/config.yaml)) - @echo; echo "You must define \"host: http://server-name.local\" in ~/.embassy/config.yaml config file first"; echo -else - start-cli package install $(PKG_ID).s9pk -endif - -clean: - rm -rf docker-images - rm -f $(PKG_ID).s9pk - rm -f scripts/*.js - -arm: - @rm -f docker-images/x86_64.tar - @ARCH=aarch64 $(MAKE) - -x86: - @rm -f docker-images/aarch64.tar - @ARCH=x86_64 $(MAKE) - -scripts/embassy.js: $(TS_FILES) - deno run --allow-read --allow-write --allow-env --allow-net scripts/bundle.ts - -docker-images/x86_64.tar: Dockerfile docker_entrypoint.sh check-syn-ack.sh -ifeq ($(ARCH),aarch64) -else - mkdir -p docker-images - docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) --platform=linux/amd64 -o type=docker,dest=docker-images/x86_64.tar . -endif - -docker-images/aarch64.tar: Dockerfile docker_entrypoint.sh check-syn-ack.sh -ifeq ($(ARCH),x86_64) -else - mkdir -p docker-images - docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) --platform=linux/arm64 -o type=docker,dest=docker-images/aarch64.tar . -endif - -$(PKG_ID).s9pk: manifest.yaml instructions.md LICENSE icon.png scripts/embassy.js docker-images/aarch64.tar docker-images/x86_64.tar -ifeq ($(ARCH),aarch64) - @echo "start-sdk: Preparing aarch64 package ..." -else ifeq ($(ARCH),x86_64) - @echo "start-sdk: Preparing x86_64 package ..." -else - @echo "start-sdk: Preparing Universal Package ..." -endif - @start-sdk pack +ARCHES := x86 +# overrides to s9pk.mk must precede the include statement +include s9pk.mk diff --git a/README.md b/README.md index a39e74d..e571f57 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,194 @@ -# Wrapper for SimpleX +

+ Project Logo +

-[SimpleX logo](https://simplex.chat/) +# SimpleX Server for StartOS -SimpleX is a highly secure and sovereign messenger. +This repository packages [SimpleX](https://github.com/simplex-chat/simplexmq) for StartOS. This document describes what makes this package different from a default SimpleX deployment. -Currently the packaging process is somewhat involved, because SimpleX do not publish official multi-architecture images on Docker Hub. I was able to get support for ARM builds [merged](https://github.com/simplex-chat/simplexmq/pull/679), however as of 2023-06-07, SimpleX have an [open issue](https://github.com/simplex-chat/simplexmq/issues/740) for not publishing ARM builds to Docker Hub. All of this is further complicated by the fact that Haskell cross compilation of SimpleXMQ is fairly broken - even using 30GB of RAM on an M1 Pro was not enough resources. +For general SimpleX usage and features, see the [upstream documentation](https://simplex.chat/docs/guide/readme.html). -This wrapper therefore uses a multi-architecture Docker image hosted on my personal Docker Hub. This multi-architecture image can be created as follows: +## How This Differs from Upstream -- Check out the SimpleXMQ repo on an ARM machine - `git@github.com:simplex-chat/simplexmq.git` -- Build the docker image using their instructions, which, as of writing, was `DOCKER_BUILDKIT=1 docker buildx build --platform linux/arm64 -t shyfire131/smp-server:arm --build-arg APP="smp-server" --build-arg APP_PORT="5223" . --push` -- Then, mirror the official AMD64 image as follows: -``` -docker pull simplexchat/smp-server -docker tag simplexchat/smp-server /smp-server:amd64 -docker push /smp-server:amd64 -``` -- At this point you need to do some manifest hacking: -- `docker buildx imagetools create -t shyfire131/smp-server:latest shyfire131/smp-server:arm shyfire131/smp-server:amd64` +This package runs both SMP (messaging) and XFTP (file transfer) servers with auto-generated credentials and pre-configured defaults. Connection URLs with embedded fingerprints and passwords are automatically generated for easy client configuration. + +## Container Runtime + +This package runs **2 containers**: + +| Container | Image | Purpose | +|-----------|-------|---------| +| smp | `simplexchat/smp-server` | SimpleX Messaging Protocol server | +| xftp | `simplexchat/xftp-server` | SimpleX File Transfer Protocol server | + +**Note:** Currently x86_64 only. ARM64 support planned for a future release. + +## Volumes + +| Volume | Contents | Backed Up | +|--------|----------|-----------| +| `smp-configs` | SMP server configuration, keys, fingerprint | Yes | +| `smp-state` | SMP message queues and state | Yes | +| `xftp-configs` | XFTP server configuration, keys, fingerprint | Yes | +| `xftp-state` | XFTP file metadata and state | Yes | +| `xftp-files` | Uploaded file storage | Yes | + +## Install Flow + +On installation: +1. Generates shared authentication password +2. Initializes SMP server (`smp-server init`) - creates keys and fingerprint +3. Initializes XFTP server (`xftp-server init`) - creates keys and fingerprint +4. Writes configuration files with defaults + +## Configuration Management -You can then reference `shyfire131/smp-server` in your Dockerfile, of course replacing shyfire131 with your own username in all of the above. +### Auto-Configured Settings -Other than that, building the .s9pk is fairly straightforward and doesn't need any special dependencies. -## Cloning +**SMP Server:** -Clone the project locally: +| Setting | Value | Purpose | +|---------|-------|---------| +| Port | 5223 (and 443) | Client connections | +| Control port | 5224 | Server management | +| Message expiry | 365 days | Auto-delete old messages | +| Notification expiry | 168 hours (7 days) | Push notification cleanup | +| Store queues | Memory | Performance optimization | +| Password | Auto-generated | Queue creation auth | +**XFTP Server:** + +| Setting | Value | Purpose | +|---------|-------|---------| +| Port | 5225 | File transfers | +| Control port | 5226 | Server management | +| Storage quota | 10 GB | Maximum file storage | +| File expiry | 168 hours (7 days) | Auto-delete uploaded files | +| Password | Shared with SMP | File upload auth | + +### User-Configurable Settings + +No configuration actions are provided. Advanced users can modify the INI files directly in the config volumes. + +## Network Interfaces + +| Interface | Type | Port | Scheme | Description | +|-----------|------|------|--------|-------------| +| SMP Server | api | 5223 | `smp://` | Messaging protocol | +| XFTP Server | api | 5225 | `xftp://` | File transfer protocol | + +Both interfaces are masked and include credentials in the connection URL format: ``` -git clone git@github.com:shyfire131/simplex-startos.git -cd simplex-startos -make +smp://:@:5223 +xftp://:@:5225 ``` -## Testing performed +## Actions + +None. Server configuration is automatic. + +## Dependencies + +None. SimpleX servers are standalone. + +## Backups -- [X] Smoke testing - DMs work on macOS, iOS and Android -- [X] Continuity testing - Server config persists across service restarts -- [X] DR testing - Backups restore successfully +All server data is backed up: +- `smp-configs` - server identity and settings +- `smp-state` - message queues +- `xftp-configs` - server identity and settings +- `xftp-state` - file metadata +- `xftp-files` - uploaded files -## Roadmap -- [ ] Support for XFTP Server (sending large attachments, will either be a standalone Service or bundled once multi-container services are supported) -- [ ] Server statistics (`log_stats: on` in `smp-server.ini`) -- [ ] Clearnet support will obviously be a superpower once available -- [ ] Use official multi-architecture Docker Hub once SimpleX team fixes that -- [ ] Configurable message retention period +**Important:** The server fingerprint is part of your identity. Losing the config volumes means clients must reconnect to a "new" server. + +## Health Checks + +| Check | Method | Success Condition | +|-------|--------|-------------------| +| SMP Server | Port listening | Port 5223 responds | +| XFTP Server | Port listening | Port 5225 responds | + +## Using with SimpleX Chat + +1. Copy the SMP server URL from StartOS interfaces +2. In SimpleX Chat app: Settings → Network & servers → SMP servers +3. Add your server URL +4. Repeat for XFTP server URL under "XFTP servers" + +Your messages and files will now route through your own servers. + +## Limitations + +1. **x86_64 only**: ARM64 (aarch64) not yet supported +2. **No web UI**: Server administration is config-file only +3. **Fixed storage quota**: XFTP limited to 10GB (requires config edit to change) +4. **No stats dashboard**: Prometheus metrics available but not exposed + +## What's Unchanged + +- Full SMP/XFTP protocol support +- End-to-end encryption +- Message queue functionality +- File transfer capabilities +- Client compatibility + +--- + +## Quick Reference (YAML) + +```yaml +package_id: simplex +arch: x86_64 only + +containers: + - name: smp + image: simplexchat/smp-server + ports: [5223, 5224] + - name: xftp + image: simplexchat/xftp-server + ports: [5225, 5226] + +volumes: + smp-configs: + backup: true + smp-state: + backup: true + xftp-configs: + backup: true + xftp-state: + backup: true + xftp-files: + backup: true + +interfaces: + smp: + type: api + port: 5223 + scheme: smp:// + includes: fingerprint, password + xftp: + type: api + port: 5225 + scheme: xftp:// + includes: fingerprint, password + +actions: [] + +dependencies: [] + +auto_configure: + - server fingerprints (generated) + - shared password (generated) + - message expiry: 365 days + - file expiry: 7 days + - storage quota: 10GB + +health_checks: + - name: SMP Server + method: port_listening + port: 5223 + - name: XFTP Server + method: port_listening + port: 5225 +``` diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..4fbfc10 --- /dev/null +++ b/assets/README.md @@ -0,0 +1 @@ +Use the `/assets` directory to include additional files or scripts needed by your service. diff --git a/assets/compat/backup.sh b/assets/compat/backup.sh deleted file mode 100755 index 8b3436f..0000000 --- a/assets/compat/backup.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -set -e - -mkdir -p /mnt/backup/etc -mkdir -p /mnt/backup/log -compat duplicity create /mnt/backup/etc /etc/opt/simplex -compat duplicity create /mnt/backup/etc /etc/opt/simplex-xftp -compat duplicity create /mnt/backup/log /var/opt/simplex \ No newline at end of file diff --git a/assets/compat/restore.sh b/assets/compat/restore.sh deleted file mode 100755 index e9fdcef..0000000 --- a/assets/compat/restore.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -set -e - -compat duplicity restore /mnt/backup/etc /etc/opt/simplex -compat duplicity restore /mnt/backup/etc /etc/opt/simplex-xftp -compat duplicity restore /mnt/backup/log /var/opt/simplex \ No newline at end of file diff --git a/check-syn-ack.sh b/check-syn-ack.sh deleted file mode 100755 index 90f28b5..0000000 --- a/check-syn-ack.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -read DURATION -if [ "$DURATION" -le 10000 ]; then - exit 60 -else - output=$(nc -zv 127.0.0.1 5223 2>&1) - - expected_output="Connection to 127.0.0.1 5223 port [tcp/*] succeeded!" - - if [ "$output" == "$expected_output" ]; then - exit 0 - else - echo "The smp-server process does not seem to be responding to connection requests" >&2 - exit 1 - fi -fi \ No newline at end of file diff --git a/docker_entrypoint.sh b/docker_entrypoint.sh deleted file mode 100755 index 2b2e367..0000000 --- a/docker_entrypoint.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env sh - -set -e -printf "\n\n [i] Starting SimpleX ...\n\n" -confd="/etc/opt/simplex" -xftp="/etc/opt/simplex-xftp" -logd="/var/opt/simplex" -# Check if smp-server has been initialized -if [ ! -f "$confd/smp-server.ini" ]; then - # Set a 15 digit server password. See the comments in smp-server.ini for a description of what this does - export PASS=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15) - # Init certificates and configs - smp-server init -y -l --password $PASS -else - export PASS=$(grep -i "^create_password" $confd/smp-server.ini | awk -F ':' '{print $2}' | awk '{$1=$1};1') -fi - -# Function to add or update a section in the smp-server.ini config file -add_or_update_section() { - local file="$1" - local section="$2" - local content="$3" - - if grep -q "^\[$section\]" "$file"; then - # Section exists, update it - awk -v section="$section" -v content="$content" ' - BEGIN { in_section = 0; printed = 0 } - /^\[/ { if (in_section && !printed) { print content; printed = 1 }; in_section = 0 } - /^\['"$section"'\]/ { in_section = 1; print; print content; printed = 1; next } - { if (!in_section || !printed) print } - END { if (in_section && !printed) print content } - ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" - echo "$section section updated in $file" - else - # Section doesn't exist, add it - echo "\n[$section]\n$content" >> "$file" - echo "$section section added to $file" - fi -} -# Add or update INFORMATION section -add_or_update_section "$confd/smp-server.ini" "INFORMATION" "source_code: https://github.com/simplex-chat/simplexmq" - -# Add or update PROXY section -add_or_update_section "$confd/smp-server.ini" "PROXY" "socks_proxy: 172.18.0.1:9050\nsocks_mode: onion" - -# Add or update WEB section -add_or_update_section "$confd/smp-server.ini" "WEB" "static_path: /var/opt/simplex/www" - -# Add or update TRANSPORT section with correct port -add_or_update_section "$confd/smp-server.ini" "TRANSPORT" "host: \nport: 5223\nlog_tls_errors: off\nwebsockets: off" - -# Check if xftp-server has been initialized -if [ ! -f "$xftp/file-server.ini" ]; then - # Init certificates and configs - mkdir -p /root/xftp - xftp-server init -q '10gb' -p /root/xftp/ - - else - echo "All good! - XFTP initialized" -fi - -# Backup store log just in case -# -# Uses the UTC (universal) time zone and this -# format: YYYY-mm-dd'T'HH:MM:SS -# year, month, day, letter T, hour, minute, second -# -# This is the ISO 8601 format without the time zone at the end. -# -_file="${logd}/smp-server-store.log" -if [ -f "${_file}" ]; then - _backup_extension="$(date -u '+%Y-%m-%dT%H:%M:%S')" - cp -v -p "${_file}" "${_file}.${_backup_extension:-date-failed}" - unset -v _backup_extension -fi -unset -v _file - -TOR_ADDRESS=$(sed -n -e 's/^tor-address: \(.*\)/\1/p' /root/start9/config.yaml) -XFTP_ADDRESS=$(sed -n -e 's/^xftp-address: \(.*\)/\1/p' /root/start9/config.yaml) -SMP_FINGERPRINT=$(cat $confd/fingerprint) -XFTP_FINGERPRINT=$(cat $xftp/fingerprint) - -SMP_URL="smp://$SMP_FINGERPRINT:$PASS@$TOR_ADDRESS" -XFTP_URL="xftp://$XFTP_FINGERPRINT@$XFTP_ADDRESS" -mkdir -p /root/start9 - -cat << EOF > /root/start9/stats.yaml ---- -version: 2 -data: - SimpleX SMP Server Address: - type: string - value: $SMP_URL - description: Your SMP Server address, used in client applications. - copyable: true - qr: true - masked: true - SimpleX XFTP Server Address: - type: string - value: $XFTP_URL - description: Your XFTP Server address, used in client applications. - copyable: true - qr: true - masked: true -EOF - -# Run smp-server and xftp-server as background processes -# Set up a trap to catch INT signal for graceful shutdown - -_term() { - echo "Caught INT signal!" - kill -INT "$smp_process" 2>/dev/null - kill -INT "$xftp_process" 2>/dev/null -} - -smp-server start +RTS -N -RTS & -smp_process=$! - -xftp-server start +RTS -N -RTS & -xftp_process=$! - -trap _term INT - -wait $xftp_process $smp_process diff --git a/icon.png b/icon.png deleted file mode 100644 index 9c9a5c0..0000000 Binary files a/icon.png and /dev/null differ diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..c0dfcf3 --- /dev/null +++ b/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/instructions.md b/instructions.md deleted file mode 100644 index 828a0ca..0000000 --- a/instructions.md +++ /dev/null @@ -1,85 +0,0 @@ -# Getting started with SimpleX - -## About - -Your SimpleX Server comes with a [SMP server](https://simplex.chat/docs/server.html) for sending/receiving messages and an [XFTP server](https://simplex.chat/docs/xftp-server.html) for transferring files and media. - -In SimpleX, participants choose which servers to use for receiving messages and files. I receive through my private server, and you receive through your private server. Or we could both receive through the same private server. Or we could each use different public servers. - -SimpleX servers function as queues. Servers only store messages until a client retrieves them, after which they are deleted. This means message histories are stored on client devices, so it is client devices that must be backed up to prevent loss of messages. - -For detailed instructions on using SimpleX, check out the [official docs](https://simplex.chat/docs/guide/readme.html). - -## Initial setup - -1. Ensure your SimpleX Server is running, and health checks are passing. - -1. Follow instructions in the [Start9 docs](https://docs.start9.com) for running Tor on your client device (phone/laptop). - -1. Download and install the [SimpleX app](https://simplex.chat/) on your client device. - -1. During initial setup of the app, if you choose to create a profile, _do not_ create a SimpleX address. You can do this later, once you have configured your servers, since your server is included in your address. - -## Creating a fully anonymous profile - -The purpose of this profile is _total_ anonymity. For this profile, you will receive messages through your self-hosted server over Tor, and you only permit sending messages to other .onion servers. - -For sending messages to non-Tor servers, we recommend creating a separate profile entirely. - -SimpleX can be configured many different ways, depending on your threat model and privacy goals, each with different tradeoffs. - -### Enable and enforce Tor - -1. If you did not create a profile during initial setup, create one now. - -1. Navigate to `Settings > Network & servers`. - -1. For iOS users: Skip this step. For other platforms: Enable "Use SOCKS proxy". - -1. Set `Use .onion hosts` to "Required" (this might be under `SOCKS proxy settings` or `Advanced network settings`). This means that this profile can _only_ send messages to other .onion servers. - -1. Finally, in `Advanced Network Settings`, set `Private Routing` to "Never". - -### Connect your SMP server for messages - -1. Navigate to `Settings > Network & servers > Message servers`. - -1. You will see some default receiving servers (e.g. smp1, smp2, smp3). It is recommended you _delete them all_ to ensure you do not accidentally use them in the future. If you choose not to delete them, at least disable the "Use for new connections" setting on each of them. This will prevent them from being used without your explicit instruction. - -1. Click "Add Server". - -1. Tap "Scan QR Code". - -1. Scan your SMP Server Address QR code, located in StartOS UI under `SimpleX Server > Properties > SimpleX SMP Server Address`. - -1. Click on the newly added server. - -1. Click "Test server" and wait for the test to pass. - -1. Enable "Use for new connections". - -1. Navigate back and click "Save servers". - -### Connect your XFTP server for media and files - -1. Navigate to `Settings > Network & servers > Media and file servers`. - -1. You will see some default receiving servers (e.g. xftp1, xftp2, xftp3). It is recommended you _delete them all_ to ensure you do not accidentally use them in the future. If you choose not to delete them, at least disable the "Use for new connections" setting on each of them. This will prevent them from being used without your explicit instruction. - -1. Click "Add server". - -1. Tap "Scan QR Code". - -1. Scan your XFTP Server Address QR code, located in StartOS UI under `SimpleX Server > Properties > SimpleX XFTP Server Address`. - -1. Click on the newly added server. - -1. Click "Test server" and wait for the test to pass. - -1. Enable "Use for new connections". - -1. Navigate back and click "Save servers". - -### Create a SimpleX address (optional) - -You can now create a SimpleX Address if you want, but you should _not_ share the address publicly, as it will link the .onion URL to your identity. Remember, the purpose of this profile is total anonymity. diff --git a/manifest.yaml b/manifest.yaml deleted file mode 100644 index d87ae9d..0000000 --- a/manifest.yaml +++ /dev/null @@ -1,131 +0,0 @@ -id: simplex -title: "SimpleX Server" -version: 6.2.0.7 -release-notes: | - - Updated to the latest upstream code with notable changes: - * journal storage for messages (BETA). - * prevent race condition when deleting queue and to avoid "orphan" messages. - * Full change log available [here](https://github.com/simplex-chat/simplexmq/blob/master/CHANGELOG.md#620) -license: MIT -wrapper-repo: "https://github.com/Start9Labs/simplex-startos" -upstream-repo: "https://github.com/simplex-chat/simplexmq/" -support-site: "https://github.com/simplex-chat/simplexmq/issues" -marketing-site: "https://simplex.chat/" -donation-url: "https://github.com/simplex-chat/simplex-chat#help-us-with-donations" -build: ["make"] -description: - short: An instant messenger without user IDs - long: | - SimpleX is a chat client that radically improves your privacy. It puts you in control of how people connect with you. -assets: - license: LICENSE - icon: icon.png - instructions: instructions.md -main: - type: docker - image: main - entrypoint: "docker_entrypoint.sh" - args: [] - mounts: - main: /root/ - conf: /etc/opt/simplex - xftp: /etc/opt/simplex-xftp - log: /var/opt/simplex -hardware-requirements: - arch: - - x86_64 - - aarch64 -health-checks: - api: - name: Listening - success-message: SimpleX Server is alive and accepting incoming connections - type: docker - image: main - system: false - entrypoint: check-syn-ack.sh - args: [] - mounts: {} - io-format: json - inject: true -config: - get: - type: script - set: - type: script -properties: - type: script -volumes: - main: - type: data - compat: - type: assets - conf: - type: data - xftp: - type: data - log: - type: data -interfaces: - main: - name: SimpleX SMP Interface - description: Do not use the URL below. Instead, look in the Properties section to get your SimpleX server address. - tor-config: - port-mapping: - 5223: "5223" - ui: false - protocols: - - tcp - xftp: - name: SimpleX XFTP Interface - description: Do not use the URL below. Instead, look in the Properties section to get your SimpleX server address. - tor-config: - port-mapping: - 443: "443" - ui: false - protocols: - - tcp -dependencies: {} -backup: - create: - type: docker - image: compat - system: true - entrypoint: /assets/backup.sh - args: - - duplicity - - create - - /mnt/backup - - /root/start9 - mounts: - BACKUP: /mnt/backup - compat: /assets - main: /root/start9 - conf: /etc/opt/simplex - xftp: /etc/opt/simplex-xftp - log: /var/opt/simplex - restore: - type: docker - image: compat - system: true - entrypoint: /assets/restore.sh - args: - - duplicity - - restore - - /mnt/backup - - /root/start9 - mounts: - BACKUP: /mnt/backup - compat: /assets - main: /root/start9 - conf: /etc/opt/simplex - xftp: /etc/opt/simplex-xftp - log: /var/opt/simplex -migrations: - from: - "*": - type: script - args: ["from"] - to: - "*": - type: script - args: ["to"] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4e1fa5a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,350 @@ +{ + "name": "hello-world-startos", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hello-world-startos", + "dependencies": { + "@start9labs/start-sdk": "^0.4.0-beta.66", + "tldts": "^7.0.19" + }, + "devDependencies": { + "@types/node": "^22.18.6", + "@vercel/ncc": "^0.38.4", + "prettier": "^3.6.2", + "typescript": "^5.9.2" + } + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@start9labs/start-sdk": { + "version": "0.4.0-beta.66", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.4.0-beta.66.tgz", + "integrity": "sha512-fsp7d3nqFapLohaKbNk1frW8fbVmjr2CBehVXVUk6JS/LSerogqlAYUFE5LKp5vJoTkuSnZbF5np4m+oxCmwCw==", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@noble/curves": "^1.8.2", + "@noble/hashes": "^1.7.2", + "@types/ini": "^4.1.1", + "deep-equality-data-structures": "^2.0.0", + "fast-xml-parser": "^5.5.6", + "ini": "^5.0.0", + "isomorphic-fetch": "^3.0.0", + "mime": "^4.0.7", + "yaml": "^2.7.1", + "zod": "^4.3.6", + "zod-deep-partial": "^1.2.0" + } + }, + "node_modules/@types/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/deep-equality-data-structures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz", + "integrity": "sha512-qgrUr7MKXq7VRN+WUpQ48QlXVGL0KdibAoTX8KRg18lgOgqbEKMAW1WZsVCtakY4+XX42pbAJzTz/DlXEFM2Fg==", + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.9", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz", + "integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/strnum": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", + "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tldts": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.27" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-deep-partial": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.4.4.tgz", + "integrity": "sha512-aWkPl7hVStgE01WzbbSxCgX4O+sSpgt8JOjvFUtMTF75VgL6MhWQbiZi+AWGN85SfSTtI9gsOtL1vInoqfDVaA==", + "license": "MIT", + "peerDependencies": { + "zod": "^4.1.13" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e48448 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "hello-world-startos", + "scripts": { + "build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript", + "prettier": "prettier --write startos", + "check": "tsc --noEmit" + }, + "dependencies": { + "@start9labs/start-sdk": "1.0.0", + "tldts": "^7.0.19" + }, + "devDependencies": { + "@types/node": "^22.18.6", + "@vercel/ncc": "^0.38.4", + "prettier": "^3.6.2", + "typescript": "^5.9.2" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": true + } +} diff --git a/s9pk.mk b/s9pk.mk new file mode 100644 index 0000000..45ebfc9 --- /dev/null +++ b/s9pk.mk @@ -0,0 +1,127 @@ +# ** Plumbing. DO NOT EDIT **. +# This file is imported by ./Makefile. Make edits there + +PACKAGE_ID := $(shell awk -F"'" '/id:/ {print $$2}' startos/manifest/index.ts) +INGREDIENTS := $(shell start-cli s9pk list-ingredients 2>/dev/null) +ARCHES ?= x86 arm riscv +TARGETS ?= arches +ifdef VARIANT +BASE_NAME := $(PACKAGE_ID)_$(VARIANT) +else +BASE_NAME := $(PACKAGE_ID) +endif + +.PHONY: all arches aarch64 x86_64 riscv64 arm arm64 x86 riscv arch/* clean install check-deps check-init package ingredients +.DELETE_ON_ERROR: +.SECONDARY: + +define SUMMARY + @manifest=$$(start-cli s9pk inspect $(1) manifest); \ + size=$$(du -h $(1) | awk '{print $$1}'); \ + title=$$(printf '%s' "$$manifest" | jq -r .title); \ + version=$$(printf '%s' "$$manifest" | jq -r .version); \ + arches=$$(printf '%s' "$$manifest" | jq -r '[.images[].arch // []] | flatten | unique | join(", ")'); \ + sdkv=$$(printf '%s' "$$manifest" | jq -r .sdkVersion); \ + gitHash=$$(printf '%s' "$$manifest" | jq -r .gitHash | sed -E 's/(.*-modified)$$/\x1b[0;31m\1\x1b[0m/'); \ + printf "\n"; \ + printf "\033[1;32m✅ Build Complete!\033[0m\n"; \ + printf "\n"; \ + printf "\033[1;37m📦 $$title\033[0m \033[36mv$$version\033[0m\n"; \ + printf "───────────────────────────────\n"; \ + printf " \033[1;36mFilename:\033[0m %s\n" "$(1)"; \ + printf " \033[1;36mSize:\033[0m %s\n" "$$size"; \ + printf " \033[1;36mArch:\033[0m %s\n" "$$arches"; \ + printf " \033[1;36mSDK:\033[0m %s\n" "$$sdkv"; \ + printf " \033[1;36mGit:\033[0m %s\n" "$$gitHash"; \ + echo "" +endef + +all: $(TARGETS) + +arches: $(ARCHES) + +universal: $(BASE_NAME).s9pk + $(call SUMMARY,$<) + +arch/%: $(BASE_NAME)_%.s9pk + $(call SUMMARY,$<) + +x86 x86_64: arch/x86_64 +arm arm64 aarch64: arch/aarch64 +riscv riscv64: arch/riscv64 + +$(BASE_NAME).s9pk: $(INGREDIENTS) .git/HEAD .git/index + @$(MAKE) --no-print-directory ingredients + @echo " Packing '$@'..." + start-cli s9pk pack -o $@ + +$(BASE_NAME)_%.s9pk: $(INGREDIENTS) .git/HEAD .git/index + @$(MAKE) --no-print-directory ingredients + @echo " Packing '$@'..." + start-cli s9pk pack --arch=$* -o $@ + +ingredients: $(INGREDIENTS) + @echo " Re-evaluating ingredients..." + +install: | check-deps check-init + @HOST=$$(awk -F'/' '/^host:/ {print $$3}' ~/.startos/config.yaml); \ + if [ -z "$$HOST" ]; then \ + echo "Error: You must define \"host: http://server-name.local\" in ~/.startos/config.yaml"; \ + exit 1; \ + fi; \ + S9PK=$$(ls -t *.s9pk 2>/dev/null | head -1); \ + if [ -z "$$S9PK" ]; then \ + echo "Error: No .s9pk file found. Run 'make' first."; \ + exit 1; \ + fi; \ + printf "\n🚀 Installing %s to %s ...\n" "$$S9PK" "$$HOST"; \ + start-cli package install -s "$$S9PK" + +publish: | all + @REGISTRY=$$(awk -F'/' '/^registry:/ {print $$3}' ~/.startos/config.yaml); \ + if [ -z "$$REGISTRY" ]; then \ + echo "Error: You must define \"registry: https://my-registry.tld\" in ~/.startos/config.yaml"; \ + exit 1; \ + fi; \ + S3BASE=$$(awk -F'/' '/^s9pk-s3base:/ {print $$3}' ~/.startos/config.yaml); \ + if [ -z "$$S3BASE" ]; then \ + echo "Error: You must define \"s3base: https://s9pks.my-s3-bucket.tld\" in ~/.startos/config.yaml"; \ + exit 1; \ + fi; \ + command -v s3cmd >/dev/null || \ + (echo "Error: s3cmd not found. It must be installed to publish using s3." && exit 1); \ + printf "\n🚀 Publishing to %s; indexing on %s ...\n" "$$S3BASE" "$$REGISTRY"; \ + for s9pk in *.s9pk; do \ + age=$$(( $$(date +%s) - $$(stat -c %Y "$$s9pk") )); \ + if [ "$$age" -gt 3600 ]; then \ + printf "\033[1;33m⚠️ %s is %d minutes old. Publish anyway? [y/N] \033[0m" "$$s9pk" "$$((age / 60))"; \ + read -r ans; \ + case "$$ans" in [yY]*) ;; *) echo "Skipping $$s9pk"; continue ;; esac; \ + fi; \ + start-cli s9pk publish "$$s9pk"; \ + done + +check-deps: + @command -v start-cli >/dev/null || \ + (echo "Error: start-cli not found. Please see https://docs.start9.com/latest/developer-guide/sdk/installing-the-sdk" && exit 1) + @command -v npm >/dev/null || \ + (echo "Error: npm not found. Please install Node.js and npm." && exit 1) + +check-init: + @if [ ! -f ~/.startos/developer.key.pem ]; then \ + echo "Initializing StartOS developer environment..."; \ + start-cli init-key; \ + fi + +javascript/index.js: $(shell find startos -type f) tsconfig.json node_modules + npm run build + +node_modules: package-lock.json + npm ci + +package-lock.json: package.json + npm i + +clean: + @echo "Cleaning up build artifacts..." + @rm -rf $(PACKAGE_ID).s9pk $(PACKAGE_ID)_x86_64.s9pk $(PACKAGE_ID)_aarch64.s9pk $(PACKAGE_ID)_riscv64.s9pk javascript node_modules diff --git a/scripts/bundle.ts b/scripts/bundle.ts deleted file mode 100644 index 07cbf3a..0000000 --- a/scripts/bundle.ts +++ /dev/null @@ -1,6 +0,0 @@ -// scripts/bundle.ts -import { bundle } from "https://deno.land/x/emit@0.40.0/mod.ts"; - -const result = await bundle("scripts/embassy.ts"); - -await Deno.writeTextFile("scripts/embassy.js", result.code); diff --git a/scripts/deps.ts b/scripts/deps.ts deleted file mode 100644 index 6519c00..0000000 --- a/scripts/deps.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "https://deno.land/x/embassyd_sdk@v0.3.3.0.5/mod.ts"; -export * from "https://deno.land/x/embassyd_sdk@v0.3.3.0.5/util.ts"; \ No newline at end of file diff --git a/scripts/embassy.ts b/scripts/embassy.ts deleted file mode 100644 index 74f56eb..0000000 --- a/scripts/embassy.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { setConfig } from "./procedures/setConfig.ts"; -export { getConfig } from "./procedures/getConfig.ts"; -export { properties } from "./procedures/properties.ts"; -export { migration } from "./procedures/migrations.ts"; -export { main } from "./procedures/main.ts"; \ No newline at end of file diff --git a/scripts/procedures/getConfig.ts b/scripts/procedures/getConfig.ts deleted file mode 100644 index 2e4503c..0000000 --- a/scripts/procedures/getConfig.ts +++ /dev/null @@ -1,24 +0,0 @@ -// To utilize the default config system built, this file is required. It defines the *structure* of the configuration file. These structured options display as changeable UI elements within the "Config" section of the service details page in the Embassy UI. - -import { compat, types as T } from "../deps.ts"; - -export const getConfig: T.ExpectedExports.getConfig = compat.getConfig({ - "tor-address": { - "name": "SMP Tor Address", - "description": "The Tor address for SMP Server", - "type": "pointer", - "subtype": "package", - "package-id": "simplex", - "target": "tor-address", - "interface": "main" - }, - "xftp-address": { - "name": "XFTP Tor Address", - "description": "The Tor address for XFTP Server", - "type": "pointer", - "subtype": "package", - "package-id": "simplex", - "target": "tor-address", - "interface": "xftp" - } - }); diff --git a/scripts/procedures/main.ts b/scripts/procedures/main.ts deleted file mode 100644 index a426122..0000000 --- a/scripts/procedures/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { types as T, util } from "../deps.ts"; - -export const main = async (effects: T.Effects) => { - // args defaulted to [] - not necessary to include if empty - await effects.runDaemon({ command: "docker_entrypoint.sh", args: [] }).wait(); - return util.ok; -} \ No newline at end of file diff --git a/scripts/procedures/migrations.ts b/scripts/procedures/migrations.ts deleted file mode 100644 index 55a55cf..0000000 --- a/scripts/procedures/migrations.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { compat, types as T } from "../deps.ts"; - -export const migration: T.ExpectedExports.migration = - compat.migrations.fromMapping( - { - "5.8.2": { - up: compat.migrations.updateConfig( - (config: any) => { - config["xftp-address"]; - - return config; - }, - true, - { version: "5.8.2", type: "up" } - ), - down: compat.migrations.updateConfig( - (config: any) => { - delete config["xftp-address"]; - - return config; - }, - true, - { version: "5.8.2", type: "down" } - ), - }, - }, - "6.2.0.7" - ); diff --git a/scripts/procedures/properties.ts b/scripts/procedures/properties.ts deleted file mode 100644 index dff99aa..0000000 --- a/scripts/procedures/properties.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { compat, types as T } from "../deps.ts"; - -export const properties: T.ExpectedExports.properties = compat.properties; diff --git a/scripts/procedures/setConfig.ts b/scripts/procedures/setConfig.ts deleted file mode 100644 index 2cea41b..0000000 --- a/scripts/procedures/setConfig.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { compat, types as T } from "../deps.ts"; - -// deno-lint-ignore require-await -export const setConfig: T.ExpectedExports.setConfig = async ( - effects: T.Effects, - newConfig: T.Config, -) => { - return compat.setConfig(effects, newConfig); -}; \ No newline at end of file diff --git a/simplexmq b/simplexmq deleted file mode 160000 index 79e9447..0000000 --- a/simplexmq +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 79e9447b73cc315ce35042b0a5f210c07ea39b07 diff --git a/startos/actions/index.ts b/startos/actions/index.ts new file mode 100644 index 0000000..c82cb95 --- /dev/null +++ b/startos/actions/index.ts @@ -0,0 +1,3 @@ +import { sdk } from '../sdk' + +export const actions = sdk.Actions.of() diff --git a/startos/backups.ts b/startos/backups.ts new file mode 100644 index 0000000..729c896 --- /dev/null +++ b/startos/backups.ts @@ -0,0 +1,12 @@ +import { sdk } from './sdk' + +export const { createBackup, restoreInit } = sdk.setupBackups( + async ({ effects }) => + sdk.Backups.ofVolumes( + 'smp-configs', + 'smp-state', + 'xftp-configs', + 'xftp-state', + 'xftp-files', + ), +) diff --git a/startos/dependencies.ts b/startos/dependencies.ts new file mode 100644 index 0000000..7221c4b --- /dev/null +++ b/startos/dependencies.ts @@ -0,0 +1,5 @@ +import { sdk } from './sdk' + +export const setDependencies = sdk.setupDependencies( + async ({ effects }) => ({}), +) diff --git a/startos/fileModels/fileServer.ini.ts b/startos/fileModels/fileServer.ini.ts new file mode 100644 index 0000000..f1d6935 --- /dev/null +++ b/startos/fileModels/fileServer.ini.ts @@ -0,0 +1,79 @@ +import { FileHelper, z } from '@start9labs/start-sdk' +import { xftpConfigDefaults } from '../utils' +import { sdk } from '../sdk' + +const { STORE_LOG, AUTH, TRANSPORT, FILES, INACTIVE_CLIENTS } = + xftpConfigDefaults + +const storeLogSchema = z.object({ + enable: z.enum(['on', 'off']).catch(STORE_LOG.enable), + expire_files_hours: z + .number() + .int() + .nonnegative() + .optional() + .catch(STORE_LOG.expire_files_hours), + log_stats: z.enum(['on', 'off']).catch(STORE_LOG.log_stats), + prometheus_interval: z + .number() + .int() + .nonnegative() + .optional() + .catch(STORE_LOG.prometheus_interval), +}) + +const authSchema = z.object({ + new_files: z.enum(['on', 'off']).catch(AUTH.new_files), + create_password: z.string().catch(''), + control_port_admin_password: z + .string() + .optional() + .catch(AUTH.control_port_admin_password), + control_port_user_password: z + .string() + .optional() + .catch(AUTH.control_port_user_password), +}) + +const transportSchema = z.object({ + host: z.string().catch(TRANSPORT.host), + port: z.literal(TRANSPORT.port).catch(TRANSPORT.port), + log_tls_errors: z.enum(['on', 'off']).catch(TRANSPORT.log_tls_errors), + control_port: z + .number() + .int() + .nonnegative() + .optional() + .catch(TRANSPORT.control_port), +}) + +const filesSchema = z.object({ + path: z.literal(FILES.path).catch(FILES.path), + storage_quota: z.string().catch(FILES.storage_quota), +}) + +const inactiveClientsSchema = z.object({ + disconnect: z + .literal(INACTIVE_CLIENTS.disconnect) + .catch(INACTIVE_CLIENTS.disconnect), +}) + +const shape = z.object({ + STORE_LOG: storeLogSchema.catch(() => storeLogSchema.parse({})), + AUTH: authSchema.catch(() => authSchema.parse({})), + TRANSPORT: transportSchema.catch(() => transportSchema.parse({})), + FILES: filesSchema.catch(() => filesSchema.parse({})), + INACTIVE_CLIENTS: inactiveClientsSchema.catch(() => + inactiveClientsSchema.parse({}), + ), +}) + +export type FileServerConfig = z.infer + +export const fileServerIni = FileHelper.ini( + { + base: sdk.volumes['xftp-configs'], + subpath: './file-server.ini', + }, + shape, +) diff --git a/startos/fileModels/ini-lib.ts b/startos/fileModels/ini-lib.ts new file mode 100644 index 0000000..14ef897 --- /dev/null +++ b/startos/fileModels/ini-lib.ts @@ -0,0 +1,157 @@ +export type IniValue = string | number | boolean | string[] +export type IniInputValue = IniValue | null | undefined +export type IniSection = Record +export type IniInputSection = Record +export type IniData = Record +export type IniInputData = Record + +export interface ParseOptions { + /** Delimiter for list values (default: ",") */ + listDelimiter?: string + /** If true, keep all values as strings without type conversion */ + rawStrings?: boolean +} + +export interface StringifyOptions { + /** Delimiter for list values (default: ",") */ + listDelimiter?: string +} + +/** + * Parse a string value into its typed equivalent + */ +function parseValue(value: string, options: ParseOptions): IniValue { + if (options.rawStrings) { + return value + } + + const trimmed = value.trim() + + // Boolean + if (trimmed.toLowerCase() === 'true') return true + if (trimmed.toLowerCase() === 'false') return false + + // Number (integers and floats, including negatives) + if (/^-?\d+(\.\d+)?$/.test(trimmed)) { + const num = parseFloat(trimmed) + if (!isNaN(num)) return num + } + + // List (contains delimiter) + const delimiter = options.listDelimiter ?? ',' + if (trimmed.includes(delimiter)) { + return trimmed.split(delimiter).map((item) => item.trim()) + } + + // Default: string + return trimmed +} + +/** + * Convert a typed value back to string + */ +function stringifyValue(value: IniValue, options: StringifyOptions): string { + if (Array.isArray(value)) { + const delimiter = options.listDelimiter ?? ',' + return value.join(delimiter) + } + return String(value) +} + +/** + * Check if a value is a section (nested object) vs a primitive value + */ +function isSection( + value: IniInputValue | IniInputSection, +): value is IniInputSection { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** + * Parse an INI string that uses `: ` as the key/value separator + */ +export function parse(content: string, options: ParseOptions = {}): IniData { + const result: IniData = {} + let currentSection: string | null = null + + const lines = content.split(/\r?\n/) + + for (const line of lines) { + const trimmed = line.trim() + + // Skip empty lines and comments + if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('#')) { + continue + } + + // Check for section header + const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/) + if (sectionMatch) { + currentSection = sectionMatch[1] + result[currentSection] = {} + continue + } + + // Parse key: value pairs + const separatorIndex = trimmed.indexOf(': ') + if (separatorIndex !== -1) { + const key = trimmed.slice(0, separatorIndex).trim() + const rawValue = trimmed.slice(separatorIndex + 2) + const value = parseValue(rawValue, options) + + if (currentSection) { + ;(result[currentSection] as IniSection)[key] = value + } else { + result[key] = value + } + } + } + + return result +} + +/** + * Stringify an object to INI format using `: ` as the key/value separator + * Note: null and undefined values are omitted (INI has no native null concept) + */ +export function stringify( + data: IniInputData, + options: StringifyOptions = {}, +): string { + const lines: string[] = [] + + // First, output top-level key-value pairs (non-sections) + for (const [key, value] of Object.entries(data)) { + if (value == null) continue // filters both null and undefined + if (!isSection(value)) { + lines.push(`${key}: ${stringifyValue(value, options)}`) + } + } + + // Then, output sections + for (const [key, value] of Object.entries(data)) { + if (value == null) continue + if (isSection(value)) { + const sectionLines: string[] = [] + for (const [subKey, subValue] of Object.entries(value)) { + if (subValue == null) continue + sectionLines.push(`${subKey}: ${stringifyValue(subValue, options)}`) + } + // Only add section if it has values + if (sectionLines.length > 0) { + if (lines.length > 0) { + lines.push('') // Blank line before section + } + lines.push(`[${key}]`) + lines.push(...sectionLines) + } + } + } + + return lines.join('\n') +} + +/** + * Type guard to check if a value is a section + */ +export { isSection } diff --git a/startos/fileModels/smpServer.ini.ts b/startos/fileModels/smpServer.ini.ts new file mode 100644 index 0000000..3e7b53b --- /dev/null +++ b/startos/fileModels/smpServer.ini.ts @@ -0,0 +1,128 @@ +import { FileHelper, z } from '@start9labs/start-sdk' +import { smpConfigDefaults, smpStatePath } from '../utils' +import * as INI from './ini-lib' +import { sdk } from '../sdk' + +const { + INFORMATION, + STORE_LOG, + AUTH, + TRANSPORT, + PROXY, + INACTIVE_CLIENTS, + WEB, +} = smpConfigDefaults + +const informationSchema = z.object({ + source_code: z + .literal(INFORMATION.source_code) + .catch(INFORMATION.source_code), +}) + +const storeLogSchema = z.object({ + enable: z.enum(['on', 'off']).catch(STORE_LOG.enable), + store_queues: z + .literal(STORE_LOG.store_queues) + .catch(STORE_LOG.store_queues), + store_messages: z + .literal(STORE_LOG.store_messages) + .catch(STORE_LOG.store_messages), + restore_messages: z.enum(['on', 'off']).catch(STORE_LOG.restore_messages), + expire_messages_days: z + .number() + .int() + .nonnegative() + .optional() + .catch(STORE_LOG.expire_messages_days), + expire_messages_on_start: z + .literal(STORE_LOG.expire_messages_on_start) + .catch(STORE_LOG.expire_messages_on_start), + expire_ntfs_hours: z + .number() + .int() + .nonnegative() + .optional() + .catch(STORE_LOG.expire_ntfs_hours), + log_stats: z.enum(['on', 'off']).catch(STORE_LOG.log_stats), + prometheus_interval: z + .number() + .int() + .nonnegative() + .optional() + .catch(STORE_LOG.prometheus_interval), +}) + +const authSchema = z.object({ + new_queues: z.literal(AUTH.new_queues).catch(AUTH.new_queues), + create_password: z.string().catch(''), + control_port_admin_password: z + .string() + .optional() + .catch(AUTH.control_port_admin_password), + control_port_user_password: z + .string() + .optional() + .catch(AUTH.control_port_user_password), +}) + +const transportSchema = z.object({ + host: z.string().catch(TRANSPORT.host), + port: z.literal(TRANSPORT.port).catch(TRANSPORT.port), + log_tls_errors: z + .literal(TRANSPORT.log_tls_errors) + .catch(TRANSPORT.log_tls_errors), + websockets: z.literal(TRANSPORT.websockets).catch(TRANSPORT.websockets), + control_port: z + .literal(TRANSPORT.control_port) + .catch(TRANSPORT.control_port), +}) + +const proxySchema = z.object({ + socks_proxy: z.literal(PROXY.socks_proxy).catch(PROXY.socks_proxy), + client_concurrency: z + .number() + .int() + .nonnegative() + .catch(PROXY.client_concurrency), +}) + +const inactiveClientsSchema = z.object({ + disconnect: z + .literal(INACTIVE_CLIENTS.disconnect) + .catch(INACTIVE_CLIENTS.disconnect), +}) + +const webSchema = z.object({ + static_path: z + .literal(`${smpStatePath}/www`) + .optional() + .catch(WEB.static_path), + http: z.literal(WEB.http).catch(WEB.http), + https: z.any().optional().catch(undefined), + cert: z.any().optional().catch(undefined), + key: z.any().optional().catch(undefined), +}) + +const shape = z.object({ + INFORMATION: informationSchema.catch(() => informationSchema.parse({})), + STORE_LOG: storeLogSchema.catch(() => storeLogSchema.parse({})), + AUTH: authSchema.catch(() => authSchema.parse({})), + TRANSPORT: transportSchema.catch(() => transportSchema.parse({})), + PROXY: proxySchema.catch(() => proxySchema.parse({})), + INACTIVE_CLIENTS: inactiveClientsSchema.catch(() => + inactiveClientsSchema.parse({}), + ), + WEB: webSchema.catch(() => webSchema.parse({})), +}) + +export type SmpServerConfig = z.infer + +export const smpServerIni = FileHelper.raw( + { + base: sdk.volumes['smp-configs'], + subpath: './smp-server.ini', + }, + (inData) => INI.stringify(inData), + (inString) => INI.parse(inString), + (data) => shape.parse(data), +) diff --git a/startos/i18n/dictionaries/default.ts b/startos/i18n/dictionaries/default.ts new file mode 100644 index 0000000..dbb713e --- /dev/null +++ b/startos/i18n/dictionaries/default.ts @@ -0,0 +1,19 @@ +export const DEFAULT_LANG = 'en_US' + +const dict = { + 'Starting SimpleX!': 0, + 'No smp-server.ini': 1, + 'No file-server.ini': 2, + 'SMP Server': 3, + 'The SMP server is ready': 4, + 'The SMP server is not ready': 5, + 'XFTP Server': 6, + 'The XFTP server is ready': 7, + 'The XFTP server is not ready': 8, + 'The SMP server for SimpleX': 9, + 'The XFTP server for SimpleX': 10, +} as const + +export type I18nKey = keyof typeof dict +export type LangDict = Record<(typeof dict)[I18nKey], string> +export default dict diff --git a/startos/i18n/dictionaries/translations.ts b/startos/i18n/dictionaries/translations.ts new file mode 100644 index 0000000..d66b53e --- /dev/null +++ b/startos/i18n/dictionaries/translations.ts @@ -0,0 +1,56 @@ +import { LangDict } from './default' + +export default { + es_ES: { + 0: '¡Iniciando SimpleX!', + 1: 'Sin smp-server.ini', + 2: 'Sin file-server.ini', + 3: 'Servidor SMP', + 4: 'El servidor SMP está listo', + 5: 'El servidor SMP no está listo', + 6: 'Servidor XFTP', + 7: 'El servidor XFTP está listo', + 8: 'El servidor XFTP no está listo', + 9: 'El servidor SMP de SimpleX', + 10: 'El servidor XFTP de SimpleX', + }, + de_DE: { + 0: 'SimpleX wird gestartet!', + 1: 'Keine smp-server.ini', + 2: 'Keine file-server.ini', + 3: 'SMP-Server', + 4: 'Der SMP-Server ist bereit', + 5: 'Der SMP-Server ist nicht bereit', + 6: 'XFTP-Server', + 7: 'Der XFTP-Server ist bereit', + 8: 'Der XFTP-Server ist nicht bereit', + 9: 'Der SMP-Server für SimpleX', + 10: 'Der XFTP-Server für SimpleX', + }, + pl_PL: { + 0: 'Uruchamianie SimpleX!', + 1: 'Brak smp-server.ini', + 2: 'Brak file-server.ini', + 3: 'Serwer SMP', + 4: 'Serwer SMP jest gotowy', + 5: 'Serwer SMP nie jest gotowy', + 6: 'Serwer XFTP', + 7: 'Serwer XFTP jest gotowy', + 8: 'Serwer XFTP nie jest gotowy', + 9: 'Serwer SMP dla SimpleX', + 10: 'Serwer XFTP dla SimpleX', + }, + fr_FR: { + 0: 'Démarrage de SimpleX !', + 1: 'Pas de smp-server.ini', + 2: 'Pas de file-server.ini', + 3: 'Serveur SMP', + 4: 'Le serveur SMP est prêt', + 5: 'Le serveur SMP n\'est pas prêt', + 6: 'Serveur XFTP', + 7: 'Le serveur XFTP est prêt', + 8: 'Le serveur XFTP n\'est pas prêt', + 9: 'Le serveur SMP de SimpleX', + 10: 'Le serveur XFTP de SimpleX', + }, +} satisfies Record diff --git a/startos/i18n/index.ts b/startos/i18n/index.ts new file mode 100644 index 0000000..1849d6d --- /dev/null +++ b/startos/i18n/index.ts @@ -0,0 +1,5 @@ +import { setupI18n } from '@start9labs/start-sdk' +import defaultDict, { DEFAULT_LANG } from './dictionaries/default' +import translations from './dictionaries/translations' + +export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG) diff --git a/startos/index.ts b/startos/index.ts new file mode 100644 index 0000000..7af589b --- /dev/null +++ b/startos/index.ts @@ -0,0 +1,11 @@ +/** + * Plumbing. DO NOT EDIT. + */ +export { createBackup } from './backups' +export { main } from './main' +export { init, uninit } from './init' +export { actions } from './actions' +import { buildManifest } from '@start9labs/start-sdk' +import { manifest as sdkManifest } from './manifest' +import { versionGraph } from './versions' +export const manifest = buildManifest(versionGraph, sdkManifest) diff --git a/startos/init/index.ts b/startos/init/index.ts new file mode 100644 index 0000000..f62ec06 --- /dev/null +++ b/startos/init/index.ts @@ -0,0 +1,18 @@ +import { sdk } from '../sdk' +import { setDependencies } from '../dependencies' +import { setInterfaces } from '../interfaces' +import { versionGraph } from '../versions' +import { actions } from '../actions' +import { restoreInit } from '../backups' +import { initServers } from './initServers' + +export const init = sdk.setupInit( + restoreInit, + versionGraph, + setInterfaces, + setDependencies, + actions, + initServers, +) + +export const uninit = sdk.setupUninit(versionGraph) diff --git a/startos/init/initServers.ts b/startos/init/initServers.ts new file mode 100644 index 0000000..80d705a --- /dev/null +++ b/startos/init/initServers.ts @@ -0,0 +1,64 @@ +import { utils } from '@start9labs/start-sdk' +import { sdk } from '../sdk' +import { + smpMounts, + xftpConfigDefaults, + xftpFilePath, + xftpMounts, +} from '../utils' +import { smpServerIni } from '../fileModels/smpServer.ini' +import { fileServerIni } from '../fileModels/fileServer.ini' + +export const initServers = sdk.setupOnInit(async (effects, kind) => { + if (kind === 'install') { + const create_password = utils.getDefaultString({ + charset: 'a-z,A-Z,1-9,!,$,%,&,*', + len: 21, + }) + + await sdk.SubContainer.withTemp( + effects, + { imageId: 'smp' }, + smpMounts, + 'init-smp', + async (sub) => { + await sub.execFail([ + 'smp-server', + 'init', + '-y', + '-l', + '--password', + create_password, + ]) + + await smpServerIni.merge(effects, { + AUTH: { create_password }, + }) + }, + ) + + await sdk.SubContainer.withTemp( + effects, + { imageId: 'xftp' }, + xftpMounts, + 'init-xftp', + async (sub) => { + await sub.execFail([ + 'xftp-server', + 'init', + '-p', + xftpFilePath, + '-q', + xftpConfigDefaults.FILES.storage_quota, + ]) + + await fileServerIni.merge(effects, { + AUTH: { create_password }, + }) + }, + ) + } else { + await smpServerIni.merge(effects, {}) + await fileServerIni.merge(effects, {}) + } +}) diff --git a/startos/interfaces.ts b/startos/interfaces.ts new file mode 100644 index 0000000..8cb2bc5 --- /dev/null +++ b/startos/interfaces.ts @@ -0,0 +1,94 @@ +import { FileHelper } from '@start9labs/start-sdk' +import { sdk } from './sdk' +import { smpPort, webPort, xftpPort } from './utils' +import { smpServerIni } from './fileModels/smpServer.ini' +import { i18n } from './i18n' + +export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { + // get fingerprints + const smpFingerprint = await FileHelper.string({ + base: sdk.volumes['smp-configs'], + subpath: './fingerprint', + }) + .read() + .const(effects) + .then((f) => f?.trim()) + + const xftpFingerprint = await FileHelper.string({ + base: sdk.volumes['xftp-configs'], + subpath: './fingerprint', + }) + .read() + .const(effects) + .then((f) => f?.trim()) + + // get password + const password = await smpServerIni + .read((s) => s.AUTH.create_password) + .const(effects) + + // ** SMP Server ** + const smpMulti = sdk.MultiHost.of(effects, 'main') + const smpMultiOrigin = await smpMulti.bindPort(smpPort, { + protocol: null, + addSsl: null, + preferredExternalPort: smpPort, + secure: { ssl: true }, + }) + const smp = sdk.createInterface(effects, { + name: i18n('SMP Server'), + id: 'smp', + description: i18n('The SMP server for SimpleX'), + type: 'api', + masked: true, + schemeOverride: { ssl: 'smp', noSsl: 'smp' }, + username: `${smpFingerprint}:${password}`, + path: '', + query: {}, + }) + const smpReceipt = await smpMultiOrigin.export([smp]) + + // ** XFTP Server ** + const xftpMulti = sdk.MultiHost.of(effects, 'xftp') + const xftpMultiOrigin = await xftpMulti.bindPort(xftpPort, { + protocol: null, + addSsl: null, + preferredExternalPort: xftpPort, + secure: { ssl: true }, + }) + const xftp = sdk.createInterface(effects, { + name: i18n('XFTP Server'), + id: 'xftp', + description: i18n('The XFTP server for SimpleX'), + type: 'api', + masked: true, + schemeOverride: { ssl: 'xftp', noSsl: 'xftp' }, + username: `${xftpFingerprint}:${password}`, + path: '', + query: {}, + }) + const xftpReceipt = await xftpMultiOrigin.export([xftp]) + + const receipts = [smpReceipt, xftpReceipt] + + // @TODO consider implementing optional web info page + // if (store.enableWeb) { + // const infoMultiOrigin = await smpMulti.bindPort(webPort, { + // protocol: 'http', + // }) + // const info = sdk.createInterface(effects, { + // name: 'Info UI', + // id: 'info', + // description: 'Information about your SMP server', + // type: 'ui', + // masked: false, + // schemeOverride: null, + // username: null, + // path: '', + // query: {}, + // }) + // receipts.push(await infoMultiOrigin.export([info])) + // } + + return receipts +}) diff --git a/startos/main.ts b/startos/main.ts new file mode 100644 index 0000000..7ef82ba --- /dev/null +++ b/startos/main.ts @@ -0,0 +1,73 @@ +import { fileServerIni } from './fileModels/fileServer.ini' +import { smpServerIni } from './fileModels/smpServer.ini' +import { i18n } from './i18n' +import { sdk } from './sdk' +import { smpMounts, smpPort, xftpMounts, xftpPort } from './utils' + +export const main = sdk.setupMain(async ({ effects }) => { + /** + * ======================== Setup (optional) ======================== + * + * In this section, we fetch any resources or run any desired preliminary commands. + */ + console.info(i18n('Starting SimpleX!')) + + // confirm configuration files are present and restart service if they change + const smpIni = await smpServerIni.read().const(effects) + if (!smpIni) { + throw new Error(i18n('No smp-server.ini')) + } + const fileIni = await fileServerIni.read().const(effects) + if (!fileIni) { + throw new Error(i18n('No file-server.ini')) + } + + /** + * ======================== Daemons ======================== + * + * In this section, we create one or more daemons that define the service runtime. + * + * Each daemon defines its own health check, which can optionally be exposed to the user. + */ + return sdk.Daemons.of(effects) + .addDaemon('smp', { + subcontainer: await sdk.SubContainer.of( + effects, + { imageId: 'smp' }, + smpMounts, + 'smp-sub', + ), + exec: { + command: sdk.useEntrypoint(), + }, + ready: { + display: i18n('SMP Server'), + fn: () => + sdk.healthCheck.checkPortListening(effects, smpPort, { + successMessage: i18n('The SMP server is ready'), + errorMessage: i18n('The SMP server is not ready'), + }), + }, + requires: [], + }) + .addDaemon('xftp', { + subcontainer: await sdk.SubContainer.of( + effects, + { imageId: 'xftp' }, + xftpMounts, + 'xftp-sub', + ), + exec: { + command: sdk.useEntrypoint(), + }, + ready: { + display: i18n('XFTP Server'), + fn: () => + sdk.healthCheck.checkPortListening(effects, xftpPort, { + successMessage: i18n('The XFTP server is ready'), + errorMessage: i18n('The XFTP server is not ready'), + }), + }, + requires: [], + }) +}) diff --git a/startos/manifest/i18n.ts b/startos/manifest/i18n.ts new file mode 100644 index 0000000..e91aa81 --- /dev/null +++ b/startos/manifest/i18n.ts @@ -0,0 +1,18 @@ +export default { + description: { + short: { + en_US: 'Freedom & security of your communications', + es_ES: 'Libertad y seguridad de tus comunicaciones', + de_DE: 'Freiheit und Sicherheit Ihrer Kommunikation', + pl_PL: 'Wolność i bezpieczeństwo Twojej komunikacji', + fr_FR: 'Liberté et sécurité de vos communications', + }, + long: { + en_US: 'An open-source, decentralized messenger designed for maximum privacy and control.', + es_ES: 'Un mensajero descentralizado y de código abierto diseñado para máxima privacidad y control.', + de_DE: 'Ein quelloffener, dezentraler Messenger für maximale Privatsphäre und Kontrolle.', + pl_PL: 'Otwartoźródłowy, zdecentralizowany komunikator zaprojektowany z myślą o maksymalnej prywatności i kontroli.', + fr_FR: 'Un messager open source et décentralisé conçu pour une confidentialité et un contrôle maximaux.', + }, + }, +} diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts new file mode 100644 index 0000000..94bb939 --- /dev/null +++ b/startos/manifest/index.ts @@ -0,0 +1,43 @@ +import { setupManifest } from '@start9labs/start-sdk' +import i18n from './i18n' + +export const manifest = setupManifest({ + id: 'simplex', + title: 'SimpleX Server', + license: 'MIT', + packageRepo: 'https://github.com/Start9Labs/simplex-startos', + upstreamRepo: 'https://github.com/simplex-chat/simplexmq/', + marketingUrl: 'https://simplex.chat/', + donationUrl: + 'https://github.com/simplex-chat/simplex-chat#help-us-with-donations', + docsUrls: ['https://github.com/simplex-chat/simplex-chat/tree/stable/docs'], + description: i18n.description, + volumes: [ + 'smp-configs', + 'smp-state', + 'xftp-configs', + 'xftp-state', + 'xftp-files', + 'main', // migration + 'conf', // migration + 'xftp', // migration + 'log', // migration + ], + images: { + smp: { + source: { + dockerTag: 'simplexchat/smp-server:v6.4.5', + }, + // @TODO aarch64 coming after 6.4.5 + arch: ['x86_64'], + }, + xftp: { + source: { + dockerTag: 'simplexchat/xftp-server:v6.4.5', + }, + // @TODO aarch64 coming after 6.4.5 + arch: ['x86_64'], + }, + }, + dependencies: {}, +}) diff --git a/startos/sdk.ts b/startos/sdk.ts new file mode 100644 index 0000000..04ae4b1 --- /dev/null +++ b/startos/sdk.ts @@ -0,0 +1,9 @@ +import { StartSdk } from '@start9labs/start-sdk' +import { manifest } from './manifest' + +/** + * Plumbing. DO NOT EDIT. + * + * The exported "sdk" const is used throughout this package codebase. + */ +export const sdk = StartSdk.of().withManifest(manifest).build(true) diff --git a/startos/utils.ts b/startos/utils.ts new file mode 100644 index 0000000..e1968d6 --- /dev/null +++ b/startos/utils.ts @@ -0,0 +1,116 @@ +import { sdk } from './sdk' + +export const smpPort = 5223 +export const smpControlPort = 5224 + +export const xftpPort = 5225 +export const xftpControlPort = 5226 + +export const smpStatePath = '/var/opt/simplex' +export const xftpFilePath = '/srv/xftp' + +export const webPort = 8000 + +export const smpMounts = sdk.Mounts.of() + .mountVolume({ + volumeId: 'smp-configs', + subpath: null, + mountpoint: '/etc/opt/simplex', + readonly: false, + }) + .mountVolume({ + volumeId: 'smp-state', + subpath: null, + mountpoint: smpStatePath, + readonly: false, + }) + +export const xftpMounts = sdk.Mounts.of() + .mountVolume({ + volumeId: 'xftp-configs', + subpath: null, + mountpoint: '/etc/opt/simplex-xftp', + readonly: false, + }) + .mountVolume({ + volumeId: 'xftp-state', + subpath: null, + mountpoint: '/var/opt/simplex-xftp', + readonly: false, + }) + .mountVolume({ + volumeId: 'xftp-files', + subpath: null, + mountpoint: xftpFilePath, + readonly: false, + }) + +export const smpConfigDefaults = { + INFORMATION: { + source_code: 'https://github.com/simplex-chat/simplexmq/', + }, + STORE_LOG: { + enable: 'on', + store_queues: 'memory', + store_messages: 'memory', + restore_messages: 'on', + expire_messages_days: 365, // @TODO ask Evgany + expire_messages_on_start: 'off', + expire_ntfs_hours: 168, + log_stats: 'on', + prometheus_interval: undefined, + }, + AUTH: { + new_queues: 'on', + control_port_admin_password: undefined, + control_port_user_password: undefined, + }, + TRANSPORT: { + host: '', + port: `${smpPort},443`, + log_tls_errors: 'off', + websockets: 'off', + control_port: smpControlPort, + }, + PROXY: { + socks_proxy: '127.0.0.1:9050', + client_concurrency: 32, + }, + INACTIVE_CLIENTS: { + disconnect: 'off', + }, + WEB: { + static_path: undefined, + http: webPort, + https: undefined, + cert: undefined, + key: undefined, + }, +} as const + +export const xftpConfigDefaults = { + STORE_LOG: { + enable: 'on', + expire_files_hours: 168, + log_stats: 'off', + prometheus_interval: undefined, + }, + AUTH: { + new_files: 'on', + control_port_admin_password: undefined, + control_port_user_password: undefined, + }, + TRANSPORT: { + host: '', + port: xftpPort, + log_tls_errors: 'off', + control_port: xftpControlPort, + }, + FILES: { + path: xftpFilePath, + storage_quota: '10gb', + }, + INACTIVE_CLIENTS: { + disconnect: 'off', + }, +} as const diff --git a/startos/versions/index.ts b/startos/versions/index.ts new file mode 100644 index 0000000..c37e7b4 --- /dev/null +++ b/startos/versions/index.ts @@ -0,0 +1,7 @@ +import { VersionGraph } from '@start9labs/start-sdk' +import { v_6_4_5_3 } from './v6.4.5.3' + +export const versionGraph = VersionGraph.of({ + current: v_6_4_5_3, + other: [], +}) diff --git a/startos/versions/v6.4.5.3.ts b/startos/versions/v6.4.5.3.ts new file mode 100644 index 0000000..21f9520 --- /dev/null +++ b/startos/versions/v6.4.5.3.ts @@ -0,0 +1,127 @@ +import { IMPOSSIBLE, utils, VersionInfo, YAML } from '@start9labs/start-sdk' +import { execFile } from 'child_process' +import { readdir, readFile, rm } from 'fs/promises' +import { join } from 'path' +import { fileServerIni } from '../fileModels/fileServer.ini' +import { smpServerIni } from '../fileModels/smpServer.ini' +import { smpConfigDefaults, xftpConfigDefaults } from '../utils' + +// NOTE, adding passwords to xftp server addresses. Previous addresses are less secure and expected to break. + +export const v_6_4_5_3 = VersionInfo.of({ + version: '6.4.5:3', + releaseNotes: { + en_US: 'Update to StartOS SDK beta.60', + es_ES: 'Actualización a StartOS SDK beta.60', + de_DE: 'Update auf StartOS SDK beta.60', + pl_PL: 'Aktualizacja do StartOS SDK beta.60', + fr_FR: 'Mise à jour vers StartOS SDK beta.60', + }, + migrations: { + up: async ({ effects }) => { + // get old stats.yaml + const statsYaml: + | { + data: { + 'SimpleX SMP Server Address': { + value: string + } + } + } + | undefined = await readFile( + '/media/startos/volumes/main/start9/stats.yaml', + 'utf-8', + ).then(YAML.parse, () => undefined) + + if (statsYaml) { + // config (was used for smp-server.ini) + await new Promise((res, rej) => { + execFile( + 'sh', + [ + '-c', + 'mv /media/startos/volumes/conf/* /media/startos/volumes/smp-configs', + ], + (err) => (err ? rej(err) : res(null)), + ) + }).catch(console.error) + + // xftp (was used for file-server.ini) + await new Promise((res, rej) => { + execFile( + 'sh', + [ + '-c', + 'mv /media/startos/volumes/xftp/* /media/startos/volumes/xftp-configs', + ], + (err) => (err ? rej(err) : res(null)), + ) + }).catch(console.error) + + // log (was used for smp-state) + await new Promise((res, rej) => { + execFile( + 'sh', + [ + '-c', + 'mv /media/startos/volumes/log/* /media/startos/volumes/smp-state', + ], + (err) => (err ? rej(err) : res(null)), + ) + }).catch(console.error) + + // main (was used for xftp files) + await new Promise((res, rej) => { + execFile( + 'sh', + [ + '-c', + 'mv /media/startos/volumes/xftp/* /media/startos/volumes/xftp-files', + ], + (err) => (err ? rej(err) : res(null)), + ) + }).catch(console.error) + + const create_password = + new URL( + statsYaml.data['SimpleX SMP Server Address'].value.replace( + 'smp://', + 'https://', + ), + ).password || + utils.getDefaultString({ + charset: 'a-z,A-Z,1-9,!,$,%,&,*', + len: 21, + }) + + // overwrite smp-server.ini + await smpServerIni.write(effects, { + ...smpConfigDefaults, + AUTH: { ...smpConfigDefaults.AUTH, create_password }, + }) + + // overwrite file-server.ini + await fileServerIni.write(effects, { + ...xftpConfigDefaults, + AUTH: { ...xftpConfigDefaults.AUTH, create_password }, + }) + // remove everything from old volumes + await Promise.all( + ['conf', 'xftp', 'log', 'main'].map((vol) => + clearVolume(`/media/startos/volumes/${vol}`), + ), + ) + } + }, + down: IMPOSSIBLE, + }, +}) + +async function clearVolume(volumePath: string) { + const entries = await readdir(volumePath) + await Promise.all( + entries.map((entry) => + rm(join(volumePath, entry), { recursive: true }).catch(console.error), + ), + ) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a2945a5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["startos/**/*.ts", "node_modules/**/startos"], + "compilerOptions": { + "target": "ES2018", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +}