Use Commonlib for self-hosted setup tools

This commit is contained in:
vorotamoroz
2026-07-20 15:47:12 +00:00
parent c49536acc9
commit 44784d4865
15 changed files with 1474 additions and 409 deletions
+52
View File
@@ -23,6 +23,8 @@ on:
- 'version-bump.mjs' - 'version-bump.mjs'
- 'utils/release-*.mjs' - 'utils/release-*.mjs'
- 'utils/release-*.unit.spec.ts' - 'utils/release-*.unit.spec.ts'
- 'utils/couchdb/**'
- 'utils/flyio/**'
- '.github/workflows/prepare-release.yml' - '.github/workflows/prepare-release.yml'
- '.github/workflows/finalise-release.yml' - '.github/workflows/finalise-release.yml'
- '.github/workflows/release.yml' - '.github/workflows/release.yml'
@@ -44,6 +46,8 @@ on:
- 'version-bump.mjs' - 'version-bump.mjs'
- 'utils/release-*.mjs' - 'utils/release-*.mjs'
- 'utils/release-*.unit.spec.ts' - 'utils/release-*.unit.spec.ts'
- 'utils/couchdb/**'
- 'utils/flyio/**'
- '.github/workflows/prepare-release.yml' - '.github/workflows/prepare-release.yml'
- '.github/workflows/finalise-release.yml' - '.github/workflows/finalise-release.yml'
- '.github/workflows/release.yml' - '.github/workflows/release.yml'
@@ -53,6 +57,54 @@ permissions:
contents: read contents: read
jobs: 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 <<EOF > .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: unit-test:
name: Unit Tests name: Unit Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
+2
View File
@@ -36,6 +36,7 @@ npm run dev # Development build with auto-rebuild (uses .env for test v
npm run build # Production build npm run build # Production build
npm run buildDev # Development build (one-time) npm run buildDev # Development build (one-time)
npm run test:integration # Run CouchDB-backed integration tests 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:cli:p2p # Run canonical P2P validation in Compose
npm run test:e2e:obsidian:local-suite # Run the real Obsidian local suite 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. - 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. - **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. - **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. - **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: - **Docker Services**: Service-backed tests use CouchDB and MinIO (S3). Canonical P2P validation owns its relay through the CLI Compose runner:
+17 -21
View File
@@ -33,7 +33,7 @@
```bash ```bash
# Adding environment variables. # Adding environment variables.
export hostname=localhost:5984 export hostname=http://localhost:5984
export username=goojdasjdas #Please change as you like. export username=goojdasjdas #Please change as you like.
export password=kpkdasdosakpdsa #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. 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 ## 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=<INSERT USERNAME HERE>
export password=<INSERT PASSWORD HERE>
export database=obsidiannotes
curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | bash curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | bash
``` ```
If it results like the following: If it results like the following:
``` ```
-- Configuring CouchDB by REST APIs... --> CouchDB provisioning completed.
{"ok":true}
""
""
""
""
""
""
""
""
""
<-- Configuring CouchDB by REST APIs Done!
``` ```
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: 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://<YOUR SERVER IP>:5984 username=<INSERT USERNAME HERE> password=<INSERT PASSWORD HERE> bash curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | hostname=http://<YOUR SERVER IP>:5984 username=<INSERT USERNAME HERE> password=<INSERT PASSWORD HERE> database=obsidiannotes bash
``` ```
## 3. Expose CouchDB to the Internet ## 3. Expose CouchDB to the Internet
@@ -177,10 +173,10 @@ Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our serv
```bash ```bash
export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com #Point to your vault export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com #Point to your vault
export database=obsidiannotes #Please change as you like export database=obsidiannotes #Please change as you like
export passphrase=dfsapkdjaskdjasdas #Please change as you like export passphrase=<INSERT A STRONG VAULT ENCRYPTION PASSPHRASE>
export username=johndoe export username=johndoe
export password=abc123 export password=<INSERT THE COUCHDB PASSWORD>
deno run -A https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts
``` ```
> [!TIP] > [!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. > 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. > 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. > 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: You will then get the following output:
```bash ```bash
obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D 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. 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. [This video](https://youtu.be/7sa_I1832Xc?t=146) may help us.
1. Install Self-hosted LiveSync 1. Install Self-hosted LiveSync
2. Choose `Use the copied setup URI` from the command palette and paste the setup URI. (obsidian://setuplivesync?settings=.....). 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`. 4. Answer `yes` and `Set it up...`, and finish the first dialogue with `Keep them disabled`.
5. `Reload app without save` once. 5. `Reload app without save` once.
+1
View File
@@ -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", "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/", "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: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: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: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", "test:contract:context:cli": "npm run build --workspace self-hosted-livesync-cli && deno task --cwd src/apps/cli/testdeno test:setup-put-cat",
+5
View File
@@ -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. - 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. - 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. - 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 ## 1.0.0-rc.0
+20 -27
View File
@@ -1,33 +1,26 @@
#!/bin/bash #!/bin/bash
if [[ -z "$hostname" ]]; then set -euo pipefail
echo "ERROR: Hostname missing"
exit 1 if ! command -v deno >/dev/null 2>&1; then
fi echo "ERROR: Deno is required to run the Commonlib-backed CouchDB provisioning tool." >&2
if [[ -z "$username" ]]; then
echo "ERROR: Username missing"
exit 1 exit 1
fi fi
if [[ -z "$password" ]]; then script_url="${provision_script_url:-https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/provision.ts}"
echo "ERROR: Password missing" script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)"
exit 1 deno_dependency_options=()
fi if [[ -n "$script_dir" && -f "$script_dir/provision.ts" ]]; then
if [[ -z "$node" ]]; then script_url="$script_dir/provision.ts"
echo "INFO: defaulting to _local" lockfile="$script_dir/../flyio/deno.lock"
node=_local 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 fi
echo "-- Configuring CouchDB by REST APIs... -->" exec deno run \
--minimum-dependency-age=0 \
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 "${deno_dependency_options[@]}" \
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 --allow-env \
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 --allow-net \
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 "$script_url"
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!"
+83
View File
@@ -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",
);
});
+250
View File
@@ -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<void>;
initialiseDatabaseVersion: (
databaseURL: string,
username: string,
password: string,
) => Promise<void>;
}
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<void> {
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<void> {
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<ProvisioningDependencies> = {},
): Promise<void> {
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.");
}
+844 -14
View File
@@ -1,25 +1,855 @@
{ {
"version": "4", "version": "5",
"specifiers": { "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": { "npm": {
"idb@8.0.0": { "@aws-sdk/checksums@3.1000.18": {
"integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" "integrity": "sha512-IImkbEyXdV6/uaF5r6Wkk+8718mQw1ll83j0a4a30R3JM/rHVFdWAiT4jtJpFjJiIwM/oJ6SxIxr0z2TaQUGqw==",
},
"octagonal-wheels@0.1.11": {
"integrity": "sha512-KsXfpziFHmlLEBe5VAXFz9OyyjJEEdSg7xxASqdzmbe5oo9dhcOeWGrQfyipRJwHAhlFkI4vEf8JCSgkcyRxYg==",
"dependencies": [ "dependencies": [
"idb", "@aws-sdk/core",
"xxhash-wasm@0.4.2", "@aws-sdk/types",
"xxhash-wasm-102@npm:xxhash-wasm@1.0.2" "@smithy/core",
"@smithy/types",
"tslib"
] ]
}, },
"xxhash-wasm@0.4.2": { "@aws-sdk/client-s3@3.1090.0": {
"integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA==" "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": { "@aws-sdk/core@3.975.3": {
"integrity": "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==" "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=="
} }
} }
} }
+15 -22
View File
@@ -1,8 +1,15 @@
#!/bin/bash #!/bin/bash
## Script for deploy and automatic setup CouchDB onto fly.io. ## 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" export hostname="https://$appname.fly.dev"
@@ -14,30 +21,16 @@ echo "region : $region"
echo "" echo ""
echo "-- START DEPLOYING --> " echo "-- START DEPLOYING --> "
set -e
fly launch --name=$appname --env="COUCHDB_USER=$username" --copy-config=true --detach --no-deploy --region ${region} --yes fly launch --name=$appname --env="COUCHDB_USER=$username" --copy-config=true --detach --no-deploy --region ${region} --yes
fly secrets set COUCHDB_PASSWORD=$password fly secrets set COUCHDB_PASSWORD=$password
fly deploy fly deploy
set +e
../couchdb/couchdb-init.sh ../couchdb/couchdb-init.sh
# flyctl deploy
echo "OK!" echo "OK!"
if command -v deno >/dev/null 2>&1; then echo "Setup finished. The Commonlib-generated Setup URI follows."
echo "Setup finished! Also, we can set up Self-hosted LiveSync instantly, by the following setup uri." echo "Its passphrase is printed only once, so store it safely."
echo "Passphrase of setup-uri will be printed only one time. Keep it safe!" echo "--- configured ---"
echo "--- configured ---" echo "database: ${database}"
echo "database : ${database}" echo "--- setup URI ---"
echo "E2EE passphrase: ${passphrase}" deno run --minimum-dependency-age=0 --allow-env generate_setupuri.ts
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
+66
View File
@@ -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",
);
});
+45 -168
View File
@@ -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 = [ function requireEnvironment(name: string): string {
"waterfall", const value = Deno.env.get(name)?.trim();
"river", if (!value) throw new Error(`${name} is required`);
"breeze", return value;
"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)]}`;
} }
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() { async function main() {
const conf = { const setupPassphrase = Deno.env.get("uri_passphrase")?.trim() ||
couchDB_URI: `${Deno.env.get("hostname")}`, generateSetupPassphrase();
couchDB_USER: `${Deno.env.get("username")}`, const settings = createNewVaultSettings();
couchDB_PASSWORD: `${Deno.env.get("password")}`, Object.assign(settings, PREFERRED_SETTING_SELF_HOSTED, {
couchDB_DBNAME: `${Deno.env.get("database")}`, couchDB_URI: requireEnvironment("hostname"),
syncOnStart: true, couchDB_USER: requireEnvironment("username"),
gcDelay: 0, couchDB_PASSWORD: requireEnvironment("password"),
periodicReplication: true, couchDB_DBNAME: requireEnvironment("database"),
syncOnFileOpen: true, batchSave: true,
encrypt: true, periodicReplication: true,
passphrase: `${Deno.env.get("passphrase")}`, syncOnStart: true,
usePathObfuscation: true, syncOnFileOpen: true,
batchSave: true, syncAfterMerge: true,
batch_size: 50, encrypt: true,
batches_limit: 50, passphrase: requireEnvironment("passphrase"),
useHistory: true, usePathObfuscation: true,
disableRequestURI: true, });
customChunkSize: 50, upsertRemoteConfigurationInPlace(settings, "couchdb", { activate: true });
syncAfterMerge: false,
concurrencyOfReadChunksOnline: 100, const setupURI = await encodeSettingsToSetupURI(settings, setupPassphrase, [
minimumIntervalOfReadChunksOnline: 100, "pluginSyncExtendedSetting",
handleFilenameCaseSensitive: false, "doNotUseFixedRevisionForChunks",
doNotUseFixedRevisionForChunks: false, ], true);
settingVersion: 10,
notifyThresholdOfRemoteStorageSize: 800, console.log("\nYour passphrase for the Setup URI is:", setupPassphrase);
}; console.log("This passphrase is never shown again, so store it safely.");
const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), uri_passphrase, false)); console.log(setupURI.trim());
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);
} }
await main(); await main();
+14 -10
View File
@@ -1,19 +1,23 @@
random_num() { random_num() {
echo $RANDOM echo "$RANDOM"
} }
random_noun() { 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") 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() { 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") 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 cp ./fly.template.toml ./fly.toml
if [ "$1" = "renew" ]; then if [ "${1:-}" = "renew" ]; then
unset appname unset appname
unset username unset username
unset password unset password
@@ -22,9 +26,9 @@ if [ "$1" = "renew" ]; then
unset region unset region
fi fi
[ -z $appname ] && export appname=$(random_adjective)-$(random_noun)-$(random_num) [ -z "${appname:-}" ] && export appname="$(random_adjective)-$(random_noun)-$(random_num)"
[ -z $username ] && export username=$(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 "${password:-}" ] && export password="$(random_secret)"
[ -z $database ] && export database="obsidiannotes" [ -z "${database:-}" ] && export database="obsidiannotes"
[ -z $passphrase ] && export passphrase=$(random_adjective)-$(random_noun)-$(random_num) [ -z "${passphrase:-}" ] && export passphrase="$(random_secret)"
[ -z $region ] && export region="nrt" [ -z "${region:-}" ] && export region="nrt"
+26
View File
@@ -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
}
)
+34 -147
View File
@@ -1,167 +1,54 @@
<!-- For translation: 20240206r0 -->
# Utilities # 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 ## CouchDB provisioning
This script can configure CouchDB with the necessary settings by REST APIs.
#### Materials `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.
- Mandatory: curl
#### 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 ```sh
export hostname=http://localhost:5984/ export hostname=http://localhost:5984
export username=couchdb-admin-username export username=couchdb-admin-username
export password=couchdb-admin-password 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`. `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.
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.
#### Materials
- Mandatory: curl, flyctl
- Recommended: deno
#### Usage
```sh ```sh
#export appname= export hostname=https://couch.example.com
#export username= export username=couchdb-admin-username
#export password= export password=couchdb-admin-password
#export database= export database=obsidiannotes
#export passphrase= export passphrase=a-strong-vault-encryption-passphrase
export region=nrt #pick your nearest location 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 ./deploy-server.sh
``` ```
The result of this command is as follows. 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`.
```
-- 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: <none> (not requested)
Redis: <none> (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
```