From 44784d4865bcd75c75394a09704781084ee4cb5c Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 20 Jul 2026 15:47:12 +0000 Subject: [PATCH] Use Commonlib for self-hosted setup tools --- .github/workflows/unit-ci.yml | 52 ++ devs.md | 2 + docs/setup_own_server.md | 38 +- package.json | 1 + updates.md | 5 + utils/couchdb/couchdb-init.sh | 47 +- utils/couchdb/provision.test.ts | 83 +++ utils/couchdb/provision.ts | 250 ++++++++ utils/flyio/deno.lock | 858 +++++++++++++++++++++++++- utils/flyio/deploy-server.sh | 37 +- utils/flyio/generate_setupuri.test.ts | 66 ++ utils/flyio/generate_setupuri.ts | 213 ++----- utils/flyio/setenv.sh | 24 +- utils/flyio/setenv.test.sh | 26 + utils/readme.md | 181 +----- 15 files changed, 1474 insertions(+), 409 deletions(-) create mode 100644 utils/couchdb/provision.test.ts create mode 100644 utils/couchdb/provision.ts create mode 100644 utils/flyio/generate_setupuri.test.ts create mode 100644 utils/flyio/setenv.test.sh diff --git a/.github/workflows/unit-ci.yml b/.github/workflows/unit-ci.yml index d8ec75a8..3061d2fd 100644 --- a/.github/workflows/unit-ci.yml +++ b/.github/workflows/unit-ci.yml @@ -23,6 +23,8 @@ on: - 'version-bump.mjs' - 'utils/release-*.mjs' - 'utils/release-*.unit.spec.ts' + - 'utils/couchdb/**' + - 'utils/flyio/**' - '.github/workflows/prepare-release.yml' - '.github/workflows/finalise-release.yml' - '.github/workflows/release.yml' @@ -44,6 +46,8 @@ on: - 'version-bump.mjs' - 'utils/release-*.mjs' - 'utils/release-*.unit.spec.ts' + - 'utils/couchdb/**' + - 'utils/flyio/**' - '.github/workflows/prepare-release.yml' - '.github/workflows/finalise-release.yml' - '.github/workflows/release.yml' @@ -53,6 +57,54 @@ permissions: contents: read jobs: + setup-tools: + name: Self-hosted setup tools + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24.x' + cache: 'npm' + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Install dependencies + run: npm ci + + - name: Run setup-tool contract tests + run: npm run test:setup-tools + + - name: Create CouchDB test configuration + run: | + cat < .test.env + hostname=http://127.0.0.1:5989/ + dbname=livesync-test-db + username=admin + password=testpassword + EOF + + - name: Start CouchDB + run: npm run test:docker-couchdb:start + + - name: Provision a versioned LiveSync database + run: npx dotenv-cli -e .test.env -- env database=setup-tools-ci retry_count=1 retry_delay_ms=0 ./utils/couchdb/couchdb-init.sh + + - name: Verify the Commonlib database version + run: | + npx dotenv-cli -e .test.env -- bash -lc 'curl --fail --silent --show-error --user "${username}:${password}" "${hostname}/setup-tools-ci/obsydian_livesync_version" | node --input-type=module -e "import { VER } from \"@vrtmrz/livesync-commonlib/compat/common/types\";let input=\"\";process.stdin.on(\"data\",chunk=>input+=chunk).on(\"end\",()=>{const document=JSON.parse(input);if(document.type!==\"versioninfo\"||document.version!==VER)throw new Error(\"Unexpected LiveSync database version\");})"' + + - name: Stop CouchDB + if: always() + run: npm run test:docker-couchdb:stop || true + unit-test: name: Unit Tests runs-on: ubuntu-latest diff --git a/devs.md b/devs.md index 3f843042..f48af9b1 100644 --- a/devs.md +++ b/devs.md @@ -36,6 +36,7 @@ npm run dev # Development build with auto-rebuild (uses .env for test v npm run build # Production build npm run buildDev # Development build (one-time) npm run test:integration # Run CouchDB-backed integration tests +npm run test:setup-tools # Check provisioning and Setup URI package contracts npm run test:e2e:cli:p2p # Run canonical P2P validation in Compose npm run test:e2e:obsidian:local-suite # Run the real Obsidian local suite ``` @@ -67,6 +68,7 @@ To facilitate development and testing, the build process can automatically copy - If you add a feature that interacts with the remote database (e.g., replication changes, custom changes feed parameters, or custom HTTP queries), you strongly expected to write an integration test to verify the behaviour against a real CouchDB server. - **Commonlib Tests**: Commonlib owns unit and package tests for shared RPC, storage, replication, and platform contracts. LiveSync CI verifies the exact packed dependency as a downstream consumer. - **CLI E2E** (`src/apps/cli/testdeno/`): Host-independent consumer workflows. The canonical Compose P2P suite covers ordinary two-peer synchronisation, replacement of the current replicator followed by transfer with the same peer, and explicit relay disconnection followed by paused and resumed reconnection. Its lifecycle entry point is included only in the Docker test build and does not add a public CLI command. Run `npm run test:e2e:cli` for the ordinary suite or `npm run test:e2e:cli:p2p` for P2P validation. +- **Self-hosted setup tools** (`utils/couchdb/` and `utils/flyio/`): Deno contract tests consume the exact locked Commonlib registry package, verify current self-hosted Setup URI defaults and remote profiles, and keep CouchDB administration separate from package-owned LiveSync database-version negotiation. `unit-ci` also provisions a real temporary CouchDB database and verifies its version document against the installed Commonlib package. Run `npm run test:setup-tools` for the local contract gate. - **Real Obsidian E2E** (`test/e2e-obsidian/`): Local-first scripts that launch real Obsidian with temporary vaults and the built Self-hosted LiveSync plug-in. Use these for boot-up sequence, vault reflection, RedFlag flows, Fast Setup (Simple Fetch), settings dialogues, restart-sensitive workflows, Object Storage regressions, and other behaviour that depends on Obsidian itself. Run focused scripts such as `npm run test:e2e:obsidian:two-vault-sync`, or use `npm run test:e2e:obsidian:local-suite:services` to run the broader local suite with CouchDB and MinIO fixtures managed by the wrapper. - **Docker Services**: Service-backed tests use CouchDB and MinIO (S3). Canonical P2P validation owns its relay through the CLI Compose runner: diff --git a/docs/setup_own_server.md b/docs/setup_own_server.md index 9db4a594..44aa9b48 100644 --- a/docs/setup_own_server.md +++ b/docs/setup_own_server.md @@ -33,7 +33,7 @@ ```bash # Adding environment variables. -export hostname=localhost:5984 +export hostname=http://localhost:5984 export username=goojdasjdas #Please change as you like. export password=kpkdasdosakpdsa #Please change as you like @@ -115,31 +115,27 @@ Congrats, move on to [step 2](#2-run-couchdb-initsh-for-initialise) Please refer to the [official document](https://docs.couchdb.org/en/stable/install/index.html). However, we do not have to configure it fully. Just the administrator needs to be configured. ## 2. Run couchdb-init.sh for initialise + +Deno 2 is required. Export the CouchDB connection and database details, then run the provisioning wrapper: + ``` +export hostname=http://localhost:5984 +export username= +export password= +export database=obsidiannotes curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | bash ``` If it results like the following: ``` --- Configuring CouchDB by REST APIs... --> -{"ok":true} -"" -"" -"" -"" -"" -"" -"" -"" -"" -<-- Configuring CouchDB by REST APIs Done! +CouchDB provisioning completed. ``` -Your CouchDB has been initialised successfully. If you want this manually, please read the script. +The wrapper runs the exact registry-pinned Commonlib consumer. When `database` is supplied, it creates the database and initialises its LiveSync database-version document through Commonlib. Without `database`, it configures only the CouchDB server. If you are using Docker Compose and the above command does not work or displays `ERROR: Hostname missing`, you can try running the following command, replacing the placeholders with your own values: ``` -curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | hostname=http://:5984 username= password= bash +curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | hostname=http://:5984 username= password= database=obsidiannotes bash ``` ## 3. Expose CouchDB to the Internet @@ -177,10 +173,10 @@ Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our serv ```bash export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com #Point to your vault export database=obsidiannotes #Please change as you like -export passphrase=dfsapkdjaskdjasdas #Please change as you like +export passphrase= export username=johndoe -export password=abc123 -deno run -A https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts +export password= +deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts ``` > [!TIP] @@ -188,14 +184,14 @@ deno run -A https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/util > Yes, the `passphrase` we have exported now is for an End-to-End Encryption passphrase. > And, `uri_passphrase` that used in the `generate_setupuri.ts` is a different one; for decrypting Set-up URI at using that. > Why: I (vorotamoroz) think that the passphrase of the Setup-URI should be different from the E2EE passphrase to prevent exposure caused by operational errors or the possibility of evil in our environment. On top of that, I believe that it is desirable for the Setup-URI to be random. Setup-URI is inevitably long, so it goes through the clipboard. I think that its passphrase should not go through the same path, so it should essentially be typed manually. -> Hence, if we keep empty for uri_passphrase, generate_setupuri.ts generates an adjective-noun-randomnumber passphrase so that we can remember it without going through the clipboard. +> If `uri_passphrase` is empty, `generate_setupuri.ts` generates a cryptographically random passphrase and prints it once. You will then get the following output: ```bash obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D -Your passphrase of Setup-URI is: patient-haze +Your passphrase for the Setup URI is: H7vX...a-random-32-character-value This passphrase is never shown again, so please note it in a safe place. ``` @@ -205,7 +201,7 @@ Please keep your passphrase of Setup-URI. [This video](https://youtu.be/7sa_I1832Xc?t=146) may help us. 1. Install Self-hosted LiveSync 2. Choose `Use the copied setup URI` from the command palette and paste the setup URI. (obsidian://setuplivesync?settings=.....). -3. Type the previously displayed passphrase (`patient-haze`) for setup-uri passphrase. +3. Type the Setup URI passphrase printed by the generator. 4. Answer `yes` and `Set it up...`, and finish the first dialogue with `Keep them disabled`. 5. `Reload app without save` once. diff --git a/package.json b/package.json index a7536e90..0ee50f5a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run svelte-check && npm run check:compatibility", "unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/", "test:unit": "vitest run --config vitest.config.unit.ts", + "test:setup-tools": "bash utils/flyio/setenv.test.sh && deno test -A --config=utils/flyio/deno.jsonc --frozen --lock=utils/flyio/deno.lock utils/couchdb/provision.test.ts utils/flyio/generate_setupuri.test.ts", "test:contract:contexts": "vitest run --config vitest.config.unit.ts src/modules/services/ObsidianServiceContext.unit.spec.ts src/apps/cli/services/NodeServiceContext.unit.spec.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts", "test:contract:context:webapp": "vitest run --config vitest.config.unit.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts", "test:contract:context:cli": "npm run build --workspace self-hosted-livesync-cli && deno task --cwd src/apps/cli/testdeno test:setup-put-cat", diff --git a/updates.md b/updates.md index 21ba45e4..debfef06 100644 --- a/updates.md +++ b/updates.md @@ -17,6 +17,11 @@ Earlier releases remain available in the [0.25 release history](docs/releases/0. - Wizard-driven new-device and existing-device setup now reserves Rebuild or Fetch before enabling imported settings, preventing ordinary start-up work from running ahead of the selected initialisation. - Manual onboarding now creates and selects CouchDB, Object Storage, and P2P remote profiles directly while preserving existing profiles. Current Setup URIs retain profile names and selections, while older flat settings remain supported through compatibility migration. - P2P panes and explicit rebuild actions now use the current transport after settings or database replacement. Disconnecting leaves the LiveSync room and closes signalling relay sockets without destroying Trystero-owned shared peers. +- Self-hosted CouchDB provisioning and Setup URI generation now consume the immutable Commonlib registry package. New database provisioning records the package-owned database version, and generated URIs use current new-Vault defaults and a selected CouchDB profile instead of legacy embedded settings. + +### Security + +- Fly.io setup now generates CouchDB and Vault encryption secrets from cryptographically secure randomness instead of short word combinations. ## 1.0.0-rc.0 diff --git a/utils/couchdb/couchdb-init.sh b/utils/couchdb/couchdb-init.sh index 323c13f3..3e41c3f8 100755 --- a/utils/couchdb/couchdb-init.sh +++ b/utils/couchdb/couchdb-init.sh @@ -1,33 +1,26 @@ #!/bin/bash -if [[ -z "$hostname" ]]; then - echo "ERROR: Hostname missing" - exit 1 -fi -if [[ -z "$username" ]]; then - echo "ERROR: Username missing" +set -euo pipefail + +if ! command -v deno >/dev/null 2>&1; then + echo "ERROR: Deno is required to run the Commonlib-backed CouchDB provisioning tool." >&2 exit 1 fi -if [[ -z "$password" ]]; then - echo "ERROR: Password missing" - exit 1 -fi -if [[ -z "$node" ]]; then - echo "INFO: defaulting to _local" - node=_local +script_url="${provision_script_url:-https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/provision.ts}" +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)" +deno_dependency_options=() +if [[ -n "$script_dir" && -f "$script_dir/provision.ts" ]]; then + script_url="$script_dir/provision.ts" + lockfile="$script_dir/../flyio/deno.lock" + deno_config="$script_dir/../flyio/deno.jsonc" + if [[ -f "$lockfile" && -f "$deno_config" ]]; then + deno_dependency_options+=("--config=$deno_config" --frozen "--lock=$lockfile") + fi fi -echo "-- Configuring CouchDB by REST APIs... -->" - -until (curl -X POST "${hostname}/_cluster_setup" -H "Content-Type: application/json" -d "{\"action\":\"enable_single_node\",\"username\":\"${username}\",\"password\":\"${password}\",\"bind_address\":\"0.0.0.0\",\"port\":5984,\"singlenode\":true}" --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd_auth/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/httpd/WWW-Authenticate" -H "Content-Type: application/json" -d '"Basic realm=\"couchdb\""' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/httpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd/max_http_request_size" -H "Content-Type: application/json" -d '"4294967296"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/couchdb/max_document_size" -H "Content-Type: application/json" -d '"50000000"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/cors/credentials" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/cors/origins" -H "Content-Type: application/json" -d '"app://obsidian.md,capacitor://localhost,http://localhost"' --user "${username}:${password}"); do sleep 5; done - -echo "<-- Configuring CouchDB by REST APIs Done!" +exec deno run \ + --minimum-dependency-age=0 \ + "${deno_dependency_options[@]}" \ + --allow-env \ + --allow-net \ + "$script_url" diff --git a/utils/couchdb/provision.test.ts b/utils/couchdb/provision.test.ts new file mode 100644 index 00000000..c4942d16 --- /dev/null +++ b/utils/couchdb/provision.test.ts @@ -0,0 +1,83 @@ +import { provisionCouchDB } from "./provision.ts"; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +Deno.test("configures CouchDB and delegates database-version initialisation", async () => { + const requests: Array<{ url: string; method: string; body: string }> = []; + const initialisations: Array<[string, string, string]> = []; + await provisionCouchDB( + { + hostname: "https://couch.example.test/", + username: "alice", + password: "secret", + database: "notes", + retryCount: 1, + retryDelayMs: 0, + }, + { + fetch: async (input, init) => { + requests.push({ + url: String(input), + method: init?.method ?? "GET", + body: String(init?.body ?? ""), + }); + return new Response("{}", { status: 201 }); + }, + sleep: async () => {}, + initialiseDatabaseVersion: async (...args) => { + initialisations.push(args); + }, + }, + ); + + assert( + requests[0].url.endsWith("/_cluster_setup"), + "cluster setup was not first", + ); + assert( + requests.some((request) => + request.url.endsWith("/_config/cors/origins") && + request.body.includes("app://obsidian.md") + ), + "the Obsidian CORS origins were not configured", + ); + assert( + requests.at(-1)?.url === "https://couch.example.test/notes", + "the requested database was not created last", + ); + assert( + initialisations.length === 1, + "database-version initialisation was not delegated once", + ); + assert( + initialisations[0][0] === "https://couch.example.test/notes", + "database-version initialisation used the wrong URL", + ); +}); + +Deno.test("leaves database creation to the client when no database is supplied", async () => { + let initialised = false; + await provisionCouchDB( + { + hostname: "http://127.0.0.1:5984", + username: "admin", + password: "secret", + retryCount: 1, + retryDelayMs: 0, + }, + { + fetch: async () => new Response("{}", { status: 200 }), + sleep: async () => {}, + initialiseDatabaseVersion: async () => { + initialised = true; + }, + }, + ); + + assert( + !initialised, + "database-version initialisation ran without a database", + ); +}); diff --git a/utils/couchdb/provision.ts b/utils/couchdb/provision.ts new file mode 100644 index 00000000..9a76b6cc --- /dev/null +++ b/utils/couchdb/provision.ts @@ -0,0 +1,250 @@ +import { PouchDB } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/pouchdb/pouchdb-browser"; +import { checkRemoteVersion } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/pouchdb/negotiation"; + +export interface CouchDBProvisioningOptions { + hostname: string; + username: string; + password: string; + node?: string; + database?: string; + origins?: string; + retryCount?: number; + retryDelayMs?: number; +} + +interface ProvisioningDependencies { + fetch: typeof fetch; + sleep: (milliseconds: number) => Promise; + initialiseDatabaseVersion: ( + databaseURL: string, + username: string, + password: string, + ) => Promise; +} + +const DEFAULT_ORIGINS = + "app://obsidian.md,capacitor://localhost,http://localhost"; + +function requireValue(value: string, name: string): string { + const trimmed = value.trim(); + if (!trimmed) throw new Error(`${name} is required`); + return trimmed; +} + +function normaliseHostname(hostname: string): string { + const parsed = new URL(requireValue(hostname, "hostname")); + parsed.pathname = parsed.pathname.replace(/\/+$/, ""); + parsed.search = ""; + parsed.hash = ""; + return parsed.href.replace(/\/$/, ""); +} + +function validateDatabaseName(database: string): string { + const trimmed = database.trim(); + if (!/^[a-z][a-z0-9_$()+-]*$/.test(trimmed)) { + throw new Error( + "database must begin with a lower-case letter and contain only lower-case letters, digits, _, $, (, ), +, or -", + ); + } + return trimmed; +} + +function basicAuthorisation(username: string, password: string): string { + return `Basic ${btoa(`${username}:${password}`)}`; +} + +async function requestWithRetry( + dependencies: ProvisioningDependencies, + label: string, + url: string, + init: RequestInit, + accept: (response: Response, body: string) => boolean, + retryCount: number, + retryDelayMs: number, +): Promise { + let lastError: unknown; + for (let attempt = 1; attempt <= retryCount; attempt++) { + try { + const response = await dependencies.fetch(url, init); + const body = await response.text(); + if (accept(response, body)) return; + const error = new Error( + `${label} failed with HTTP ${response.status}: ${body}`, + ); + if (response.status < 500) throw error; + lastError = error; + } catch (error) { + lastError = error; + if ( + error instanceof Error && + error.message.startsWith(`${label} failed with HTTP 4`) + ) { + throw error; + } + } + if (attempt < retryCount) await dependencies.sleep(retryDelayMs); + } + throw lastError instanceof Error + ? lastError + : new Error(`${label} failed after ${retryCount} attempts`); +} + +export async function initialiseLiveSyncDatabaseVersion( + databaseURL: string, + username: string, + password: string, +): Promise { + const database = new PouchDB(databaseURL, { + adapter: "http", + auth: { username, password }, + skip_setup: true, + }); + try { + const compatible = await checkRemoteVersion( + database, + async () => false, + ); + if (!compatible) { + throw new Error( + "the remote database uses an incompatible LiveSync database version", + ); + } + } finally { + await database.close(); + } +} + +export async function provisionCouchDB( + options: CouchDBProvisioningOptions, + overrides: Partial = {}, +): Promise { + const hostname = normaliseHostname(options.hostname); + const username = requireValue(options.username, "username"); + const password = requireValue(options.password, "password"); + const node = encodeURIComponent(options.node?.trim() || "_local"); + const origins = options.origins?.trim() || DEFAULT_ORIGINS; + const retryCount = options.retryCount ?? 12; + const retryDelayMs = options.retryDelayMs ?? 5_000; + if (!Number.isInteger(retryCount) || retryCount < 1) { + throw new Error("retryCount must be a positive integer"); + } + if (!Number.isFinite(retryDelayMs) || retryDelayMs < 0) { + throw new Error("retryDelayMs must be zero or greater"); + } + + const dependencies: ProvisioningDependencies = { + fetch, + sleep: (milliseconds) => + new Promise((resolve) => setTimeout(resolve, milliseconds)), + initialiseDatabaseVersion: initialiseLiveSyncDatabaseVersion, + ...overrides, + }; + const headers = { + "Content-Type": "application/json", + Authorization: basicAuthorisation(username, password), + }; + const configure = async ( + label: string, + path: string, + body: string, + method = "PUT", + accept: (response: Response, body: string) => boolean = (response) => + response.ok, + ) => + await requestWithRetry( + dependencies, + label, + `${hostname}${path}`, + { method, headers, body }, + accept, + retryCount, + retryDelayMs, + ); + + await configure( + "single-node cluster setup", + "/_cluster_setup", + JSON.stringify({ + action: "enable_single_node", + username, + password, + bind_address: "0.0.0.0", + port: 5984, + singlenode: true, + }), + "POST", + (response, body) => + response.ok || + ((response.status === 400 || response.status === 409) && + /already|finished/i.test(body)), + ); + + const settings: Array<[string, string, string]> = [ + ["require authenticated HTTP users", "chttpd/require_valid_user", '"true"'], + [ + "require authenticated HTTP users for authentication", + "chttpd_auth/require_valid_user", + '"true"', + ], + [ + "set the HTTP authentication challenge", + "httpd/WWW-Authenticate", + '"Basic realm=\\"couchdb\\""', + ], + ["enable HTTP CORS", "httpd/enable_cors", '"true"'], + ["enable clustered HTTP CORS", "chttpd/enable_cors", '"true"'], + [ + "set the maximum HTTP request size", + "chttpd/max_http_request_size", + '"4294967296"', + ], + [ + "set the maximum document size", + "couchdb/max_document_size", + '"50000000"', + ], + ["enable CORS credentials", "cors/credentials", '"true"'], + ["set allowed CORS origins", "cors/origins", JSON.stringify(origins)], + ]; + for (const [label, key, body] of settings) { + await configure(label, `/_node/${node}/_config/${key}`, body); + } + + if (options.database?.trim()) { + const database = validateDatabaseName(options.database); + const databaseURL = `${hostname}/${encodeURIComponent(database)}`; + await requestWithRetry( + dependencies, + "create database", + databaseURL, + { method: "PUT", headers }, + (response) => response.ok || response.status === 412, + retryCount, + retryDelayMs, + ); + await dependencies.initialiseDatabaseVersion( + databaseURL, + username, + password, + ); + } +} + +function optionalNumber(name: string): number | undefined { + const value = Deno.env.get(name)?.trim(); + return value ? Number(value) : undefined; +} + +if (import.meta.main) { + await provisionCouchDB({ + hostname: Deno.env.get("hostname") ?? "", + username: Deno.env.get("username") ?? "", + password: Deno.env.get("password") ?? "", + node: Deno.env.get("node"), + database: Deno.env.get("database"), + origins: Deno.env.get("origins"), + retryCount: optionalNumber("retry_count"), + retryDelayMs: optionalNumber("retry_delay_ms"), + }); + console.log("CouchDB provisioning completed."); +} diff --git a/utils/flyio/deno.lock b/utils/flyio/deno.lock index 30d3c5eb..7ca3f1f5 100644 --- a/utils/flyio/deno.lock +++ b/utils/flyio/deno.lock @@ -1,25 +1,855 @@ { - "version": "4", + "version": "5", "specifiers": { - "npm:octagonal-wheels@0.1.11": "0.1.11" + "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4": "0.1.0-rc.4" }, "npm": { - "idb@8.0.0": { - "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" - }, - "octagonal-wheels@0.1.11": { - "integrity": "sha512-KsXfpziFHmlLEBe5VAXFz9OyyjJEEdSg7xxASqdzmbe5oo9dhcOeWGrQfyipRJwHAhlFkI4vEf8JCSgkcyRxYg==", + "@aws-sdk/checksums@3.1000.18": { + "integrity": "sha512-IImkbEyXdV6/uaF5r6Wkk+8718mQw1ll83j0a4a30R3JM/rHVFdWAiT4jtJpFjJiIwM/oJ6SxIxr0z2TaQUGqw==", "dependencies": [ - "idb", - "xxhash-wasm@0.4.2", - "xxhash-wasm-102@npm:xxhash-wasm@1.0.2" + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" ] }, - "xxhash-wasm@0.4.2": { - "integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA==" + "@aws-sdk/client-s3@3.1090.0": { + "integrity": "sha512-R6GX9cd1jljwzZ8xFmgAI/hHCuX1MobIKBdsymv7WL9SENvO9Vgz9KOR6avTnu0Ao+w1LmxnTe+jqmZXEn7Q/Q==", + "dependencies": [ + "@aws-sdk/checksums", + "@aws-sdk/core", + "@aws-sdk/credential-provider-node", + "@aws-sdk/middleware-sdk-s3", + "@aws-sdk/signature-v4-multi-region", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/node-http-handler", + "@smithy/types", + "tslib" + ] }, - "xxhash-wasm@1.0.2": { - "integrity": "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==" + "@aws-sdk/core@3.975.3": { + "integrity": "sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA==", + "dependencies": [ + "@aws-sdk/types", + "@aws-sdk/xml-builder", + "@aws/lambda-invoke-store", + "@smithy/core", + "@smithy/signature-v4", + "@smithy/types", + "bowser", + "tslib" + ] + }, + "@aws-sdk/credential-provider-env@3.972.59": { + "integrity": "sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-http@3.972.61": { + "integrity": "sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/node-http-handler", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-ini@3.973.4": { + "integrity": "sha512-e6ZvVsj90aRALf1kHP+J4iqC1496ZpVgqI/+u0LJ5HL7q7ATauGy4gdDvRCP13L1pN/fMiZLah162PGIYkbUVQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-login", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/credential-provider-imds", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-login@3.972.66": { + "integrity": "sha512-g2fsqm87r/nKthLZ0VkkDBElkGg0PvSa8d97HQ6EilMbJTZ6hxa8FxkSZyJfgPfFdZn0TTmkOffQmTSUcAHIng==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-node@3.972.70": { + "integrity": "sha512-3xzvkGdykBunxqh8WudmUpSyLWvIhfI6aBQo1b5rb3mDO5mNLadK+0hiI0qBQBMVynJbfLO+Ajy9dztMwy9O8w==", + "dependencies": [ + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-ini", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@aws-sdk/types", + "@smithy/core", + "@smithy/credential-provider-imds", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-process@3.972.59": { + "integrity": "sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-sso@3.973.3": { + "integrity": "sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/token-providers", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-web-identity@3.972.65": { + "integrity": "sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/middleware-sdk-s3@3.972.64": { + "integrity": "sha512-RBi43anhDBUv+HCfxCOXwGOE7GmT4n7ChV04Mwr22RhXTNcamW/iWnJlOotDPCZSrJ4dEvhZSiWWQMwLX+ZhFA==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/signature-v4-multi-region", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/nested-clients@3.997.33": { + "integrity": "sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/signature-v4-multi-region", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/node-http-handler", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/signature-v4-multi-region@3.996.41": { + "integrity": "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==", + "dependencies": [ + "@aws-sdk/types", + "@smithy/signature-v4", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/token-providers@3.1088.0": { + "integrity": "sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/types@3.974.2": { + "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", + "dependencies": [ + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/xml-builder@3.972.36": { + "integrity": "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==", + "dependencies": [ + "@smithy/types", + "tslib" + ] + }, + "@aws/lambda-invoke-store@0.3.0": { + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==" + }, + "@noble/secp256k1@3.1.0": { + "integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==" + }, + "@smithy/core@3.29.5": { + "integrity": "sha512-i0dk2t5B+CwV/dcJdUHILYkOQF5lof8f44dFCfDWToGCxjT9YQ+CgHqTAvJxzc3+zqQwm2QtVoJ5IqiNar/CnQ==", + "dependencies": [ + "@smithy/types", + "tslib" + ] + }, + "@smithy/credential-provider-imds@4.4.10": { + "integrity": "sha512-MJenAe4OKRZUo1LdYYFDCsSHxaHvInIU/z52GsheO9vl1/VSySVCr0zkyKD6TFiGkSUaWGxvKZ/70OvgUZR5HQ==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/fetch-http-handler@5.6.7": { + "integrity": "sha512-3zpg8yqqyXzoK2TsRDdkqVOj2RDBFfLXwCczOZ5c7TWB4eiaebfSCsbMjDPYB3PJ9ihV62QaeadZ+wLadZtNGA==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/md5-js@4.4.10": { + "integrity": "sha512-XI5xhWxRkWuiLNj0/Z30vLTPpZe9UQcg57Ox/n4vzGmFiHSrD+xAmx6ubEcWt6u69IOH+4LAucpaZk5AMxB8oQ==", + "dependencies": [ + "@smithy/core", + "tslib" + ] + }, + "@smithy/middleware-apply-body-checksum@4.5.10": { + "integrity": "sha512-1qFwlILFq+QnI1oqZIBpLda1W6oCkI667GPTyAn0eRan72ddQ/zA1CoKiIezTHLQKxINpPvDKVwsw/znLmKEbg==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/node-http-handler@4.9.7": { + "integrity": "sha512-wCU8HCLjAtAVqxxe0j2xff9LcEPw3yjBbg5IdQDIYFnxnPxbxcSLc7rgex7kqm9L/WYOnJEgaWQlfDkZleozMA==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/signature-v4@5.6.6": { + "integrity": "sha512-efP6DN3UTFrzIsGO42/xcabv8jU7+9nwEdphFUH7yL0k010ERyAWaO41KFQIDLcFZLZ8xzIQr4wplFxNzslSGQ==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/types@4.16.1": { + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", + "dependencies": [ + "tslib" + ] + }, + "@smithy/util-retry@4.5.10": { + "integrity": "sha512-hYu5ieq8myuO29xCQV0IIhicRm1aO0lcNBeMcF1mVVAVQC0oylPGKFliMq5NbxtdOvRxJWCMuir2gwKI91f1lw==", + "dependencies": [ + "@smithy/core", + "tslib" + ] + }, + "@trystero-p2p/core@0.25.3": { + "integrity": "sha512-lQKNq/ha+vF6kQZrpaXJGzlzxLF/Fhizoy5dwJUYQlkqRrbPup/Jov2EpPX/CPr2X+f79HxVzonkeni3MxmCuQ==" + }, + "@trystero-p2p/nostr@0.25.3": { + "integrity": "sha512-nZV9Fl/GXuhIkJSQ+wIkdMoRLO1oSTUL/+vZorukaojeAdOpT/61e8b9Vzbt8L1ktMTaGPEJHw2BG/B5BCwBuw==", + "dependencies": [ + "@noble/secp256k1", + "@trystero-p2p/core" + ] + }, + "@vrtmrz/livesync-commonlib@0.1.0-rc.4": { + "integrity": "sha512-u4FdbjnYg7lAf38z7eUv4eq4vxEdrl4rFMxiDZiJ7T701awKiflkXGIJQnaHdLaMa3zkRBU15qqEo7GtextmlA==", + "dependencies": [ + "@aws-sdk/client-s3", + "@smithy/fetch-http-handler", + "@smithy/md5-js", + "@smithy/middleware-apply-body-checksum", + "@smithy/types", + "@smithy/util-retry", + "@trystero-p2p/nostr", + "diff-match-patch", + "events", + "fflate", + "idb", + "markdown-it", + "minimatch", + "octagonal-wheels", + "pouchdb-adapter-http", + "pouchdb-adapter-idb", + "pouchdb-adapter-indexeddb", + "pouchdb-adapter-memory", + "pouchdb-core", + "pouchdb-errors", + "pouchdb-find", + "pouchdb-mapreduce", + "pouchdb-merge", + "pouchdb-replication", + "pouchdb-utils", + "qrcode-generator", + "transform-pouch", + "xxhash-wasm-102@npm:xxhash-wasm@1.1.0" + ] + }, + "abstract-leveldown@2.7.2": { + "integrity": "sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w==", + "dependencies": [ + "xtend" + ], + "deprecated": true + }, + "abstract-leveldown@6.2.3": { + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "dependencies": [ + "buffer", + "immediate", + "level-concat-iterator", + "level-supports", + "xtend" + ], + "deprecated": true + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "balanced-match@4.0.4": { + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bowser@2.14.1": { + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" + }, + "brace-expansion@5.0.7": { + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dependencies": [ + "balanced-match" + ] + }, + "buffer@5.7.1": { + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dependencies": [ + "base64-js", + "ieee754" + ] + }, + "core-util-is@1.0.3": { + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "deferred-leveldown@5.3.0": { + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "dependencies": [ + "abstract-leveldown@6.2.3", + "inherits" + ], + "deprecated": true + }, + "diff-match-patch@1.0.5": { + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, + "double-ended-queue@2.1.0-0": { + "integrity": "sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==" + }, + "entities@4.5.0": { + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "errno@0.1.8": { + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dependencies": [ + "prr" + ], + "bin": true + }, + "events@3.3.0": { + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fetch-cookie@2.2.0": { + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "dependencies": [ + "set-cookie-parser", + "tough-cookie" + ] + }, + "fflate@0.8.3": { + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==" + }, + "functional-red-black-tree@1.0.1": { + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==" + }, + "idb@8.0.3": { + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==" + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "immediate@3.3.0": { + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "isarray@0.0.1": { + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "level-codec@9.0.2": { + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "dependencies": [ + "buffer" + ], + "deprecated": true + }, + "level-concat-iterator@2.0.1": { + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", + "deprecated": true + }, + "level-errors@2.0.1": { + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "dependencies": [ + "errno" + ], + "deprecated": true + }, + "level-iterator-stream@4.0.2": { + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "dependencies": [ + "inherits", + "readable-stream@3.6.2", + "xtend" + ] + }, + "level-supports@1.0.1": { + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "dependencies": [ + "xtend" + ] + }, + "levelup@4.4.0": { + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "dependencies": [ + "deferred-leveldown", + "level-errors", + "level-iterator-stream", + "level-supports", + "xtend" + ], + "deprecated": true + }, + "linkify-it@5.0.2": { + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dependencies": [ + "uc.micro" + ] + }, + "ltgt@2.2.1": { + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==" + }, + "markdown-it@14.3.0": { + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dependencies": [ + "argparse", + "entities", + "linkify-it", + "mdurl", + "punycode.js", + "uc.micro" + ], + "bin": true + }, + "mdurl@2.0.0": { + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "memdown@1.4.1": { + "integrity": "sha512-iVrGHZB8i4OQfM155xx8akvG9FIj+ht14DX5CQkCTG4EHzZ3d3sgckIf/Lm9ivZalEsFuEVnWv2B2WZvbrro2w==", + "dependencies": [ + "abstract-leveldown@2.7.2", + "functional-red-black-tree", + "immediate", + "inherits", + "ltgt", + "safe-buffer@5.1.2" + ], + "deprecated": true + }, + "minimatch@10.2.5": { + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dependencies": [ + "brace-expansion" + ] + }, + "node-fetch@2.6.9": { + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dependencies": [ + "whatwg-url" + ] + }, + "octagonal-wheels@0.1.51": { + "integrity": "sha512-KTlfqKPjobHJg/t3A539srnFf+VHr1aXkHSmsNDDpiI5UFC7FamZ95dWpJfGE2EI/HULR5hveQDgkazmz8SAcg==", + "dependencies": [ + "idb" + ] + }, + "pouchdb-abstract-mapreduce@9.0.0": { + "integrity": "sha512-SnTtqwAEiAa3uxKbc1J7LfiBViwEkKe2xkK92zxyTXPqWBvMnh4UU3GXxx7GrXTM4L9llsQ3lSjpbH4CNqG1Mw==", + "dependencies": [ + "pouchdb-binary-utils", + "pouchdb-collate", + "pouchdb-errors", + "pouchdb-fetch", + "pouchdb-mapreduce-utils", + "pouchdb-md5", + "pouchdb-utils" + ] + }, + "pouchdb-adapter-http@9.0.0": { + "integrity": "sha512-2eL008XeRZkdyp3hMHHOhdIPqK9H6Mn4SLlQvit4zCbqnOFfAswzPjUmHULGMbDUCrQBTu6y82FnV6NHXv9kgw==", + "dependencies": [ + "pouchdb-binary-utils", + "pouchdb-errors", + "pouchdb-fetch", + "pouchdb-utils" + ] + }, + "pouchdb-adapter-idb@9.0.0": { + "integrity": "sha512-2oLlgwMyOQwdKuzrEmOv8T7jFVgX7JgT4Cr81zX3eiiRClp7xXGgjv41ZRdVCAbM530sIN8BudafaQRVFKRVmA==", + "dependencies": [ + "pouchdb-adapter-utils", + "pouchdb-binary-utils", + "pouchdb-errors", + "pouchdb-json", + "pouchdb-merge", + "pouchdb-utils" + ] + }, + "pouchdb-adapter-indexeddb@9.0.0": { + "integrity": "sha512-/mcCbnVR0VKwtVZWKf8lVSdADLD0yApjFudu4d+0jeLWAeBSGZBRKYlogz2PGs4uTA7GVc2TXjVCNGUdkCM9ZQ==", + "dependencies": [ + "pouchdb-adapter-utils", + "pouchdb-binary-utils", + "pouchdb-errors", + "pouchdb-md5", + "pouchdb-merge", + "pouchdb-utils" + ] + }, + "pouchdb-adapter-leveldb-core@9.0.0": { + "integrity": "sha512-b3ZGPtVXyivGL5SK3AIDG7PrNsZdoDpGFkmTytDTtctkVhxOg71gnXXP+CrupENPqSNG/eGbKW4w+bbMpxy6aA==", + "dependencies": [ + "double-ended-queue", + "levelup", + "pouchdb-adapter-utils", + "pouchdb-binary-utils", + "pouchdb-core", + "pouchdb-errors", + "pouchdb-json", + "pouchdb-md5", + "pouchdb-merge", + "pouchdb-utils", + "sublevel-pouchdb", + "through2" + ] + }, + "pouchdb-adapter-memory@9.0.0": { + "integrity": "sha512-XbCwJ5f5U9dGdkiDikzYjTebdPHuA6Ghylx1Pq0lDe4y6l8R9xhjDSUy56pJ8G2F4Z+8QdB5FBY9EQoFlFSXWQ==", + "dependencies": [ + "memdown", + "pouchdb-adapter-leveldb-core" + ] + }, + "pouchdb-adapter-utils@9.0.0": { + "integrity": "sha512-hmbm4ey0HL0vtoY1tRTPIt2FfYjvMh3DWoGGSxXDTS73qTFQ+Fhhi5I0AnN9PcD2omfKQAVXiYks4kkMvlAHqA==", + "dependencies": [ + "pouchdb-binary-utils", + "pouchdb-errors", + "pouchdb-md5", + "pouchdb-merge", + "pouchdb-utils" + ] + }, + "pouchdb-binary-utils@9.0.0": { + "integrity": "sha512-2OMtgDZi82vqs+zNDE0YiYjOaWkYCUcZJZKK3WkRr+XYRu+2B7umJrnygJFhUwoGedBbHSrlQBLhdNV3F1AX1A==" + }, + "pouchdb-changes-filter@9.0.0": { + "integrity": "sha512-ig0fo0WLgIjAniFJ19Uw1Y+oxiypqC+Skhd8BCETRVXOhLBzueRwEQR4thffyo0UayYVqldJfSR5wHSDvEVk/A==", + "dependencies": [ + "pouchdb-errors", + "pouchdb-selector-core", + "pouchdb-utils" + ] + }, + "pouchdb-checkpointer@9.0.0": { + "integrity": "sha512-yu1OlWw78oTHKOkg1GoxxF2qB7YUsjK3rUDJOChMs/sVlZwOTZ4mGdWFPBr3udxSGvR77E+g89kpdmAWhPpHvA==", + "dependencies": [ + "pouchdb-collate", + "pouchdb-utils" + ] + }, + "pouchdb-collate@9.0.0": { + "integrity": "sha512-TrnEDNZEmIIl+W3xKUO8h+geqVLQ90oZe5ujPkl8myUzpREULWXWQBnV5EzPXVEKDBpJlb8T3I6oy/zdWGQpdA==" + }, + "pouchdb-core@9.0.0": { + "integrity": "sha512-98SJgs8bqXhr4gMGuOTR8yVeLlMYy797zlOtdlvlXIxIicvocyA8ColhVVhdBXPNOGxT2HwReIMywdIVAgibpg==", + "dependencies": [ + "pouchdb-changes-filter", + "pouchdb-errors", + "pouchdb-fetch", + "pouchdb-merge", + "pouchdb-utils", + "uuid" + ] + }, + "pouchdb-errors@9.0.0": { + "integrity": "sha512-961PSMLhW0UqqdJ566g+CdLZ5pkBJRd6l4WWpCDdD0USvE4xYfYGzv43w7nZZBw1k3Xdy092yqPge7yX/tfnyw==" + }, + "pouchdb-fetch@9.0.0": { + "integrity": "sha512-TbE3cUcAJQrwb9kr44tDP0X+NAbcqgjsTvcL30L4xzBNJeCPTIRjukYX80s154SHJUXBxcWRiPsMmNqpXsjfCA==", + "dependencies": [ + "fetch-cookie", + "node-fetch" + ] + }, + "pouchdb-find@9.0.0": { + "integrity": "sha512-vvVhq4eEOmSkwSRwf2NBYtdhURB7ryJ7sUI4WDN00GuLUj2g8jAXBJuZIryVgdYt/5S5cfn70iRL6Eow+LFhpA==", + "dependencies": [ + "pouchdb-abstract-mapreduce", + "pouchdb-collate", + "pouchdb-errors", + "pouchdb-fetch", + "pouchdb-md5", + "pouchdb-selector-core", + "pouchdb-utils" + ] + }, + "pouchdb-generate-replication-id@9.0.0": { + "integrity": "sha512-wetxjU0W/qNYtfHIoKwBO73ddUr0/eqzYOkoKHSFXCgOzYmTglDeqXiVY9LPysRXTgaHUJPKC5LoknZZw7e+Dw==", + "dependencies": [ + "pouchdb-collate", + "pouchdb-md5" + ] + }, + "pouchdb-json@9.0.0": { + "integrity": "sha512-aI41mYVyI195GXuT1Ys7mLIB/Mvrz11ihoTP6km6hYqVgSuaUxuZcFUozlyTJiZXr7H5kdhNgclhlVnjir4JAA==", + "dependencies": [ + "vuvuzela" + ] + }, + "pouchdb-mapreduce-utils@9.0.0": { + "integrity": "sha512-Bjh8W6QXqp1j7MKmHhYYp5cYlcQsm5drD8Jd/F+ZlfNt18uiD2SQXWzGM5797+tiW/LszFGb8ttw0uHWjxufCQ==", + "dependencies": [ + "pouchdb-utils" + ] + }, + "pouchdb-mapreduce@9.0.0": { + "integrity": "sha512-ZD8PleQ9atzQAzT2LZWsvooUVEfsen5QGv/SDfci20IleCaFW2A2q7OERrqY0YWKDCCNRsWhPWPmsFvZC9K8DQ==", + "dependencies": [ + "pouchdb-abstract-mapreduce", + "pouchdb-mapreduce-utils", + "pouchdb-utils" + ] + }, + "pouchdb-md5@9.0.0": { + "integrity": "sha512-58xUYBvW3/s+aH0j4uOhhN8yCk0LQ254cxBzI/gbKA9PrfwHpe4zrr0L/ia5ml3A30oH1f8aTnuVMwWDkFcuww==", + "dependencies": [ + "pouchdb-binary-utils", + "spark-md5" + ] + }, + "pouchdb-merge@9.0.0": { + "integrity": "sha512-Xh+TgOZCkGoZpI589btKf/cTiuQ5CsnPl9YpdW4h0cAPusniN6XNsR62F+/HbL9wirI6XTEPHUrk7MsQbk3S3A==", + "dependencies": [ + "pouchdb-utils" + ] + }, + "pouchdb-replication@9.0.0": { + "integrity": "sha512-EZ68KJ3ZUWuPe35NxP6WnRw8J6Zudf0j/tZ/6mOSrCcp3EbtBNt8Ke2FaAThUgiFahVnHD5Y8nd53EGs2DLygg==", + "dependencies": [ + "pouchdb-checkpointer", + "pouchdb-errors", + "pouchdb-generate-replication-id", + "pouchdb-utils" + ] + }, + "pouchdb-selector-core@9.0.0": { + "integrity": "sha512-ZYHYsdoedwm8j5tYofz+3+uUSK8i+7tRCBb01T0OuqDQb17+w5mzjHF8Ppi160xdPUPaWCo1Un+nLWGJzkmA3g==", + "dependencies": [ + "pouchdb-collate", + "pouchdb-utils" + ] + }, + "pouchdb-utils@9.0.0": { + "integrity": "sha512-xWZE5c+nAslgmLC8JBZbky8AYgdz7pKtv7KTSi6CD2tuQD0WyNKib0YnhZndeE84dksTeZlqlg56RQHsHoB2LQ==", + "dependencies": [ + "pouchdb-errors", + "pouchdb-md5", + "uuid" + ] + }, + "pouchdb-wrappers@5.0.0": { + "integrity": "sha512-fXqsVn+rmlPtxaAIGaQP5TkiaT39OMwvMk+ScLLtHrmfXD2KBO6fe/qBl38N/rpTn0h/A058dPN4fLAHt550zA==" + }, + "prr@1.0.1": { + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" + }, + "psl@1.15.0": { + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": [ + "punycode" + ] + }, + "punycode.js@2.3.1": { + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" + }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "qrcode-generator@1.5.2": { + "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==" + }, + "querystringify@2.2.0": { + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "readable-stream@1.1.14": { + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": [ + "core-util-is", + "inherits", + "isarray", + "string_decoder@0.10.31" + ] + }, + "readable-stream@3.6.2": { + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": [ + "inherits", + "string_decoder@1.3.0", + "util-deprecate" + ] + }, + "requires-port@1.0.0": { + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "safe-buffer@5.1.2": { + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "set-cookie-parser@2.7.2": { + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, + "spark-md5@3.0.2": { + "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==" + }, + "string_decoder@0.10.31": { + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "string_decoder@1.3.0": { + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": [ + "safe-buffer@5.2.1" + ] + }, + "sublevel-pouchdb@9.0.0": { + "integrity": "sha512-pX4r8+F7wuts0C81kUJ341h4bl2aRe7qV572FE8X1FMz9VkKlmi2nPD1vfeiOJXz5Y09I4MHjGULAbqvTfQZEQ==", + "dependencies": [ + "level-codec", + "ltgt", + "readable-stream@1.1.14" + ] + }, + "through2@3.0.2": { + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dependencies": [ + "inherits", + "readable-stream@3.6.2" + ] + }, + "tough-cookie@4.1.4": { + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": [ + "psl", + "punycode", + "universalify", + "url-parse" + ] + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "transform-pouch@2.0.0": { + "integrity": "sha512-nDZovo0U5o0UdMNL93fMQgGjrwH9h4F/a7qqRTnF6cVA+FfgyXiJPTrSuD+LmWSO7r2deZt0P0oeCD8hkgxl5g==", + "dependencies": [ + "pouchdb-wrappers" + ] + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "uc.micro@2.1.0": { + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "universalify@0.2.0": { + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" + }, + "url-parse@1.5.10": { + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": [ + "querystringify", + "requires-port" + ] + }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": true, + "bin": true + }, + "vuvuzela@1.0.3": { + "integrity": "sha512-Tm7jR1xTzBbPW+6y1tknKiEhz04Wf/1iZkcTJjSFcpNko43+dFW6+OOeQe9taJIug3NdfUAjFKgUSyQrIKaDvQ==" + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": [ + "tr46", + "webidl-conversions" + ] + }, + "xtend@4.0.2": { + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "xxhash-wasm@1.1.0": { + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==" } } } diff --git a/utils/flyio/deploy-server.sh b/utils/flyio/deploy-server.sh index 967c1760..9c59329c 100755 --- a/utils/flyio/deploy-server.sh +++ b/utils/flyio/deploy-server.sh @@ -1,8 +1,15 @@ #!/bin/bash ## Script for deploy and automatic setup CouchDB onto fly.io. -## We need Deno for generating the Setup-URI. +## Deno is used for Commonlib-backed provisioning and Setup URI generation. -source setenv.sh $@ +set -euo pipefail + +if ! command -v deno >/dev/null 2>&1; then + echo "ERROR: Deno 2 is required for CouchDB provisioning and Setup URI generation." >&2 + exit 1 +fi + +source setenv.sh "$@" export hostname="https://$appname.fly.dev" @@ -14,30 +21,16 @@ echo "region : $region" echo "" echo "-- START DEPLOYING --> " -set -e fly launch --name=$appname --env="COUCHDB_USER=$username" --copy-config=true --detach --no-deploy --region ${region} --yes fly secrets set COUCHDB_PASSWORD=$password fly deploy -set +e ../couchdb/couchdb-init.sh -# flyctl deploy echo "OK!" -if command -v deno >/dev/null 2>&1; then - echo "Setup finished! Also, we can set up Self-hosted LiveSync instantly, by the following setup uri." - echo "Passphrase of setup-uri will be printed only one time. Keep it safe!" - echo "--- configured ---" - echo "database : ${database}" - echo "E2EE passphrase: ${passphrase}" - echo "--- setup uri ---" - deno run -A generate_setupuri.ts -else - echo "Setup finished! Here is the configured values (reprise)!" - echo "-- YOUR CONFIGURATION --" - echo "URL : $hostname" - echo "username: $username" - echo "password: $password" - echo "-- YOUR CONFIGURATION --" - echo "If we had Deno, we would got the setup uri directly!" -fi +echo "Setup finished. The Commonlib-generated Setup URI follows." +echo "Its passphrase is printed only once, so store it safely." +echo "--- configured ---" +echo "database: ${database}" +echo "--- setup URI ---" +deno run --minimum-dependency-age=0 --allow-env generate_setupuri.ts diff --git a/utils/flyio/generate_setupuri.test.ts b/utils/flyio/generate_setupuri.test.ts new file mode 100644 index 00000000..d7540f70 --- /dev/null +++ b/utils/flyio/generate_setupuri.test.ts @@ -0,0 +1,66 @@ +import { decodeSettingsFromSetupURI } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/API/processSetting"; +import { DEFAULT_SETTINGS } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/settings"; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +Deno.test("generates a current self-hosted Setup URI through the published Commonlib contract", async () => { + const scriptPath = new URL("./generate_setupuri.ts", import.meta.url); + const command = new Deno.Command(Deno.execPath(), { + args: ["run", "-A", scriptPath.pathname], + env: { + hostname: "https://couch.example.test", + username: "alice", + password: "couch-secret", + database: "notes", + passphrase: "vault-secret", + uri_passphrase: "setup-secret", + }, + stdout: "piped", + stderr: "piped", + }); + + const result = await command.output(); + const stdout = new TextDecoder().decode(result.stdout); + const stderr = new TextDecoder().decode(result.stderr); + assert(result.success, `generator failed:\n${stdout}\n${stderr}`); + + const setupURI = stdout.match(/obsidian:\/\/setuplivesync\?settings=\S+/) + ?.[0]; + assert(setupURI, `generator did not print a Setup URI:\n${stdout}`); + + const decoded = await decodeSettingsFromSetupURI(setupURI, "setup-secret"); + assert(decoded, "Commonlib could not decode the generated Setup URI"); + const effectiveSettings = { ...DEFAULT_SETTINGS, ...decoded }; + assert( + effectiveSettings.customChunkSize === 60, + "the Setup URI did not use the current self-hosted chunk-size recommendation", + ); + assert( + effectiveSettings.chunkSplitterVersion === "v3-rabin-karp", + "the Setup URI did not use the current chunk splitter", + ); + assert( + effectiveSettings.E2EEAlgorithm === "v2", + "the Setup URI did not use the current E2EE algorithm", + ); + assert( + !Object.hasOwn(decoded, "doNotUseFixedRevisionForChunks"), + "the Setup URI serialised the obsolete fixed-revision compatibility setting", + ); + + const profiles = Object.values(decoded.remoteConfigurations ?? {}); + assert( + profiles.length === 1, + "the Setup URI did not contain exactly one CouchDB remote profile", + ); + assert( + decoded.activeConfigurationId === profiles[0].id, + "the CouchDB remote profile was not selected", + ); + assert( + profiles[0].uri.startsWith("sls+https://"), + "the selected remote profile was not a CouchDB connection URI", + ); +}); diff --git a/utils/flyio/generate_setupuri.ts b/utils/flyio/generate_setupuri.ts index 390c2e1c..c4f35245 100644 --- a/utils/flyio/generate_setupuri.ts +++ b/utils/flyio/generate_setupuri.ts @@ -1,175 +1,52 @@ -import { encrypt } from "npm:octagonal-wheels@0.1.30/encryption/encryption"; +import { encodeSettingsToSetupURI } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/API/processSetting"; +import { upsertRemoteConfigurationInPlace } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/remote-configurations"; +import { + createNewVaultSettings, + PREFERRED_SETTING_SELF_HOSTED, +} from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/settings"; -const noun = [ - "waterfall", - "river", - "breeze", - "moon", - "rain", - "wind", - "sea", - "morning", - "snow", - "lake", - "sunset", - "pine", - "shadow", - "leaf", - "dawn", - "glitter", - "forest", - "hill", - "cloud", - "meadow", - "sun", - "glade", - "bird", - "brook", - "butterfly", - "bush", - "dew", - "dust", - "field", - "fire", - "flower", - "firefly", - "feather", - "grass", - "haze", - "mountain", - "night", - "pond", - "darkness", - "snowflake", - "silence", - "sound", - "sky", - "shape", - "surf", - "thunder", - "violet", - "water", - "wildflower", - "wave", - "water", - "resonance", - "sun", - "log", - "dream", - "cherry", - "tree", - "fog", - "frost", - "voice", - "paper", - "frog", - "smoke", - "star", -]; -const adjectives = [ - "autumn", - "hidden", - "bitter", - "misty", - "silent", - "empty", - "dry", - "dark", - "summer", - "icy", - "delicate", - "quiet", - "white", - "cool", - "spring", - "winter", - "patient", - "twilight", - "dawn", - "crimson", - "wispy", - "weathered", - "blue", - "billowing", - "broken", - "cold", - "damp", - "falling", - "frosty", - "green", - "long", - "late", - "lingering", - "bold", - "little", - "morning", - "muddy", - "old", - "red", - "rough", - "still", - "small", - "sparkling", - "thrumming", - "shy", - "wandering", - "withered", - "wild", - "black", - "young", - "holy", - "solitary", - "fragrant", - "aged", - "snowy", - "proud", - "floral", - "restless", - "divine", - "polished", - "ancient", - "purple", - "lively", - "nameless", -]; -function friendlyString() { - return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${noun[Math.floor(Math.random() * noun.length)]}`; +function requireEnvironment(name: string): string { + const value = Deno.env.get(name)?.trim(); + if (!value) throw new Error(`${name} is required`); + return value; } -const uri_passphrase = `${Deno.env.get("uri_passphrase") ?? friendlyString()}`; +function generateSetupPassphrase(): string { + const bytes = crypto.getRandomValues(new Uint8Array(24)); + return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll( + "/", + "_", + ).replace(/=+$/, ""); +} -const URIBASE = "obsidian://setuplivesync?settings="; async function main() { - const conf = { - couchDB_URI: `${Deno.env.get("hostname")}`, - couchDB_USER: `${Deno.env.get("username")}`, - couchDB_PASSWORD: `${Deno.env.get("password")}`, - couchDB_DBNAME: `${Deno.env.get("database")}`, - syncOnStart: true, - gcDelay: 0, - periodicReplication: true, - syncOnFileOpen: true, - encrypt: true, - passphrase: `${Deno.env.get("passphrase")}`, - usePathObfuscation: true, - batchSave: true, - batch_size: 50, - batches_limit: 50, - useHistory: true, - disableRequestURI: true, - customChunkSize: 50, - syncAfterMerge: false, - concurrencyOfReadChunksOnline: 100, - minimumIntervalOfReadChunksOnline: 100, - handleFilenameCaseSensitive: false, - doNotUseFixedRevisionForChunks: false, - settingVersion: 10, - notifyThresholdOfRemoteStorageSize: 800, - }; - const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), uri_passphrase, false)); - const theURI = `${URIBASE}${encryptedConf}`; - console.log("\nYour passphrase of Setup-URI is: ", uri_passphrase); - console.log("This passphrase is never shown again, so please note it in a safe place."); - console.log(theURI); + const setupPassphrase = Deno.env.get("uri_passphrase")?.trim() || + generateSetupPassphrase(); + const settings = createNewVaultSettings(); + Object.assign(settings, PREFERRED_SETTING_SELF_HOSTED, { + couchDB_URI: requireEnvironment("hostname"), + couchDB_USER: requireEnvironment("username"), + couchDB_PASSWORD: requireEnvironment("password"), + couchDB_DBNAME: requireEnvironment("database"), + batchSave: true, + periodicReplication: true, + syncOnStart: true, + syncOnFileOpen: true, + syncAfterMerge: true, + encrypt: true, + passphrase: requireEnvironment("passphrase"), + usePathObfuscation: true, + }); + upsertRemoteConfigurationInPlace(settings, "couchdb", { activate: true }); + + const setupURI = await encodeSettingsToSetupURI(settings, setupPassphrase, [ + "pluginSyncExtendedSetting", + "doNotUseFixedRevisionForChunks", + ], true); + + console.log("\nYour passphrase for the Setup URI is:", setupPassphrase); + console.log("This passphrase is never shown again, so store it safely."); + console.log(setupURI.trim()); } + await main(); diff --git a/utils/flyio/setenv.sh b/utils/flyio/setenv.sh index 1c1f4c1f..50e2dff3 100755 --- a/utils/flyio/setenv.sh +++ b/utils/flyio/setenv.sh @@ -1,19 +1,23 @@ random_num() { - echo $RANDOM + echo "$RANDOM" } random_noun() { nouns=("waterfall" "river" "breeze" "moon" "rain" "wind" "sea" "morning" "snow" "lake" "sunset" "pine" "shadow" "leaf" "dawn" "glitter" "forest" "hill" "cloud" "meadow" "sun" "glade" "bird" "brook" "butterfly" "bush" "dew" "dust" "field" "fire" "flower" "firefly" "feather" "grass" "haze" "mountain" "night" "pond" "darkness" "snowflake" "silence" "sound" "sky" "shape" "surf" "thunder" "violet" "water" "wildflower" "wave" "water" "resonance" "sun" "log" "dream" "cherry" "tree" "fog" "frost" "voice" "paper" "frog" "smoke" "star") - echo ${nouns[$(($RANDOM % ${#nouns[*]}))]} + echo "${nouns[$((RANDOM % ${#nouns[*]}))]}" } random_adjective() { adjectives=("autumn" "hidden" "bitter" "misty" "silent" "empty" "dry" "dark" "summer" "icy" "delicate" "quiet" "white" "cool" "spring" "winter" "patient" "twilight" "dawn" "crimson" "wispy" "weathered" "blue" "billowing" "broken" "cold" "damp" "falling" "frosty" "green" "long" "late" "lingering" "bold" "little" "morning" "muddy" "old" "red" "rough" "still" "small" "sparkling" "thrumming" "shy" "wandering" "withered" "wild" "black" "young" "holy" "solitary" "fragrant" "aged" "snowy" "proud" "floral" "restless" "divine" "polished" "ancient" "purple" "lively" "nameless") - echo ${adjectives[$(($RANDOM % ${#adjectives[*]}))]} + echo "${adjectives[$((RANDOM % ${#adjectives[*]}))]}" +} + +random_secret() { + deno eval 'const bytes=crypto.getRandomValues(new Uint8Array(24));console.log(btoa(String.fromCharCode(...bytes)).replaceAll("+","-").replaceAll("/","_").replace(/=+$/, ""));' } cp ./fly.template.toml ./fly.toml -if [ "$1" = "renew" ]; then +if [ "${1:-}" = "renew" ]; then unset appname unset username unset password @@ -22,9 +26,9 @@ if [ "$1" = "renew" ]; then unset region fi -[ -z $appname ] && export appname=$(random_adjective)-$(random_noun)-$(random_num) -[ -z $username ] && export username=$(random_adjective)-$(random_noun)-$(random_num) -[ -z $password ] && export password=$(random_adjective)-$(random_noun)-$(random_num) -[ -z $database ] && export database="obsidiannotes" -[ -z $passphrase ] && export passphrase=$(random_adjective)-$(random_noun)-$(random_num) -[ -z $region ] && export region="nrt" +[ -z "${appname:-}" ] && export appname="$(random_adjective)-$(random_noun)-$(random_num)" +[ -z "${username:-}" ] && export username="$(random_adjective)-$(random_noun)-$(random_num)" +[ -z "${password:-}" ] && export password="$(random_secret)" +[ -z "${database:-}" ] && export database="obsidiannotes" +[ -z "${passphrase:-}" ] && export passphrase="$(random_secret)" +[ -z "${region:-}" ] && export region="nrt" diff --git a/utils/flyio/setenv.test.sh b/utils/flyio/setenv.test.sh new file mode 100644 index 00000000..82886754 --- /dev/null +++ b/utils/flyio/setenv.test.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +fixture_dir="$(mktemp -d)" +trap 'rm -rf "$fixture_dir"' EXIT +cp "$(dirname "$0")/setenv.sh" "$(dirname "$0")/fly.template.toml" "$fixture_dir/" + +( + cd "$fixture_dir" + appname="" + username="" + password="" + database="" + passphrase="" + region="" + source ./setenv.sh keep + + [[ "$password" =~ ^[A-Za-z0-9_-]{32}$ ]] || { + echo "generated CouchDB password is not a 32-character base64url secret" >&2 + exit 1 + } + [[ "$passphrase" =~ ^[A-Za-z0-9_-]{32}$ ]] || { + echo "generated Vault encryption passphrase is not a 32-character base64url secret" >&2 + exit 1 + } +) diff --git a/utils/readme.md b/utils/readme.md index 6fe52ac9..501fc9c7 100644 --- a/utils/readme.md +++ b/utils/readme.md @@ -1,167 +1,54 @@ - # Utilities -Here are some useful things. -## couchdb +These utilities support self-hosted CouchDB provisioning and Setup URI generation. They consume the immutable `@vrtmrz/livesync-commonlib@0.1.0-rc.4` registry package directly; the utility lockfile records its resolved package integrity. -### couchdb-init.sh -This script can configure CouchDB with the necessary settings by REST APIs. +## CouchDB provisioning -#### Materials -- Mandatory: curl +`couchdb/couchdb-init.sh` is a Bash wrapper for the Deno provisioning tool. Deno 2 is required. The tool configures single-node CouchDB, authenticated access, CORS for Obsidian, and the request and document size limits used by Self-hosted LiveSync. -#### Usage +Set `database` to create a database as part of provisioning. The tool then uses Commonlib's database negotiation contract to initialise and verify the LiveSync database version. If `database` is omitted, only the CouchDB server is configured and the first LiveSync client remains responsible for creating its database. ```sh -export hostname=http://localhost:5984/ +export hostname=http://localhost:5984 export username=couchdb-admin-username export password=couchdb-admin-password -./couchdb-init.sh +export database=obsidiannotes +./couchdb/couchdb-init.sh ``` -curl result will be shown, however, all of them can be ignored if the script has been run completely. +Optional variables are: -## fly.io +- `node`, which defaults to `_local`; +- `origins`, which defaults to the supported Obsidian desktop, mobile, and local origins; +- `retry_count`, which defaults to `12`; and +- `retry_delay_ms`, which defaults to `5000`. -### deploy-server.sh +Authentication and other non-retryable HTTP failures stop immediately. Network and server failures are retried within the configured bound. -A fully automated CouchDB deployment script. We can deploy CouchDB onto fly.io. The only we need is an account of it. +## Setup URI generation -All omitted configurations will be determined at random. (And, it is preferred). The region is configured to `nrt`. -If Japan is not close to you, please choose a region closer to you. However, the deployed database will work if you leave it at all. +`flyio/generate_setupuri.ts` creates a current self-hosted configuration from Commonlib's new-Vault and self-hosted presets, stores the CouchDB connection as the selected remote profile, and encodes it with Commonlib's Setup URI contract. -#### Materials -- Mandatory: curl, flyctl -- Recommended: deno - -#### Usage ```sh -#export appname= -#export username= -#export password= -#export database= -#export passphrase= -export region=nrt #pick your nearest location +export hostname=https://couch.example.com +export username=couchdb-admin-username +export password=couchdb-admin-password +export database=obsidiannotes +export passphrase=a-strong-vault-encryption-passphrase +export uri_passphrase=a-separate-setup-uri-passphrase # Optional +deno run --minimum-dependency-age=0 --allow-env ./flyio/generate_setupuri.ts +``` + +If `uri_passphrase` is omitted, the tool generates and prints a cryptographically random one. Store the Setup URI and its passphrase separately. The `passphrase` value protects synchronised Vault data and must also be stored safely. + +## Fly.io deployment + +`flyio/deploy-server.sh` deploys CouchDB through `flyctl`, provisions the selected database through the tool above, and prints a Commonlib-generated Setup URI. Both `flyctl` and Deno 2 are required. + +```sh +export region=nrt # Choose a nearby Fly.io region. +cd flyio ./deploy-server.sh ``` -The result of this command is as follows. - -``` --- YOUR CONFIGURATION -- -URL : https://young-darkness-25342.fly.dev -username: billowing-cherry-22580 -password: misty-dew-13571 -region : nrt - --- START DEPLOYING --> -An existing fly.toml file was found -Using build strategies '[the "couchdb:latest" docker image]'. Remove [build] from fly.toml to force a rescan -Creating app in /home/vorotamoroz/dev/obsidian-livesync/utils/flyio -We're about to launch your app on Fly.io. Here's what you're getting: - -Organization: vorotamoroz (fly launch defaults to the personal org) -Name: young-darkness-25342 (specified on the command line) -Region: Tokyo, Japan (specified on the command line) -App Machines: shared-cpu-1x, 256MB RAM (specified on the command line) -Postgres: (not requested) -Redis: (not requested) - -Created app 'young-darkness-25342' in organization 'personal' -Admin URL: https://fly.io/apps/young-darkness-25342 -Hostname: young-darkness-25342.fly.dev -Wrote config file fly.toml -Validating /home/vorotamoroz/dev/obsidian-livesync/utils/flyio/fly.toml -Platform: machines -✓ Configuration is valid -Your app is ready! Deploy with `flyctl deploy` -Secrets are staged for the first deployment -==> Verifying app config -Validating /home/vorotamoroz/dev/obsidian-livesync/utils/flyio/fly.toml -Platform: machines -✓ Configuration is valid ---> Verified app config -==> Building image -Searching for image 'couchdb:latest' remotely... -image found: img_ox20prk63084j1zq - -Watch your deployment at https://fly.io/apps/young-darkness-25342/monitoring - -Provisioning ips for young-darkness-25342 - Dedicated ipv6: 2a09:8280:1::37:fde9 - Shared ipv4: 66.241.124.163 - Add a dedicated ipv4 with: fly ips allocate-v4 - -Creating a 1 GB volume named 'couchdata' for process group 'app'. Use 'fly vol extend' to increase its size -This deployment will: - * create 1 "app" machine - -No machines in group app, launching a new machine - -WARNING The app is not listening on the expected address and will not be reachable by fly-proxy. -You can fix this by configuring your app to listen on the following addresses: - - 0.0.0.0:5984 -Found these processes inside the machine with open listening sockets: - PROCESS | ADDRESSES ------------------*--------------------------------------- - /.fly/hallpass | [fdaa:0:73b9:a7b:22e:3851:7f28:2]:22 - -Finished launching new machines - -NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling - -------- -Checking DNS configuration for young-darkness-25342.fly.dev - -Visit your newly deployed app at https://young-darkness-25342.fly.dev/ --- Configuring CouchDB by REST APIs... --> -curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to young-darkness-25342.fly.dev:443 -{"ok":true} -"" -"" -"" -"" -"" -"" -"" -"" -"" -<-- Configuring CouchDB by REST APIs Done! -OK! -Setup finished! Also, we can set up Self-hosted LiveSync instantly, by the following setup uri. -Passphrase of setup-uri will be printed only one time. Keep it safe! ---- configured --- -database : obsidiannotes -E2EE passphrase: dark-wildflower-26467 ---- setup uri --- -obsidian://setuplivesync?settings=%5B%22gZkBwjFbLqxbdSIbJymU%2FmTPBPAKUiHVGDRKYiNnKhW0auQeBgJOfvnxexZtMCn8sNiIUTAlxNaMGF2t%2BCEhpJoeCP%2FO%2BrwfN5LaNDQyky1Uf7E%2B64A5UWyjOYvZDOgq4iCKSdBAXp9oO%2BwKh4MQjUZ78vIVvJp8Mo6NWHfm5fkiWoAoddki1xBMvi%2BmmN%2FhZatQGcslVb9oyYWpZocduTl0a5Dv%2FQviGwlYQ%2F4NY0dVDIoOdvaYS%2FX4GhNAnLzyJKMXhPEJHo9FvR%2FEOBuwyfMdftV1SQUZ8YDCuiR3T7fh7Kn1c6OFgaFMpFm%2BWgIJ%2FZpmAyhZFpEcjpd7ty%2BN9kfd9gQsZM4%2BYyU9OwDd2DahVMBWkqoV12QIJ8OlJScHHdcUfMW5ex%2F4UZTWKNEHJsigITXBrtq11qGk3rBfHys8O0vY6sz%2FaYNM3iAOsR1aoZGyvwZm4O6VwtzK8edg0T15TL4O%2B7UajQgtCGxgKNYxb8EMOGeskv7NifYhjCWcveeTYOJzBhnIDyRbYaWbkAXQgHPBxzJRkkG%2FpBPfBBoJarj7wgjMvhLJ9xtL4FbP6sBNlr8jtAUCoq4L7LJcRNF4hlgvjJpL2BpFZMzkRNtUBcsRYR5J%2BM1X2buWi2BHncbSiRRDKEwNOQkc%2FmhMJjbAn%2F8eNKRuIICOLD5OvxD7FZNCJ0R%2BWzgrzcNV%22%2C%22ec7edc900516b4fcedb4c7cc01000000%22%2C%22fceb5fe54f6619ee266ed9a887634e07%22%5D - -Your passphrase of Setup-URI is: patient-haze -This passphrase is never shown again, so please note it in a safe place. -``` - -All we have to do is copy the setup-URI (`obsidian`://...`) and open it from Self-hosted LiveSync on Obsidian. - -If you did not install Deno, configurations will be printed again, instead of the setup-URI. In this case, we should configure it manually. - -### delete-server.sh - -The pair script of `deploy-server.sh`. We can delete the deployed server by this with fly.toml. - -#### Materials - -- Mandatory: flyctl, jq -- Recommended: none - -#### Usage -```sh -./delete-server.sh -``` - -``` -App 'young-darkness-25342 is going to be scaled according to this plan: - -1 machines for group 'app' on region 'nrt' of size 'shared-cpu-1x' -Executing scale plan - Destroyed e28667eec57158 group:app region:nrt size:shared-cpu-1x -Destroyed app young-darkness-25342 -``` \ No newline at end of file +Set `appname`, `username`, `password`, `database`, `passphrase`, or `region` before running the script to override its generated values. Use `delete-server.sh` to remove the Fly.io application described by the generated `fly.toml`.