1
0
mirror of https://github.com/vrtmrz/obsidian-livesync.git synced 2025-08-10 22:11:45 +02:00

### New Feature

- Now, we can send custom headers to the server.
- Authentication with JWT in CouchDB is now supported.

### Improved

- The QR Code for set-up can be shown also from the setting dialogue now.
- Conflict checking for preventing unexpected overwriting on the boot-up process has been quite faster.

### Fixed

- Some bugs on Dev and Testing modules have been fixed.
This commit is contained in:
vorotamoroz
2025-04-10 14:24:33 +01:00
parent 30467d1c25
commit d8a41fe45d
14 changed files with 399 additions and 45 deletions

14
package-lock.json generated
View File

@@ -18,7 +18,7 @@
"fflate": "^0.8.2", "fflate": "^0.8.2",
"idb": "^8.0.2", "idb": "^8.0.2",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.24", "octagonal-wheels": "^0.1.25",
"qrcode-generator": "^1.4.4", "qrcode-generator": "^1.4.4",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"trystero": "^0.20.1", "trystero": "^0.20.1",
@@ -8514,9 +8514,9 @@
} }
}, },
"node_modules/octagonal-wheels": { "node_modules/octagonal-wheels": {
"version": "0.1.24", "version": "0.1.25",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.24.tgz", "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.25.tgz",
"integrity": "sha512-ywzcq3FyW/xM37RhXkgwkERzgO6hG7uQHfpqHKcvPaT0H54e0/WoWEV65A2ttGp7Kxrq+UgXxVM1UT+KcSbPkA==", "integrity": "sha512-WXe+AKgDlYU9FZ2/CbRXeTvpL0xknrL1NuG27+E6Vzg4RmeyWh8hM4lItGCiSqvAGbm8atn50WtaMSY7y5qfGg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"idb": "^8.0.2" "idb": "^8.0.2"
@@ -17577,9 +17577,9 @@
} }
}, },
"octagonal-wheels": { "octagonal-wheels": {
"version": "0.1.24", "version": "0.1.25",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.24.tgz", "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.25.tgz",
"integrity": "sha512-ywzcq3FyW/xM37RhXkgwkERzgO6hG7uQHfpqHKcvPaT0H54e0/WoWEV65A2ttGp7Kxrq+UgXxVM1UT+KcSbPkA==", "integrity": "sha512-WXe+AKgDlYU9FZ2/CbRXeTvpL0xknrL1NuG27+E6Vzg4RmeyWh8hM4lItGCiSqvAGbm8atn50WtaMSY7y5qfGg==",
"requires": { "requires": {
"idb": "^8.0.2" "idb": "^8.0.2"
} }

View File

@@ -77,7 +77,7 @@
"fflate": "^0.8.2", "fflate": "^0.8.2",
"idb": "^8.0.2", "idb": "^8.0.2",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.24", "octagonal-wheels": "^0.1.25",
"qrcode-generator": "^1.4.4", "qrcode-generator": "^1.4.4",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"trystero": "^0.20.1", "trystero": "^0.20.1",

View File

@@ -10,6 +10,7 @@ export const EVENT_REQUEST_OPEN_SETTINGS = "request-open-settings";
export const EVENT_REQUEST_OPEN_SETTING_WIZARD = "request-open-setting-wizard"; export const EVENT_REQUEST_OPEN_SETTING_WIZARD = "request-open-setting-wizard";
export const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri"; export const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri";
export const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri"; export const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri";
export const EVENT_REQUEST_SHOW_SETUP_QR = "request-show-setup-qr";
export const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab"; export const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab";
@@ -35,6 +36,7 @@ declare global {
[EVENT_REQUEST_OPEN_P2P]: undefined; [EVENT_REQUEST_OPEN_P2P]: undefined;
[EVENT_REQUEST_OPEN_SETUP_URI]: undefined; [EVENT_REQUEST_OPEN_SETUP_URI]: undefined;
[EVENT_REQUEST_COPY_SETUP_URI]: undefined; [EVENT_REQUEST_COPY_SETUP_URI]: undefined;
[EVENT_REQUEST_SHOW_SETUP_QR]: undefined;
[EVENT_REQUEST_RUN_DOCTOR]: string; [EVENT_REQUEST_RUN_DOCTOR]: string;
} }
} }

