diff --git a/.gitignore b/.gitignore index 4ce7e77..ca256b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -nostr.s9pk -image.tar -scripts/*.js +*.s9pk +procedures/*.js +node_modules/ .DS_Store .vscode/ docker-images \ No newline at end of file diff --git a/Makefile b/Makefile index fa934aa..6020734 100644 --- a/Makefile +++ b/Makefile @@ -20,21 +20,27 @@ else endif # for rebuilding just the arm image. will include docker-images/x86_64.tar into the s9pk if it exists -arm: docker-images/aarch64.tar scripts/embassy.js +arm: docker-images/aarch64.tar procedures/embassy.js embassy-sdk pack # for rebuilding just the x86 image. will include docker-images/aarch64.tar into the s9pk if it exists -x86: docker-images/x86_64.tar scripts/embassy.js +x86: docker-images/x86_64.tar procedures/embassy.js embassy-sdk pack clean: rm -rf docker-images rm -f image.tar rm -f $(PKG_ID).s9pk - rm -f scripts/*.js + rm -f procedures/*.js -scripts/embassy.js: $(TS_FILES) - deno bundle scripts/embassy.ts scripts/embassy.js +procedures/embassy.js: $(TS_FILES) + cd procedures && npm run build + +check: $(TS_FILES) + cd procedures && npm run check + +fmt: $(TS_FILES) + cd procedures && npm run prettier docker-images/aarch64.tar: Dockerfile docker_entrypoint.sh ifeq ($(ARCH),x86_64) @@ -50,7 +56,7 @@ else docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) --build-arg ARCH=x86_64 --build-arg PLATFORM=amd64 --platform=linux/amd64 -o type=docker,dest=docker-images/x86_64.tar . endif -$(PKG_ID).s9pk: manifest.yaml instructions.md icon.png LICENSE scripts/embassy.js docker-images/aarch64.tar docker-images/x86_64.tar +$(PKG_ID).s9pk: manifest.yaml instructions.md icon.png LICENSE procedures/embassy.js docker-images/aarch64.tar docker-images/x86_64.tar ifeq ($(ARCH),aarch64) @echo "embassy-sdk: Preparing aarch64 package ..." else ifeq ($(ARCH),x86_64) diff --git a/README.md b/README.md index 414821b..414671b 100644 --- a/README.md +++ b/README.md @@ -1,112 +1 @@ -# Wrapper for nostr - -This is a nostr relay, written in Rust. It currently supports the entire relay protocol, and persists data with SQLite. This repository creates the `s9pk` package that is installed to run `nostr` on [embassyOS](https://github.com/Start9Labs/embassy-os/). Learn more about service packaging in the [Developer Docs](https://start9.com/latest/developer-docs/). - -## Dependencies - -Install the system dependencies below to build this project by following the instructions in the provided links. You can also find detailed steps to setup your environment in the service packaging [documentation](https://github.com/Start9Labs/service-pipeline#development-environment). - -- [docker](https://docs.docker.com/get-docker) -- [docker-buildx](https://docs.docker.com/buildx/working-with-buildx/) -- [yq](https://mikefarah.gitbook.io/yq) -- [deno](https://deno.land/) -- [make](https://www.gnu.org/software/make/) -- [embassy-sdk](https://github.com/Start9Labs/embassy-os/tree/master/backend) - -## Build environment -Prepare your embassyOS build environment. In this example we are using Ubuntu 20.04. -1. Install docker -``` -curl -fsSL https://get.docker.com -o- | bash -sudo usermod -aG docker "$USER" -exec sudo su -l $USER -``` -2. Set buildx as the default builder -``` -docker buildx install -docker buildx create --use -``` -3. Enable cross-arch emulated builds in docker -``` -docker run --privileged --rm linuxkit/binfmt:v0.8 -``` -4. Install yq -``` -sudo snap install yq -``` -5. Install deno -``` -sudo snap install deno -``` -6. Install essentials build packages -``` -sudo apt-get install -y build-essential openssl libssl-dev libc6-dev clang libclang-dev ca-certificates -``` -7. Install Rust -``` -curl https://sh.rustup.rs -sSf | sh -# Choose nr 1 (default install) -source $HOME/.cargo/env -``` -8. Build and install embassy-sdk -``` -cd ~/ && git clone --recursive https://github.com/Start9Labs/embassy-os.git -cd embassy-os/backend/ -./install-sdk.sh -embassy-sdk init -``` -Now you are ready to build the `nostr` package! - -## Cloning - -Clone the project locally: - -``` -git clone https://github.com/Start9Labs/nostr-wrapper.git -cd nostr-wrapper -``` - -## Building - -To build the `nostr` package for all platforms using embassy-sdk version >=0.3.3, run the following command: - -``` -make -``` - -To build the `nostr` package for a single platform using embassy-sdk version <=0.3.2, run: - -``` -# for amd64 -make ARCH=x86_64 -``` -or -``` -# for arm64 -make ARCH=aarch64 -``` - -## Installing (on embassyOS) - -Run the following commands to determine successful install: -> :information_source: Change embassy-server-name.local to your Embassy address - -``` -embassy-cli auth login -# Enter your embassy password -embassy-cli --host https://embassy-server-name.local package install nostr.s9pk -``` - -If you already have your `embassy-cli` config file setup with a default `host`, you can install simply by running: - -``` -make install -``` - -> **Tip:** You can also install the nostr.s9pk using **Sideload Service** under the **System > Manage** section. - -### Verify Install - -Go to your Embassy Services page, select **Nostr**, configure and start the service. Then, verify its interfaces are accessible. - -**Done!** +# Wrapper for Nostr RS Relay \ No newline at end of file diff --git a/icon.png b/assets/icon.png similarity index 100% rename from icon.png rename to assets/icon.png diff --git a/instructions.md b/assets/instructions.md similarity index 98% rename from instructions.md rename to assets/instructions.md index c6fa321..c44444f 100644 --- a/instructions.md +++ b/assets/instructions.md @@ -26,7 +26,7 @@ You will not be able to save the config until you follow the instructions below. ### Running a private relay -Private relays act as a personal backup for your posts, follows, messages, +Private relays act as a private backup for your posts, follows, messages, settings, etc. Without a private relay, there is no guaranteeing these things will be saved anywhere, and they could disappear at any time. diff --git a/config.json b/config.json deleted file mode 100644 index 40b0fb5..0000000 --- a/config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "relays": null, - "following": null -} diff --git a/docker_entrypoint.sh b/docker_entrypoint.sh deleted file mode 100755 index b8978d7..0000000 --- a/docker_entrypoint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -chown -R $APP_USER:$APP_USER $APP_DATA - -su - $APP_USER > /dev/null 2>&1 - -cp $APP_DATA/config.toml.tmp $APP/config.toml - -exec tini ./nostr-rs-relay -- --db /data diff --git a/manifest.yaml b/manifest.yaml deleted file mode 100644 index 01905cf..0000000 --- a/manifest.yaml +++ /dev/null @@ -1,94 +0,0 @@ -id: nostr -title: "Nostr RS Relay" -version: 0.8.2.3 -release-notes: | - * Hotfix: revert from rsync to duplicity backups -license: mit -wrapper-repo: "https://github.com/Start9Labs/nostr-rs-relay-wrapper" -upstream-repo: "https://sr.ht/~gheartsfield/nostr-rs-relay/" -support-site: "https://todo.sr.ht/~gheartsfield/nostr-rs-relay" -marketing-site: "https://nostr.com/" -build: ["make"] -description: - short: A Nostr relay, written in Rust. - long: | - This is a Nostr relay, written in Rust. It currently supports the entire relay protocol, and persists data with SQLite. -assets: - license: LICENSE - icon: icon.png - instructions: instructions.md -main: - type: docker - image: main - entrypoint: "docker_entrypoint.sh" - args: [] - mounts: - main: /data -health-checks: - main: - name: Websocket Server - success-message: The Nostr websocket server is alive and listening for connections. - type: script -config: - get: - type: script - set: - type: script -properties: - type: script -volumes: - main: - type: data -interfaces: - websocket: - name: Websocket Interface - description: Nostr websocket relay interface - tor-config: - port-mapping: - 80: "8080" - lan-config: - 443: - ssl: true - internal: 8080 - ui: false - protocols: - - tcp - - http -alerts: {} -dependencies: {} -backup: - create: - type: docker - image: compat - system: true - entrypoint: compat - args: - - duplicity - - create - - /mnt/backup - - /data - mounts: - BACKUP: /mnt/backup - main: /data - restore: - type: docker - image: compat - system: true - entrypoint: compat - args: - - duplicity - - restore - - /mnt/backup - - /data - mounts: - BACKUP: /mnt/backup - main: /data -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..08dc4c4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,142 @@ +{ + "name": "nostr-rs-relay-startos", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@start9labs/start-sdk": "0.4.0-rev0.lib0.rc7" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@vercel/ncc": "^0.36.1", + "prettier": "^2.8.4", + "typescript": "^4.9.5" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, + "node_modules/@start9labs/start-sdk": { + "version": "0.4.0-rev0.lib0.rc7", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.4.0-rev0.lib0.rc7.tgz", + "integrity": "sha512-SgWkMuCY0VyUF4QzGqwuy/lFBaX2Cve0aJPYsiShnFV3d4xhQZUi568lNzm/qZ9kZQ5t8sFhseHmIM6+lQn2Kw==", + "dependencies": { + "@iarna/toml": "^2.2.5", + "isomorphic-fetch": "^3.0.0", + "ts-matches": "^5.4.1", + "yaml": "^2.2.2" + } + }, + "node_modules/@types/node": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.0.0.tgz", + "integrity": "sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw==", + "dev": true + }, + "node_modules/@vercel/ncc": { + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.36.1.tgz", + "integrity": "sha512-S4cL7Taa9yb5qbv+6wLgiKVZ03Qfkc4jGRuiUQMQ8HGBD5pcNRnHeYM33zBvJE4/zJGjJJ8GScB+WmTsn9mORw==", + "dev": true, + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "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==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-matches": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.4.1.tgz", + "integrity": "sha512-kXrY75F0s0WD15N2bWKDScKlKgwnusN6dTRzGs1N7LlxQRnazrsBISC1HL4sy2adsyk65Zbx3Ui3IGN8leAFOQ==" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "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==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "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==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2646854 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "scripts": { + "build": "ncc build procedures/index.ts -o ./", + "prettier": "prettier --write startos", + "check": "tsc --noEmit" + }, + "dependencies": { + "@start9labs/start-sdk": "0.4.0-rev0.lib0.rc7" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@vercel/ncc": "^0.36.1", + "prettier": "^2.8.4", + "typescript": "^4.9.5" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": true + } +} diff --git a/scripts/deps.ts b/scripts/deps.ts deleted file mode 100644 index d6e8849..0000000 --- a/scripts/deps.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "https://deno.land/x/embassyd_sdk@v0.3.3.0.9/mod.ts"; -export * as TOML from "https://deno.land/std@0.177.0/encoding/toml.ts"; diff --git a/scripts/embassy.ts b/scripts/embassy.ts deleted file mode 100644 index df6835b..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 { health } from "./procedures/health.ts"; diff --git a/scripts/procedures/getConfig.ts b/scripts/procedures/getConfig.ts deleted file mode 100644 index b3957ea..0000000 --- a/scripts/procedures/getConfig.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { compat } from "../deps.ts"; - -export const [getConfig, setConfigMatcher] = compat.getConfigAndMatcher( - { - "tor-address": { - "name": "Tor Address", - "description": "The Tor address for the websocket server.", - "type": "pointer", - "subtype": "package", - "package-id": "nostr", - "target": "tor-address", - "interface": "websocket", - }, - "lan-address": { - "name": "Tor Address", - "description": "The LAN address for the websocket server.", - "type": "pointer", - "subtype": "package", - "package-id": "nostr", - "target": "lan-address", - "interface": "websocket", - }, - "relay-type": { - "type": "union", - "name": "Relay Type", - "warning": - "Running a public relay carries risk. Your relay can be spammed, resulting in large amounts of disk usage.", - "tag": { - "id": "type", - "name": "Relay Type", - "description": - "Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.", - "variant-names": { - "private": "Private", - "public": "Public", - }, - }, - "default": "private", - "variants": { - "private": { - "pubkey_whitelist": { - "name": "Pubkey Whitelist (hex)", - "description": - "A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.", - "type": "list", - "range": "[1,*)", - "subtype": "string", - "spec": { - "placeholder": "hex (not npub) pubkey", - "pattern": "[0-9a-fA-F]{64}", - "pattern-description": - "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", - }, - "default": Array(), // [] as string [] - }, - }, - "public": { - "info": { - "name": "Relay Info", - "description": "General public info about your relay", - "type": "object", - "spec": { - "name": { - "name": "Relay Name", - "description": "Your relay's human-readable identifier", - "type": "string", - "nullable": true, - "placeholder": "Bob's Public Relay", - "pattern": ".{3,32}", - "pattern-description": - "Must be at least 3 character and no more than 32 characters", - "masked": false - }, - "description": { - "name": "Relay Description", - "description": "A more detailed description for your relay", - "type": "string", - "nullable": true, - "placeholder": "The best relay in town", - "pattern": ".{6,256}", - "pattern-description": - "Must be at least 6 character and no more than 256 characters", - "masked": false - }, - "pubkey": { - "name": "Admin contact pubkey (hex)", - "description": - "The Nostr hex (not npub) pubkey of the relay administrator", - "type": "string", - "nullable": true, - "placeholder": "hex (not npub) pubkey", - "pattern": "[0-9a-fA-F]{64}", - "pattern-description": - "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", - "masked": false - }, - "contact": { - "name": "Admin contact email", - "description": "The email address of the relay administrator", - "type": "string", - "nullable": true, - "pattern": "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+", - "pattern-description": "Must be a valid email address.", - "masked": false - }, - }, - }, - "limits": { - "name": "Limits", - "description": - "Data limits to protect your relay from using too many resources", - "type": "object", - "spec": { - "messages_per_sec": { - "name": "Messages Per Second Limit", - "description": - "Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.", - "type": "number", - "nullable": false, - "range": "[1,*)", - "integral": true, - "default": 2, - "units": "messages/sec", - }, - "subscriptions_per_min": { - "name": "Subscriptions Per Minute Limit", - "description": - "Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.", - "type": "number", - "nullable": false, - "range": "[1,*)", - "integral": true, - "default": 10, - "units": "subscriptions", - }, - "max_blocking_threads": { - "name": "Max Blocking Threads", - "description": - "Maximum number of blocking threads used for database connections.", - "type": "number", - "nullable": false, - "range": "[0,*)", - "integral": true, - "units": "threads", - "default": 16, - }, - "max_event_bytes": { - "name": "Max Event Size", - "description": - "Limit the maximum size of an EVENT message. Set to 0 for unlimited", - "type": "number", - "nullable": false, - "range": "[0,*)", - "integral": true, - "units": "bytes", - "default": 131072, - }, - "max_ws_message_bytes": { - "name": "Max Websocket Message Size", - "description": "Maximum WebSocket message in bytes.", - "type": "number", - "nullable": false, - "range": "[0,*)", - "integral": true, - "units": "bytes", - "default": 131072, - }, - "max_ws_frame_bytes": { - "name": "Max Websocket Frame Size", - "description": "Maximum WebSocket frame size in bytes.", - "type": "number", - "nullable": false, - "range": "[0,*)", - "integral": true, - "units": "bytes", - "default": 131072, - }, - "event_kind_blacklist": { - "name": "Event Kind Blacklist", - "description": - "Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds", - "type": "list", - "range": "[0,*)", - "subtype": "number", - "spec": { - "integral": true, - "placeholder": 30023, - "range": '(0,100000]', - }, - "default": Array(), // [] as number [] - }, - }, - }, - }, - }, - }, - } as const, -); - -export type SetConfig = typeof setConfigMatcher._TYPE; diff --git a/scripts/procedures/health.ts b/scripts/procedures/health.ts deleted file mode 100644 index 5c6fc7b..0000000 --- a/scripts/procedures/health.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { types as T, healthUtil } from "../deps.ts"; - -export const health: T.ExpectedExports.health = { - "main": healthUtil.checkWebUrl("http://nostr.embassy:8080") -} 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 c6abb31..0000000 --- a/scripts/procedures/migrations.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { compat, types as T } from "../deps.ts"; - -export const migration: T.ExpectedExports.migration = compat.migrations - .fromMapping({ - "0.8.2": { - up: compat.migrations.updateConfig( - (config) => { - config.info = {}; - config.limits = { "event_kind_blacklist": [] }; - config.authorization = { "pubkey_whitelist": [] }; - return config; - }, - true, - { version: "0.8.2", type: "up" }, - ), - down: compat.migrations.updateConfig( - (_config) => { - return {}; - }, - true, - { version: "0.8.2", type: "down" }, - ), - }, - "0.8.2.1": { - up: compat.migrations.updateConfig( - (config: any) => { - return { - "relay-type": { - type: "private", - pubkey_whitelist: config.authorization.pubkey_whitelist - } - }; - }, - false, - { version: "0.8.2.1", type: "up" }, - ), - down: compat.migrations.updateConfig( - (_config) => { - return { - info: {}, - limits: { "event_kind_blacklist": [] }, - authorization: { "pubkey_whitelist": [] } - }; - }, - false, - { version: "0.8.2.1", type: "down" }, - ), - }, - }, - "0.8.2.3", -); diff --git a/scripts/procedures/properties.ts b/scripts/procedures/properties.ts deleted file mode 100644 index 6148f86..0000000 --- a/scripts/procedures/properties.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { matches, types as T, util, YAML } from "../deps.ts"; - -const { shape, string } = matches; - -const noPropertiesFound: T.ResultType = { - result: { - version: 2, - data: { - "Not Ready": { - type: "string", - value: - "Could not find properties. Nostr RS Relay might still be starting...", - qr: false, - copyable: false, - masked: false, - description: "Properties could not be found", - }, - }, - }, -} as const; - -const configMatcher = shape({ - "tor-address": string, - "lan-address": string, -}); - -export const properties: T.ExpectedExports.properties = async ( - effects: T.Effects, -) => { - if ( - await util.exists(effects, { - volumeId: "main", - path: "start9/config.yaml", - }) === false - ) { - return noPropertiesFound; - } - const config = configMatcher.unsafeCast(YAML.parse( - await effects.readFile({ - path: "start9/config.yaml", - volumeId: "main", - }), - )); - const properties: T.ResultType = { - result: { - version: 2, - data: { - "Nostr relay websocket URL (Tor, recommended)": { - type: "string", - value: `ws://${config["tor-address"]}`, - description: - "Share this URL with anyone who wants to connect to your relay over Tor. Remember to enable `network.websocket.allowInsecureFromHTTPS` in `about:config` to be able to access your relay using a client in Firefox or Tor Browser.", - copyable: true, - qr: false, - masked: false, - }, - "Nostr relay websocket URL (LAN, not recommended)": { - type: "string", - value: `wss://${config["lan-address"]}`, - description: - "Use this URL to connect to your relay over LAN. Only for testing purposes.", - copyable: true, - qr: false, - masked: false, - }, - }, - }, - } as const; - return properties; -}; diff --git a/scripts/procedures/setConfig.ts b/scripts/procedures/setConfig.ts deleted file mode 100644 index b32bca7..0000000 --- a/scripts/procedures/setConfig.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { compat, TOML, types as T } from "../deps.ts"; - -export const setConfig: T.ExpectedExports.setConfig = async ( - effects: T.Effects, - input: T.Config, -) => { - const config: any = { - network: { - address: "0.0.0.0", - port: 8080 - }, - options: { - reject_future_seconds: 1800 - }, - info: { - relay_url: `ws://${input["tor-address"]}` - } - }; - - const relayTypeUnion = input["relay-type"] as any; - if (relayTypeUnion.type === "private") { - config.authorization = { - "pubkey_whitelist": relayTypeUnion["pubkey_whitelist"] - }; - } else if (relayTypeUnion.type === "public") { - config.info = { ...config.info, ...relayTypeUnion.info }; - config.limits = relayTypeUnion.limits; - } - - const volumeId = "main" - - await effects.createDir({ - path: "start9", - volumeId, - }); - - await effects.writeFile({ - path: "config.toml.tmp", - toWrite: TOML.stringify(config), - volumeId, - }); - - return await compat.setConfig(effects, input); -}; diff --git a/startos/manifest.ts b/startos/manifest.ts new file mode 100644 index 0000000..a148304 --- /dev/null +++ b/startos/manifest.ts @@ -0,0 +1,46 @@ +import { setupManifest } from '@start9labs/start-sdk/lib/manifest/setupManifest' + +export const manifest = setupManifest({ + id: 'nostr', + title: 'Nostr RS Relay', + version: '0.8.9', + releaseNotes: 'Update for StartOS 0.4.0', + license: 'mit', + replaces: Array(), + wrapperRepo: 'https://github.com/Start9Labs/nostr-rs-relay-wrapper/', + upstreamRepo: 'https://github.com/scsibug/nostr-rs-relay/', + supportSite: 'https://github.com/scsibug/nostr-rs-relay/issues/', + marketingSite: 'https://nostr.com/', + donationUrl: null, + description: { + short: 'A Nostr relay, written in Rust', + long: 'Gitea is a community managed lightweight code hosting solution.', + }, + assets: { + license: 'LICENSE', + icon: 'assets/icon.png', + instructions: 'assets/instructions.md', + }, + volumes: { + main: 'data', + }, + containers: { + main: { + image: 'main', + mounts: { + main: '/data', + }, + }, + }, + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: {}, +}) + +export type Manifest = typeof manifest diff --git a/startos/procedures/actions/index.ts b/startos/procedures/actions/index.ts new file mode 100644 index 0000000..de1acce --- /dev/null +++ b/startos/procedures/actions/index.ts @@ -0,0 +1,3 @@ +import { sdk } from '../../sdk' + +export const { actions, actionsMetadata } = sdk.setupActions() diff --git a/startos/procedures/backups.ts b/startos/procedures/backups.ts new file mode 100644 index 0000000..3f340e4 --- /dev/null +++ b/startos/procedures/backups.ts @@ -0,0 +1,3 @@ +import { sdk } from '../sdk' + +export const { createBackup, restoreBackup } = sdk.setupBackups('main') diff --git a/startos/procedures/config/file-models/config.toml.ts b/startos/procedures/config/file-models/config.toml.ts new file mode 100644 index 0000000..c6dc7e2 --- /dev/null +++ b/startos/procedures/config/file-models/config.toml.ts @@ -0,0 +1,45 @@ +import { matches } from '@start9labs/start-sdk' +import FileHelper from '@start9labs/start-sdk/lib/util/fileHelper' + +const { object, array, string, natural, anyOf, allOf } = matches + +const tomlShape = allOf( + object({ + network: object({ + address: string, + port: natural, + }), + options: object({ + reject_future_seconds: natural, + }), + info: object({ + relay_url: string, + }), + }), + anyOf( + object({ + authorization: object({ + pubkey_whitelist: array(string), + }), + }), + object({ + info: object({ + name: string.optional(), + description: string.optional(), + pubkey: string.optional(), + contact: string.optional(), + }), + limits: object({ + messages_per_sec: natural, + subscriptions_per_min: natural, + max_blocking_threads: natural, + max_event_bytes: natural, + max_ws_message_bytes: natural, + max_ws_frame_bytes: natural, + event_kind_blacklist: array(natural), + }), + }), + ), +) + +export const tomlFile = FileHelper.toml('config.toml', tomlShape) diff --git a/startos/procedures/config/index.ts b/startos/procedures/config/index.ts new file mode 100644 index 0000000..6ebab87 --- /dev/null +++ b/startos/procedures/config/index.ts @@ -0,0 +1,9 @@ +import { sdk } from '../../sdk' +import { configSpec } from './spec' +import { read } from './read' +import { save } from './save' + +/** + * This is a static file. There is no need to make changes here + */ +export const { getConfig, setConfig } = sdk.setupConfig(configSpec, save, read) diff --git a/startos/procedures/config/read.ts b/startos/procedures/config/read.ts new file mode 100644 index 0000000..c9375a3 --- /dev/null +++ b/startos/procedures/config/read.ts @@ -0,0 +1,33 @@ +import { sdk } from '../../sdk' +import { configSpec } from './spec' +import { tomlFile } from './file-models/config.toml' + +export const read = sdk.setupConfigRead( + configSpec, + async ({ effects, utils }) => { + const data = await tomlFile.read(effects) + + if (data == null) return + + if ('authorization' in data) { + return { + relayType: { + unionSelectKey: 'private' as const, + unionValueKey: { + pubkey_whitelist: data.authorization.pubkey_whitelist, + }, + }, + } + } + + return { + relayType: { + unionSelectKey: 'public' as const, + unionValueKey: { + info: data.info, + limits: data.limits, + }, + }, + } + }, +) diff --git a/startos/procedures/config/save.ts b/startos/procedures/config/save.ts new file mode 100644 index 0000000..9e45000 --- /dev/null +++ b/startos/procedures/config/save.ts @@ -0,0 +1,57 @@ +import { sdk } from '../../sdk' +import { configSpec } from './spec' +import { tomlFile } from './file-models/config.toml' +import { setInterfaces } from '../interfaces' + +export const save = sdk.setupConfigSave( + configSpec, + async ({ effects, utils, input, dependencies }) => { + const toSave = { + network: { + address: '0.0.0.0', + port: 8080, + }, + options: { + reject_future_seconds: 1800, + }, + info: { + relay_url: `ws://${await effects.getServiceTorHostname('torHostname')}`, + }, + } + + const relayType = input.relayType + + if (relayType.unionSelectKey === 'private') { + await tomlFile.write( + { + ...toSave, + authorization: { + pubkey_whitelist: relayType.unionValueKey.pubkey_whitelist, + }, + }, + effects, + ) + } else { + const { info, limits } = relayType.unionValueKey + await tomlFile.write( + { + ...toSave, + info: { + ...toSave.info, + ...info, + }, + limits, + }, + effects, + ) + } + + const dependenciesReceipt = await effects.setDependencies([]) + + return { + interfacesReceipt: await setInterfaces({ effects, utils, input }), + dependenciesReceipt, + restart: true, + } + }, +) diff --git a/startos/procedures/config/spec.ts b/startos/procedures/config/spec.ts new file mode 100644 index 0000000..6d778df --- /dev/null +++ b/startos/procedures/config/spec.ts @@ -0,0 +1,186 @@ +import { sdk } from '../../sdk' +const { Config, Value, List, Variants } = sdk + +// private config +export const privateConfig = Config.of({ + pubkey_whitelist: Value.list( + List.text( + { + name: 'Pubkey Whitelist (hex)', + minLength: 1, + description: + 'A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.', + }, + { + placeholder: 'hex (not npub) pubkey', + patterns: [ + { + regex: '[0-9a-fA-F]{64}', + description: + 'Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.', + }, + ], + }, + ), + ), +}) + +// public config +export const publicConfig = Config.of({ + info: Value.object( + { + name: 'Relay Info', + description: 'General public info about your relay', + }, + Config.of({ + name: Value.text({ + name: 'Relay Name', + description: "Your relay's human-readable identifier", + required: false, + placeholder: "Bob's Public Relay", + patterns: [ + { + regex: '.{3,32}', + description: + 'Must be at least 3 character and no more than 32 characters', + }, + ], + }), + description: Value.text({ + name: 'Relay Description', + description: 'A more detailed description for your relay', + required: false, + placeholder: 'The best relay in town', + patterns: [ + { + regex: '.{6,256}', + description: + 'Must be at least 6 character and no more than 256 characters', + }, + ], + }), + pubkey: Value.text({ + name: 'Admin contact pubkey (hex)', + description: + 'The Nostr hex (not npub) pubkey of the relay administrator', + required: false, + placeholder: 'hex (not npub) pubkey', + patterns: [ + { + regex: '[0-9a-fA-F]{64}', + description: + 'Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.', + }, + ], + }), + contact: Value.text({ + name: 'Admin contact email', + description: 'The email address of the relay administrator', + required: false, + patterns: [ + { + regex: '[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+', + description: 'Must be a valid email address.', + }, + ], + inputmode: 'email', + }), + }), + ), + limits: Value.object( + { + name: 'Limits', + description: + 'Data limits to protect your relay from using too many resources', + }, + Config.of({ + messages_per_sec: Value.number({ + name: 'Messages Per Second Limit', + description: + 'Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.', + required: { default: 2 }, + min: 1, + integer: true, + units: 'msgs/sec', + }), + subscriptions_per_min: Value.number({ + name: 'Subscriptions Per Minute Limit', + description: + 'Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.', + required: { default: 10 }, + min: 1, + integer: true, + units: 'subs/min', + }), + max_blocking_threads: Value.number({ + name: 'Max Blocking Threads', + description: + 'Maximum number of blocking threads used for database connections.', + required: { default: 16 }, + integer: true, + min: 1, + units: 'threads', + }), + max_event_bytes: Value.number({ + name: 'Max Event Size', + description: + 'Limit the maximum size of an EVENT message. Set to 0 for unlimited', + required: { default: 131_072 }, + integer: true, + min: 1, + units: 'bytes', + }), + max_ws_message_bytes: Value.number({ + name: 'Max Websocket Message Size', + description: 'Maximum WebSocket message in bytes.', + required: { default: 131_072 }, + integer: true, + min: 1, + units: 'bytes', + }), + max_ws_frame_bytes: Value.number({ + name: 'Max Websocket Frame Size', + description: 'Maximum WebSocket frame size in bytes.', + required: { default: 131_072 }, + integer: true, + min: 1, + units: 'bytes', + }), + event_kind_blacklist: Value.list( + List.number( + { + name: 'Event Kind Blacklist', + description: + 'Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds', + }, + { + integer: true, + min: 1, + placeholder: '30023', + }, + ), + ), + }), + ), +}) + +// combined union config - private or public +export const configSpec = Config.of({ + relayType: Value.union( + { + name: 'Relay Type', + description: + 'Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.', + warning: + 'Running a public relay carries risk. Your relay can be spammed, resulting in large amounts of disk usage.', + required: { default: null }, + }, + Variants.of({ + private: { name: 'Private', spec: privateConfig }, + public: { name: 'Public', spec: publicConfig }, + }), + ), +}) + +// This line is necessary to satisfy Typescript. Do not touch it +export type ConfigSpec = typeof configSpec.validator._TYPE diff --git a/startos/procedures/dependencies/dependencyConfig/index.ts b/startos/procedures/dependencies/dependencyConfig/index.ts new file mode 100644 index 0000000..bdd35f9 --- /dev/null +++ b/startos/procedures/dependencies/dependencyConfig/index.ts @@ -0,0 +1,4 @@ +import { sdk } from '../../../sdk' +import { configSpec } from '../../config/spec' + +export const dependencyConfig = sdk.setupDependencyConfig(configSpec, {}) diff --git a/startos/procedures/dependencies/dependencyMounts.ts b/startos/procedures/dependencies/dependencyMounts.ts new file mode 100644 index 0000000..90de154 --- /dev/null +++ b/startos/procedures/dependencies/dependencyMounts.ts @@ -0,0 +1,3 @@ +import { sdk } from '../../sdk' + +export const dependencyMounts = sdk.setupDependencyMounts() diff --git a/startos/procedures/index.ts b/startos/procedures/index.ts new file mode 100644 index 0000000..3cf62c5 --- /dev/null +++ b/startos/procedures/index.ts @@ -0,0 +1,7 @@ +export { getConfig, setConfig } from './config' +export { createBackup, restoreBackup } from './backups' +export { main } from './main' +export { init, uninit } from './init' +export { actions } from './actions' +export { dependencyConfig } from './dependencies/dependencyConfig' +export { dependencyMounts } from './dependencies/dependencyMounts' diff --git a/startos/procedures/init.ts b/startos/procedures/init.ts new file mode 100644 index 0000000..8b63142 --- /dev/null +++ b/startos/procedures/init.ts @@ -0,0 +1,24 @@ +import { sdk } from '../sdk' +import { setInterfaces } from './interfaces' +import { migrations } from './migrations' + +const install = sdk.setupInstall(async ({ effects, utils }) => { + await utils.childProcess.exec('chown -R $APP_USER:$APP_USER $APP_DATA') +}) + +const uninstall = sdk.setupUninstall(async ({ effects, utils }) => {}) + +const exportedValues = sdk.setupExports(({ effects, utils }) => { + return { + ui: [], + services: [], + } +}) + +export const { init, uninit } = sdk.setupInit( + migrations, + install, + uninstall, + setInterfaces, + exportedValues, +) diff --git a/startos/procedures/interfaces.ts b/startos/procedures/interfaces.ts new file mode 100644 index 0000000..d98cd2d --- /dev/null +++ b/startos/procedures/interfaces.ts @@ -0,0 +1,33 @@ +import { sdk } from '../sdk' +import { configSpec } from './config/spec' + +export const relayPort = 8080 +export const relayInterfaceId = 'relay' + +/** + * ======================== Interfaces ======================== + * + * In this section, you will decide how the service will be exposed to the outside world + */ +export const setInterfaces = sdk.setupInterfaces( + configSpec, + async ({ effects, utils, input }) => { + const multi = utils.host.multi('multi') + const multiOrigin = await multi.bindPort(relayPort, { protocol: 'ws' }) + const multiInterface = utils.createInterface({ + name: 'Relay Websocket', + id: relayInterfaceId, + description: 'Nostr clients use this interface to connect to the relay', + hasPrimary: false, + disabled: false, + ui: false, + username: null, + path: '', + search: {}, + }) + + const multiReceipt = await multiInterface.export([multiOrigin]) + + return [multiReceipt] + }, +) diff --git a/startos/procedures/main.ts b/startos/procedures/main.ts new file mode 100644 index 0000000..293813d --- /dev/null +++ b/startos/procedures/main.ts @@ -0,0 +1,41 @@ +import { sdk } from '../sdk' +import { ExpectedExports } from '@start9labs/start-sdk/lib/types' +import { manifest } from '../manifest' +import { HealthReceipt } from '@start9labs/start-sdk/lib/health/HealthReceipt' +import { Daemons } from '@start9labs/start-sdk/lib/mainFn/Daemons' +import { relayPort } from './interfaces' + +export const main: ExpectedExports.main = sdk.setupMain( + async ({ effects, utils, started }) => { + /** + * ======================== Setup ======================== + */ + + console.info('Starting Nostr RS Relay!') + + /** + * ======================== Additional Health Checks (optional) ======================== + */ + const healthReceipts: HealthReceipt[] = [] + + /** + * ======================== Daemons ======================== + */ + return Daemons.of({ + effects, + started, + healthReceipts, + }).addDaemon('ws', { + command: ['./nostr-rs-relay', '--db', '/data'], + requires: [], + ready: { + display: 'Service Ready', + fn: () => + utils.checkPortListening(relayPort, { + successMessage: `${manifest.title} is live`, + errorMessage: `${manifest.title} is unreachable`, + }), + }, + }) + }, +) diff --git a/startos/procedures/migrations/index.ts b/startos/procedures/migrations/index.ts new file mode 100644 index 0000000..792845a --- /dev/null +++ b/startos/procedures/migrations/index.ts @@ -0,0 +1,4 @@ +import { sdk } from '../../sdk' +import { v0_8_9 } from './v0_8_9' + +export const migrations = sdk.setupMigrations(v0_8_9) diff --git a/startos/procedures/migrations/v0_8_9.ts b/startos/procedures/migrations/v0_8_9.ts new file mode 100644 index 0000000..c5c862a --- /dev/null +++ b/startos/procedures/migrations/v0_8_9.ts @@ -0,0 +1,13 @@ +import { sdk } from '../../sdk' +import { rmdir } from 'fs/promises' + +export const v0_8_9 = sdk.Migration.of({ + version: '0.8.9', + up: async ({ effects, utils }) => { + // remove old start9 dir + await rmdir('/root/start9') + }, + down: async ({ effects, utils }) => { + throw new Error('Downgrade not permitted') + }, +}) diff --git a/startos/sdk.ts b/startos/sdk.ts new file mode 100644 index 0000000..bf3fe8f --- /dev/null +++ b/startos/sdk.ts @@ -0,0 +1,13 @@ +import { StartSdk } from '@start9labs/start-sdk/lib/StartSdk' +import { manifest } from './manifest' +import { Store } from './store' + +/** + * This is a static file that provides type safety throughout the codebase + * + * the exported sdk const should be used instead of StartSdk directly + */ +export const sdk = StartSdk.of() + .withManifest(manifest) + .withStore() + .build(true) diff --git a/startos/store.ts b/startos/store.ts new file mode 100644 index 0000000..5f7dab9 --- /dev/null +++ b/startos/store.ts @@ -0,0 +1 @@ +export type Store = {} diff --git a/startos/utils.ts b/startos/utils.ts new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b814af3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["startos/**/*.ts"], + "compilerOptions": { + "target": "es2022", + "module": "None", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +}