Development Notes

Releasing New Versions

To release a new version:

  1. Update version in package.json.
  2. Run npm run version to update manifest.json & versions.json. * manifest.json: holds the new version number and the minimum Obsidian version required for your latest release.
    • versions.json: holds the history of version numbers and their minimum Obsidian version, so older versions of Obsidian can download an older version of your plugin that’s compatible.
  3. Run npm run release:check as described under Validation. Resolve any failures before continuing.
  4. Review the generated dist/release-check/report.json and SHA256SUMS. A passed local check produces freshly verified production assets; it does not replace the desktop and mobile checks below.
  5. For changes to editors, password dialogs, or save flows, optionally run the desktop smoke check. Routine releases use release:check; Android testing is not required. Check that the following files were generated for the version in package.json (replace <version> with that version):
    • dist/meld-encrypt-<version>.zip
    • dist/meld-encrypt-<version>/meld-encrypt/manifest.json
    • dist/meld-encrypt-<version>/meld-encrypt/main.js
    • dist/meld-encrypt-<version>/meld-encrypt/styles.css
  6. Finalize the release notes, commit all release changes, and merge next into main. Push main, then create and push an annotated tag matching the version at that commit.
  7. Create a new GitHub release using your new version number as the Tag version. Use the exact version number, and don’t include a prefix v. See the following example: obsidian-sample-plugin.
  8. Upload the ZIP and the three individual plugin files from step 5 as binary attachments. Use the freshly built files for this version.
  9. Verify the attached assets and publish the release as the latest stable version. Confirm that Obsidian detects and installs the update.

The production build requires the zip command. Verify archives with unzip -t; a successful bundle alone does not prove packaging succeeded.

Adding Your Plugin to the Community Plugin List

To add your plugin to the community plugin list:

  1. Publish an initial version.
  2. Make sure you have a README.md file in the root of your repository.
  3. Submit through the Obsidian Community directory, following Obsidian’s release guide. Only the initial release needs submission.

How to Use

  1. Clone this repository.
  2. Run npm i or yarn to install dependencies.
  3. Run npm run dev to start the compiler in watch mode.

Community review: standalone Node CLI

The community review scans tools/src/mdenc.ts even though it is a standalone Node.js recovery tool. It is built separately by scripts/recovery-builds.mjs with platform: 'node'; the Obsidian plugin bundle starts at src/main.ts and does not import the CLI. Moving the source outside src does not exclude it from community review.

The following CLI-only warnings are expected:

  • node:fs and node:path imports provide local filesystem access for listing, checking, and recovering encrypted notes. The CLI runs directly in Node.js, outside Obsidian, so it cannot depend on Obsidian’s Platform API. These imports are not loaded by the mobile plugin.
  • Console output is the CLI’s user interface: file listings (including CSV, JSON, and tables), check results, and recovery progress/results. It is intentional command output, rather than plugin debug logging.

Keep these warnings documented for reviewers instead of adding forbidden rule suppressions or changing the CLI’s output formats. Local lint applies Obsidian’s recommended rules to plugin source and Node/TypeScript rules to CLI source. A clean local lint run therefore does not imply that the broader community review has no CLI warnings. Plugin-side Node imports must still be dynamic and guarded by Obsidian’s Platform.isDesktop API.

Validation

Install dependencies with npm ci, then run:

npm run release:check

The command checks matching versions in the package, lockfile, manifest, and compatibility map; verifies Git, zip, unzip, and installed dependencies; rebuilds recovery bundles in memory; runs type checking, the full test suite, and lint once each; and builds and verifies the release assets. ZIP integrity, exact contents, and byte equality with the standalone files are checked. The current version’s production output directory and ZIP are replaced before building; other versions’ output is preserved.

Tracked files are never rewritten. If recovery bundles are stale, the check stops before testing them and lists the affected files. Regenerate them with:

npm run build-tool-mdenc
npm run dev-tool-mdenc
npm run build-tool-decrypt

Review the changes to tools/mdenc.mjs, tools/mdenc-debug.mjs, tools/offline-decrypt.js, and tools/decrypt.html, then rerun release:check. CLI and offline-decryptor tests execute these tracked files, so they must match fresh builds before tests run. The existing build commands still write their outputs normally.

Every check run replaces dist/release-check/report.json with the version, Git commit, working-tree state, stage results, timings, and any failure details. A successful run also writes dist/release-check/SHA256SUMS, containing hashes of the ZIP and three standalone release assets; paths are relative to the repository root. Verify them with sha256sum -c dist/release-check/SHA256SUMS. Failure returns a nonzero exit status, marks later stages skipped, and removes old checksum output. Uncommitted changes are allowed and recorded in the report.

This is local automated validation. It does not launch Obsidian or perform smoke tests, commit, push, tag, or publish. Optional desktop smoke and update checks remain separate steps. Run git diff --check before committing. For documentation-only changes, check the diff, referenced paths, and claims against the implementation; rebuilding unchanged bundles is unnecessary.

Desktop Smoke Check

Keep routine validation quick with npm run release:check. Android/emulator testing is not part of this project’s release process.

For editor, password-dialog, or save-flow changes, optionally run:

npm run smoke:desktop

This Linux-only command builds the production plugin and runs nine functional checks in a fresh vault: plugin loading, ribbon visibility across delayed plugin reloads, whole-note encryption, password changes and reopening, conversion of unsaved edits, heading navigation in Reading view and Live Preview, cursor and scroll preservation across mode switches, inline editing with hints and marker visibility, and legacy whole-note and inline upgrades. It was verified with Obsidian 1.14.4. It is separate from npm test and release:check.

Use --obsidian /path/to/Obsidian.AppImage to choose an executable, or --timeout 60 to override the default 30-second startup/debug-request timeout. An installed Obsidian desktop, display access, and a Node version with built-in fetch and WebSocket are required. --help lists the options.

tests/test-vault stays reserved for manual testing and remains the target of npm run dev. Automation never reads or changes it. Test notes come from the public fixtures in tests/fixtures/smoke; generated vaults and profiles live under the ignored dist/smoke directory.

Each run retains its report, tested asset snapshot, and logs under dist/smoke/desktop-<id>/. dist/smoke/desktop-latest.json points to the latest report. Failure returns nonzero and attempts a screenshot. The harness cleans up its own app process, vault, and profile on success, failure, or cancellation. Cleanup failures mark the run failed and retain the remaining state for inspection. Nothing is committed, pushed, tagged, or published by the command.

Shared Implementation Paths

  • src/services/PasswordForm.ts owns password and confirmation fields, hint editing, validation, and Enter-key navigation for both password dialogs. Keep validation separate from submission: navigation must not confirm a result that can still be cancelled. Inline-only hint restrictions are supplied by the inline dialog; its marker-visibility and text controls remain in that dialog.
  • FeatureInplaceEncrypt.encryptSelection() is the shared writer for new inline text and edited text saved from the decryption dialog. It validates hints, encrypts with the default format, formats markers, and replaces the selection. Re-saving preserves the existing password, hint, and marker visibility.
  • FeatureWholeNoteEncryptV2.addEncryptedNoteMenuItems() defines the shared Change password and Lock & close actions. Keep eligibility checks in the editor and file menu handlers; the file menu requires the selected file to match the active encrypted view.
  • src/services/Base64.ts provides chunked byte encoding and decoding with atob’s legacy tolerance. Keep v3’s canonical Base64 validation in the decodeBase64() function in CryptoHelperV3.ts; do not apply those stricter rules to legacy readers. The v3 module retains its encodeBase64 export.
  • tools/src/mdenc.ts shares original-input block scanning and password attempts between checking and recovery. Build recovered output from source spans without rescanning or replacing text inside recovered plaintext. An inline note must decrypt completely before its output is written; checking continues to report later blocks after a failure.

Manually Installing the Plugin

Copy the files main.js, styles.css, and manifest.json to your vault at VaultFolder/.obsidian/plugins/your-plugin-id/, replacing your-plugin-id with the actual ID of your plugin.

If the your-plugin-id folder does not exist, create it before copying the files.

API Documentation

For detailed API Documentation, see the Obsidian API repository.

Encryption format v3 (released in 3.0.0)

New whole-note files and inline gamma (γ) blocks use one authenticated encryption format with two representations. Whole-note JSON stores readable parameters:

{
  "version": "3.0",
  "hint": "optional public hint",
  "kdf": {
    "name": "PBKDF2",
    "hash": "SHA-512",
    "iterations": 220000,
    "salt": "<16 random bytes, Base64>"
  },
  "cipher": {
    "name": "AES-GCM",
    "keyLength": 256,
    "iv": "<12 random bytes, Base64>",
    "tagLength": 128
  },
  "encodedData": "<ciphertext followed by the 16-byte authentication tag, Base64>"
}

Inline blocks preserve the existing %%🔐γ … 🔐%% or 🔐γ … 🔐 markers and optional 💡hint💡. Their payload is standard padded Base64 of these bytes:

Offset Length Meaning
0 1 Crypto suite identifier: 1
1 3 PBKDF2 iteration count, unsigned big-endian
4 16 Salt
20 12 IV
32 Remaining, at least 16 Ciphertext followed by authentication tag

Suite 1 permanently means PBKDF2-HMAC-SHA512 and AES-256-GCM, with a 16-byte salt, 12-byte IV and 16-byte tag. It cannot be reassigned when defaults change. The gamma marker supplies the format version. The suite and iteration header adds four bytes to the previous experimental v3 payload; it contains no JSON. A zero-length plaintext still produces a 48-byte payload (64 Base64 characters).

For both representations, AES-GCM additional authenticated data (AAD) is the UTF-8 encoding of compact JSON.stringify({ version, kdf, cipher }). Reconstruct these objects in exactly this order:

  • Root: version, kdf, cipher.
  • KDF: name, hash, iterations, salt.
  • Cipher: name, keyLength, iv, tagLength.

Values match the JSON example; salt and IV use canonical padded Base64. No whitespace or trailing newline is included in AAD. Source JSON property order is irrelevant because readers rebuild this header. Hints and marker visibility are public and are not authenticated. Do not store passwords or derived keys.

Readers validate the complete envelope before deriving a key. Only suite 1 and its corresponding JSON algorithm/length values are supported. Iterations must be integers from 220,000 through 2,000,000; encryption defaults to 220,000, but decryption uses the stored count. Invalid Base64, unknown metadata fields, unsupported parameters, wrong lengths, truncated tags, and out-of-range counts are rejected. Valid-looking metadata changes fail GCM authentication.

Whole-note v1/v2 and inline obsolete/alpha/beta formats retain their existing readers. Saving a legacy whole note upgrades it; inline content upgrades when edited and saved through its decryption modal. Earlier experimental v3 layouts are not supported. Physically blank files retain the existing initialization behavior, but serialized v3 envelopes always require authenticated ciphertext, even for empty text.

After changing the format or its shared helpers, follow Validation to rebuild the tracked decrypt tools and test their generated bundles.


This site uses Just the Docs, a documentation theme for Jekyll.