Submodule src/lib updated: dc4a27611d...ad3f7ee995

View File

@@ -18,6 +18,7 @@ import {
type AUTO_MERGED, type AUTO_MERGED,
type RemoteDBSettings, type RemoteDBSettings,
type TweakValues, type TweakValues,
type CouchDBCredentials,
} from "./lib/src/common/types.ts"; } from "./lib/src/common/types.ts";
import { type FileEventItem } from "./common/types.ts"; import { type FileEventItem } from "./common/types.ts";
import { type SimpleStore } from "./lib/src/common/utils.ts"; import { type SimpleStore } from "./lib/src/common/utils.ts";
@@ -283,16 +284,14 @@ export default class ObsidianLiveSyncPlugin
$$connectRemoteCouchDB( $$connectRemoteCouchDB(
uri: string, uri: string,
auth: { auth: CouchDBCredentials,
username: string;
password: string;
},
disableRequestURI: boolean, disableRequestURI: boolean,
passphrase: string | false, passphrase: string | false,
useDynamicIterationCount: boolean, useDynamicIterationCount: boolean,
performSetup: boolean, performSetup: boolean,
skipInfo: boolean, skipInfo: boolean,
compression: boolean compression: boolean,
customHeaders: Record<string, string>
): Promise< ): Promise<
| string | string
| { | {

View File

@@ -81,9 +81,9 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
this._log("Collecting local files on the DB", LOG_LEVEL_VERBOSE); this._log("Collecting local files on the DB", LOG_LEVEL_VERBOSE);
const _DBEntries = [] as MetaEntry[]; const _DBEntries = [] as MetaEntry[];
// const _DBEntriesTask = [] as (() => Promise<MetaEntry | false>)[];
let count = 0; let count = 0;
for await (const doc of this.localDatabase.findAllNormalDocs()) { // Fetch all documents from the database (including conflicts to prevent overwriting).
for await (const doc of this.localDatabase.findAllNormalDocs({ conflicts: true })) {
count++; count++;
if (count % 25 == 0) if (count % 25 == 0)
this._log( this._log(
@@ -200,9 +200,8 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
if (w && !(w.deleted || w._deleted)) { if (w && !(w.deleted || w._deleted)) {
if (!this.core.$$isFileSizeExceeded(w.size)) { if (!this.core.$$isFileSizeExceeded(w.size)) {
// Prevent applying the conflicted state to the storage. // Prevent applying the conflicted state to the storage.
const conflicted = await this.core.databaseFileAccess.getConflictedRevs(path); if (w._conflicts?.length ?? 0 > 0) {
if (conflicted.length > 0) { this._log(`UPDATE STORAGE: ${path} has conflicts. skipped (x)`, LOG_LEVEL_INFO);
this._log(`UPDATE STORAGE: ${path} has conflicts. skipped`, LOG_LEVEL_INFO);
return; return;
} }
// await this.pullFile(path, undefined, false, undefined, false); // await this.pullFile(path, undefined, false, undefined, false);
@@ -237,8 +236,7 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
runAll("SYNC DATABASE AND STORAGE", fileMap, async (e) => { runAll("SYNC DATABASE AND STORAGE", fileMap, async (e) => {
const { file, doc } = e; const { file, doc } = e;
// Prevent applying the conflicted state to the storage. // Prevent applying the conflicted state to the storage.
const conflicted = await this.core.databaseFileAccess.getConflictedRevs(file.path); if (doc._conflicts?.length ?? 0 > 0) {
if (conflicted.length > 0) {
this._log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO); this._log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO);
return; return;
} }

View File

@@ -1,7 +1,16 @@
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_DEBUG, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { LOG_LEVEL_DEBUG, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "../../deps.ts"; import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "../../deps.ts";
import { type EntryDoc, type FilePathWithPrefix } from "../../lib/src/common/types.ts"; import {
type CouchDBCredentials,
type EntryDoc,
type FilePathWithPrefix,
type JWTCredentials,
type JWTHeader,
type JWTParams,
type JWTPayload,
type PreparedJWT,
} from "../../lib/src/common/types.ts";
import { getPathFromTFile } from "../../common/utils.ts"; import { getPathFromTFile } from "../../common/utils.ts";
import { import {
disableEncryption, disableEncryption,
@@ -13,7 +22,9 @@ import {
import { setNoticeClass } from "../../lib/src/mock_and_interop/wrapper.ts"; import { setNoticeClass } from "../../lib/src/mock_and_interop/wrapper.ts";
import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts"; import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts";
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts"; import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts";
import { reactive, reactiveSource } from "octagonal-wheels/dataobject/reactive"; import { reactive, reactiveSource } from "octagonal-wheels/dataobject/reactive.js";
import { arrayBufferToBase64Single, writeString } from "../../lib/src/string_and_binary/convert.ts";
import { Refiner } from "octagonal-wheels/dataobject/Refiner";
setNoticeClass(Notice); setNoticeClass(Notice);
@@ -99,29 +110,166 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
} }
} }
_importKey(auth: JWTCredentials) {
if (auth.jwtAlgorithm == "HS256" || auth.jwtAlgorithm == "HS512") {
const key = (auth.jwtKey || "").trim();
if (key == "") {
throw new Error("JWT key is empty");
}
const binaryDerString = window.atob(key);
const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}
const hashName = auth.jwtAlgorithm == "HS256" ? "SHA-256" : "SHA-512";
return crypto.subtle.importKey("raw", binaryDer, { name: "HMAC", hash: { name: hashName } }, true, [
"sign",
]);
} else if (auth.jwtAlgorithm == "ES256" || auth.jwtAlgorithm == "ES512") {
const pem = auth.jwtKey
.replace(/-----BEGIN [^-]+-----/, "")
.replace(/-----END [^-]+-----/, "")
.replace(/\s+/g, "");
// const pem = key.replace(/\s/g, "");
const binaryDerString = window.atob(pem);
const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}
// const binaryDer = base64ToArrayBuffer(pem);
const namedCurve = auth.jwtAlgorithm == "ES256" ? "P-256" : "P-521";
const param = { name: "ECDSA", namedCurve };
return crypto.subtle.importKey("pkcs8", binaryDer, param, true, ["sign"]);
} else {
throw new Error("Supplied JWT algorithm is not supported.");
}
}
_currentCryptoKey = new Refiner<JWTCredentials, CryptoKey>({
evaluation: async (auth, previous) => {
return await this._importKey(auth);
},
});
_jwt = new Refiner<JWTParams, PreparedJWT>({
evaluation: async (params, previous) => {
const encodedHeader = btoa(JSON.stringify(params.header));
const encodedPayload = btoa(JSON.stringify(params.payload));
const buff = `${encodedHeader}.${encodedPayload}`.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
const key = await this._currentCryptoKey.update(params.credentials).value;
let token = "";
if (params.header.alg == "ES256" || params.header.alg == "ES512") {
const jwt = await crypto.subtle.sign(
{ name: "ECDSA", hash: { name: "SHA-256" } },
key,
writeString(buff)
);
token = (await arrayBufferToBase64Single(jwt))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
} else if (params.header.alg == "HS256" || params.header.alg == "HS512") {
const jwt = await crypto.subtle.sign(
{ name: "HMAC", hash: { name: params.header.alg } },
key,
writeString(buff)
);
token = (await arrayBufferToBase64Single(jwt))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
} else {
throw new Error("JWT algorithm is not supported.");
}
return {
...params,
token: `${buff}.${token}`,
} as PreparedJWT;
},
});
_jwtParams = new Refiner<JWTCredentials, JWTParams>({
evaluation(source, previous) {
const kid = source.jwtKid || undefined;
const sub = (source.jwtSub || "").trim();
if (sub == "") {
throw new Error("JWT sub is empty");
}
const algorithm = source.jwtAlgorithm || "";
if (!algorithm) {
throw new Error("JWT algorithm is not configured.");
}
if (algorithm != "HS256" && algorithm != "HS512" && algorithm != "ES256" && algorithm != "ES512") {
throw new Error("JWT algorithm is not supported.");
}
const header: JWTHeader = {
alg: source.jwtAlgorithm || "HS256",
typ: "JWT",
kid,
};
const iat = ~~(new Date().getTime() / 1000);
const exp = iat + (source.jwtExpDuration || 5) * 60; // 5 minutes
const payload = {
exp,
iat,
sub: source.jwtSub || "",
"_couchdb.roles": ["_admin"],
} satisfies JWTPayload;
return {
header,
payload,
credentials: source,
};
},
shouldUpdate(isDifferent, source, previous) {
if (isDifferent) {
return true;
}
if (!previous) {
return true;
}
// if expired.
const d = ~~(new Date().getTime() / 1000);
if (previous.payload.exp < d) {
// console.warn(`jwt expired ${previous.payload.exp} < ${d}`);
return true;
}
return false;
},
});
async $$connectRemoteCouchDB( async $$connectRemoteCouchDB(
uri: string, uri: string,
auth: { username: string; password: string }, auth: CouchDBCredentials,
disableRequestURI: boolean, disableRequestURI: boolean,
passphrase: string | false, passphrase: string | false,
useDynamicIterationCount: boolean, useDynamicIterationCount: boolean,
performSetup: boolean, performSetup: boolean,
skipInfo: boolean, skipInfo: boolean,
compression: boolean compression: boolean,
customHeaders: Record<string, string>
): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> { ): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> {
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid"; if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters."; if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces."; if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
const userNameAndPassword = auth.username && auth.password ? `${auth.username}:${auth.password}` : ""; let authHeader = "";
if (this.authHeaderSource.value != userNameAndPassword) { if ("username" in auth) {
this.authHeaderSource.value = userNameAndPassword; const userNameAndPassword = auth.username && auth.password ? `${auth.username}:${auth.password}` : "";
if (this.authHeaderSource.value != userNameAndPassword) {
this.authHeaderSource.value = userNameAndPassword;
}
authHeader = this.authHeader.value;
} else if ("jwtAlgorithm" in auth) {
const params = await this._jwtParams.update(auth).value;
const jwt = await this._jwt.update(params).value;
const token = jwt.token;
authHeader = `Bearer ${token}`;
} }
const authHeader = this.authHeader.value;
// const _this = this;
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = { const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
adapter: "http", adapter: "http",
auth, auth: "username" in auth ? auth : undefined,
skip_setup: !performSetup, skip_setup: !performSetup,
fetch: async (url: string | Request, opts?: RequestInit) => { fetch: async (url: string | Request, opts?: RequestInit) => {
let size = ""; let size = "";
@@ -146,9 +294,18 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
// --> native Fetch API. // --> native Fetch API.
try { try {
if (this.settings.enableDebugTools) { if (customHeaders) {
// Issue #407 for (const [key, value] of Object.entries(customHeaders)) {
(opts!.headers as Headers).append("ngrok-skip-browser-warning", "123"); if (key && value) {
(opts!.headers as Headers).append(key, value);
}
}
// // Issue #407
// (opts!.headers as Headers).append("ngrok-skip-browser-warning", "123");
}
// debugger;
if (!("username" in auth)) {
(opts!.headers as Headers).append("authorization", authHeader);
} }
this.plugin.requestCount.value = this.plugin.requestCount.value + 1; this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
const response: Response = await fetch(url, opts); const response: Response = await fetch(url, opts);

View File

@@ -1,4 +1,4 @@
import { fireAndForget } from "octagonal-wheels/promises"; import { delay, fireAndForget } from "octagonal-wheels/promises";
import { __onMissingTranslation } from "../../lib/src/common/i18n"; import { __onMissingTranslation } from "../../lib/src/common/i18n";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
@@ -6,6 +6,7 @@ import { eventHub } from "../../common/events";
import { enableTestFunction } from "./devUtil/testUtils.ts"; import { enableTestFunction } from "./devUtil/testUtils.ts";
import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts"; import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { FilePathWithPrefix } from "../../lib/src/common/types.ts";
export class ModuleDev extends AbstractObsidianModule implements IObsidianModule { export class ModuleDev extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> { $everyOnloadStart(): Promise<boolean> {
@@ -98,9 +99,41 @@ export class ModuleDev extends AbstractObsidianModule implements IObsidianModule
} }
async $everyOnLayoutReady(): Promise<boolean> { async $everyOnLayoutReady(): Promise<boolean> {
if (!this.settings.enableDebugTools) return Promise.resolve(true); if (!this.settings.enableDebugTools) return Promise.resolve(true);
if (await this.core.storageAccess.isExistsIncludeHidden("_SHOWDIALOGAUTO.md")) { // if (await this.core.storageAccess.isExistsIncludeHidden("_SHOWDIALOGAUTO.md")) {
void this.core.$$showView(VIEW_TYPE_TEST); // void this.core.$$showView(VIEW_TYPE_TEST);
} // }
this.addCommand({
id: "test-create-conflict",
name: "Create conflict",
callback: async () => {
const filename = "test-create-conflict.md";
const content = `# Test create conflict\n\n`;
const w = await this.core.databaseFileAccess.store({
name: filename as FilePathWithPrefix,
path: filename as FilePathWithPrefix,
body: new Blob([content], { type: "text/markdown" }),
stat: {
ctime: new Date().getTime(),
mtime: new Date().getTime(),
size: content.length,
type: "file",
},
});
if (w) {
const id = await this.core.$$path2id(filename as FilePathWithPrefix);
const f = await this.core.localDatabase.getRaw(id);
console.log(f);
console.log(f._rev);
const revConflict = f._rev.split("-")[0] + "-" + (parseInt(f._rev.split("-")[1]) + 1).toString();
console.log(await this.core.localDatabase.bulkDocsRaw([f], { new_edits: false }));
console.log(
await this.core.localDatabase.bulkDocsRaw([{ ...f, _rev: revConflict }], { new_edits: false })
);
}
},
});
await delay(1);
return true; return true;
} }
testResults = writable<[boolean, string, string][]>([]); testResults = writable<[boolean, string, string][]>([]);

View File

@@ -160,6 +160,10 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
} }
async _dumpFileList(outFile?: string) { async _dumpFileList(outFile?: string) {
if (!this.core || !this.core.storageAccess) {
this._log("No storage access", LOG_LEVEL_INFO);
return;
}
const files = this.core.storageAccess.getFiles(); const files = this.core.storageAccess.getFiles();
const out = [] as any[]; const out = [] as any[];
const webcrypto = await getWebCrypto(); const webcrypto = await getWebCrypto();

View File

@@ -94,6 +94,14 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
region: settings.region, region: settings.region,
secretKey: settings.secretKey, secretKey: settings.secretKey,
useCustomRequestHandler: settings.useCustomRequestHandler, useCustomRequestHandler: settings.useCustomRequestHandler,
bucketCustomHeaders: settings.bucketCustomHeaders,
couchDB_CustomHeaders: settings.couchDB_CustomHeaders,
useJWT: settings.useJWT,
jwtKey: settings.jwtKey,
jwtAlgorithm: settings.jwtAlgorithm,
jwtKid: settings.jwtKid,
jwtExpDuration: settings.jwtExpDuration,
jwtSub: settings.jwtSub,
}; };
settings.encryptedCouchDBConnection = await this.encryptConfigurationItem( settings.encryptedCouchDBConnection = await this.encryptConfigurationItem(
JSON.stringify(connectionSetting), JSON.stringify(connectionSetting),

View File

@@ -191,6 +191,11 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
delete saveData.couchDB_USER; delete saveData.couchDB_USER;
delete saveData.couchDB_PASSWORD; delete saveData.couchDB_PASSWORD;
delete saveData.passphrase; delete saveData.passphrase;
delete saveData.jwtKey;
delete saveData.jwtKid;
delete saveData.jwtSub;
delete saveData.couchDB_CustomHeaders;
delete saveData.bucketCustomHeaders;
} }
return saveData; return saveData;
} }

View File

@@ -9,7 +9,12 @@ import { configURIBase, configURIBaseQR } from "../../common/types.ts";
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js"; // import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js";
import { decrypt, encrypt } from "../../lib/src/encryption/e2ee_v2.ts"; import { decrypt, encrypt } from "../../lib/src/encryption/e2ee_v2.ts";
import { fireAndForget } from "../../lib/src/common/utils.ts"; import { fireAndForget } from "../../lib/src/common/utils.ts";
import { EVENT_REQUEST_COPY_SETUP_URI, EVENT_REQUEST_OPEN_SETUP_URI, eventHub } from "../../common/events.ts"; import {
EVENT_REQUEST_COPY_SETUP_URI,
EVENT_REQUEST_OPEN_SETUP_URI,
EVENT_REQUEST_SHOW_SETUP_QR,
eventHub,
} from "../../common/events.ts";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { decodeAnyArray, encodeAnyArray } from "../../common/utils.ts"; import { decodeAnyArray, encodeAnyArray } from "../../common/utils.ts";
import qrcode from "qrcode-generator"; import qrcode from "qrcode-generator";
@@ -54,6 +59,7 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
}); });
eventHub.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => fireAndForget(() => this.command_openSetupURI())); eventHub.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => fireAndForget(() => this.command_openSetupURI()));
eventHub.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => fireAndForget(() => this.command_copySetupURI())); eventHub.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => fireAndForget(() => this.command_copySetupURI()));
eventHub.onEvent(EVENT_REQUEST_SHOW_SETUP_QR, () => fireAndForget(() => this.encodeQR()));
return Promise.resolve(true); return Promise.resolve(true);
} }
async encodeQR() { async encodeQR() {

View File

@@ -35,7 +35,7 @@ import {
readAsBlob, readAsBlob,
sizeToHumanReadable, sizeToHumanReadable,
} from "../../../lib/src/common/utils.ts"; } from "../../../lib/src/common/utils.ts";
import { versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts"; import { arrayBufferToBase64Single, versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts";
import { Logger } from "../../../lib/src/common/logger.ts"; import { Logger } from "../../../lib/src/common/logger.ts";
import { import {
balanceChunkPurgedDBs, balanceChunkPurgedDBs,
@@ -72,6 +72,7 @@ import {
EVENT_REQUEST_OPEN_SETUP_URI, EVENT_REQUEST_OPEN_SETUP_URI,
EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_REQUEST_RELOAD_SETTING_TAB,
EVENT_REQUEST_RUN_DOCTOR, EVENT_REQUEST_RUN_DOCTOR,
EVENT_REQUEST_SHOW_SETUP_QR,
eventHub, eventHub,
} from "../../../common/events.ts"; } from "../../../common/events.ts";
import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock"; import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
@@ -81,6 +82,7 @@ import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSy
import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts"; import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts";
import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts"; import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
import { mount } from "svelte"; import { mount } from "svelte";
import { getWebCrypto } from "../../../lib/src/mods.ts";
export type OnUpdateResult = { export type OnUpdateResult = {
visibility?: boolean; visibility?: boolean;
@@ -814,6 +816,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
return false; return false;
}; };
const enableOnlySyncDisabled = enableOnly(() => !isAnySyncEnabled()); const enableOnlySyncDisabled = enableOnly(() => !isAnySyncEnabled());
const combineOnUpdate = (func1: OnUpdateFunc, func2: OnUpdateFunc): OnUpdateFunc => {
return () => ({
...func1(),
...func2(),
});
};
const onlyOnP2POrCouchDB = () => const onlyOnP2POrCouchDB = () =>
({ ({
visibility: visibility:
@@ -979,7 +987,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI);
}); });
}); });
new Setting(paneEl)
.setName($msg("Setup.ShowQRCode"))
.setDesc($msg("Setup.ShowQRCode.Desc"))
.addButton((text) => {
text.setButtonText($msg("Setup.ShowQRCode")).onClick(() => {
eventHub.emitEvent(EVENT_REQUEST_SHOW_SETUP_QR);
});
});
}); });
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleReset")).then((paneEl) => { void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleReset")).then((paneEl) => {
new Setting(paneEl) new Setting(paneEl)
.setName($msg("obsidianLiveSyncSettingTab.nameDiscardSettings")) .setName($msg("obsidianLiveSyncSettingTab.nameDiscardSettings"))
@@ -1444,6 +1461,10 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
new Setting(paneEl).autoWireText("bucket", { holdValue: true }); new Setting(paneEl).autoWireText("bucket", { holdValue: true });
new Setting(paneEl).autoWireToggle("useCustomRequestHandler", { holdValue: true }); new Setting(paneEl).autoWireToggle("useCustomRequestHandler", { holdValue: true });
new Setting(paneEl).autoWireTextArea("bucketCustomHeaders", {
holdValue: true,
placeHolder: "x-custom-header: value\n x-custom-header2: value2",
});
new Setting(paneEl) new Setting(paneEl)
.setName($msg("obsidianLiveSyncSettingTab.nameTestConnection")) .setName($msg("obsidianLiveSyncSettingTab.nameTestConnection"))
.addButton((button) => .addButton((button) =>
@@ -1465,6 +1486,7 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
"secretKey", "secretKey",
"bucket", "bucket",
"useCustomRequestHandler", "useCustomRequestHandler",
"bucketCustomHeaders",
]) ])
.addOnUpdate(onlyOnMinIO); .addOnUpdate(onlyOnMinIO);
}); });
@@ -1511,20 +1533,119 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
holdValue: true, holdValue: true,
onUpdate: enableOnlySyncDisabled, onUpdate: enableOnlySyncDisabled,
}); });
new Setting(paneEl).autoWireText("couchDB_USER", { new Setting(paneEl).autoWireToggle("useJWT", {
holdValue: true, holdValue: true,
onUpdate: enableOnlySyncDisabled, onUpdate: enableOnlySyncDisabled,
}); });
new Setting(paneEl).autoWireText("couchDB_USER", {
holdValue: true,
onUpdate: combineOnUpdate(
enableOnlySyncDisabled,
visibleOnly(() => !this.editingSettings.useJWT)
),
});
new Setting(paneEl).autoWireText("couchDB_PASSWORD", { new Setting(paneEl).autoWireText("couchDB_PASSWORD", {
holdValue: true, holdValue: true,
isPassword: true, isPassword: true,
onUpdate: enableOnlySyncDisabled, onUpdate: combineOnUpdate(
enableOnlySyncDisabled,
visibleOnly(() => !this.editingSettings.useJWT)
),
});
const algorithms = {
["HS256"]: "HS256",
["HS512"]: "HS512",
["ES256"]: "ES256",
["ES512"]: "ES512",
} as const;
new Setting(paneEl).autoWireDropDown("jwtAlgorithm", {
options: algorithms,
onUpdate: combineOnUpdate(
enableOnlySyncDisabled,
visibleOnly(() => this.editingSettings.useJWT)
),
});
new Setting(paneEl).autoWireTextArea("jwtKey", {
holdValue: true,
onUpdate: combineOnUpdate(
enableOnlySyncDisabled,
visibleOnly(() => this.editingSettings.useJWT)
),
});
// eslint-disable-next-line prefer-const
let generatedKeyDivEl: HTMLDivElement;
new Setting(paneEl)
.setDesc("Generate ES256 Keypair for testing")
.addButton((button) =>
button.setButtonText("Generate").onClick(async () => {
const crypto = await getWebCrypto();
const keyPair = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
true,
["sign", "verify"]
);
const pubKey = await crypto.subtle.exportKey("spki", keyPair.publicKey);
const privateKey = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
const encodedPublicKey = await arrayBufferToBase64Single(pubKey);
const encodedPrivateKey = await arrayBufferToBase64Single(privateKey);
const privateKeyPem = `> -----BEGIN PRIVATE KEY-----\n> ${encodedPrivateKey}\n> -----END PRIVATE KEY-----`;
const publicKeyPem = `> -----BEGIN PUBLIC KEY-----\\n${encodedPublicKey}\\n-----END PUBLIC KEY-----`;
const title = $msg("Setting.GenerateKeyPair.Title");
const msg = $msg("Setting.GenerateKeyPair.Desc", {
public_key: publicKeyPem,
private_key: privateKeyPem,
});
await MarkdownRenderer.render(
this.plugin.app,
"## " + title + "\n\n" + msg,
generatedKeyDivEl,
"/",
this.plugin
);
})
)
.addOnUpdate(
combineOnUpdate(
enableOnlySyncDisabled,
visibleOnly(() => this.editingSettings.useJWT)
)
);
generatedKeyDivEl = this.createEl(
paneEl,
"div",
{ text: "" },
(el) => {},
visibleOnly(() => this.editingSettings.useJWT)
);
new Setting(paneEl).autoWireText("jwtKid", {
holdValue: true,
onUpdate: combineOnUpdate(
enableOnlySyncDisabled,
visibleOnly(() => this.editingSettings.useJWT)
),
});
new Setting(paneEl).autoWireText("jwtSub", {
holdValue: true,
onUpdate: combineOnUpdate(
enableOnlySyncDisabled,
visibleOnly(() => this.editingSettings.useJWT)
),
});
new Setting(paneEl).autoWireNumeric("jwtExpDuration", {
holdValue: true,
onUpdate: combineOnUpdate(
enableOnlySyncDisabled,
visibleOnly(() => this.editingSettings.useJWT)
),
}); });
new Setting(paneEl).autoWireText("couchDB_DBNAME", { new Setting(paneEl).autoWireText("couchDB_DBNAME", {
holdValue: true, holdValue: true,
onUpdate: enableOnlySyncDisabled, onUpdate: enableOnlySyncDisabled,
}); });
new Setting(paneEl).autoWireTextArea("couchDB_CustomHeaders", { holdValue: true });
new Setting(paneEl) new Setting(paneEl)
.setName($msg("obsidianLiveSyncSettingTab.nameTestDatabaseConnection")) .setName($msg("obsidianLiveSyncSettingTab.nameTestDatabaseConnection"))
.setClass("wizardHidden") .setClass("wizardHidden")
@@ -1562,6 +1683,13 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
"couchDB_USER", "couchDB_USER",
"couchDB_PASSWORD", "couchDB_PASSWORD",
"couchDB_DBNAME", "couchDB_DBNAME",
"jwtAlgorithm",
"jwtExpDuration",
"jwtKey",
"jwtSub",
"jwtKid",
"useJWT",
"couchDB_CustomHeaders",
]) ])
.addOnUpdate(onlyOnCouchDB); .addOnUpdate(onlyOnCouchDB);
}); });
@@ -1985,7 +2113,6 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
LEVEL_ADVANCED LEVEL_ADVANCED
).then((paneEl) => { ).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
new Setting(paneEl) new Setting(paneEl)
.autoWireText("settingSyncFile", { holdValue: true }) .autoWireText("settingSyncFile", { holdValue: true })
.addApplyButton(["settingSyncFile"]); .addApplyButton(["settingSyncFile"]);
@@ -2330,6 +2457,11 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase); pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase);
pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID); pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID);
pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays); pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays);
pluginConfig.jwtKey = redact(pluginConfig.jwtKey);
pluginConfig.jwtSub = redact(pluginConfig.jwtSub);
pluginConfig.jwtKid = redact(pluginConfig.jwtKid);
pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders);
pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders);
const endpoint = pluginConfig.endpoint; const endpoint = pluginConfig.endpoint;
if (endpoint == "") { if (endpoint == "") {
pluginConfig.endpoint = "Not configured or AWS"; pluginConfig.endpoint = "Not configured or AWS";
@@ -3383,6 +3515,7 @@ ${stringifyYaml(pluginConfig)}`;
const region = this.plugin.settings.region; const region = this.plugin.settings.region;
const endpoint = this.plugin.settings.endpoint; const endpoint = this.plugin.settings.endpoint;
const useCustomRequestHandler = this.plugin.settings.useCustomRequestHandler; const useCustomRequestHandler = this.plugin.settings.useCustomRequestHandler;
const customHeaders = this.plugin.settings.bucketCustomHeaders;
return new JournalSyncMinio( return new JournalSyncMinio(
id, id,
key, key,
@@ -3391,7 +3524,8 @@ ${stringifyYaml(pluginConfig)}`;
this.plugin.simpleStore, this.plugin.simpleStore,
this.plugin, this.plugin,
useCustomRequestHandler, useCustomRequestHandler,
region region,
customHeaders
); );
} }
async resetRemoteBucket() { async resetRemoteBucket() {

View File

@@ -443,4 +443,12 @@ span.ls-mark-cr::after {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
max-width: max-content; max-width: max-content;
}
.sls-keypair pre {
max-width: 100%;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
} }