feat: Desktop auto-updater + release SBOMs

- tauri-plugin-updater + process: pubkey embedded in tauri.conf.json,
  createUpdaterArtifacts, endpoint = GitHub latest.json manifest.
- Connect screen checks before auto-connect (bounded 5s) — a saved
  URL would otherwise navigate away before the check resolves.
  "Update & restart" downloads, verifies minisign, installs, relaunches.
- Release workflow signs updater artifacts with
  TAURI_SIGNING_PRIVATE_KEY secrets, ships the .sig files, and assembles
  latest.json from them. No key → no manifest → no update, never a
  silent unsigned install.
- anchore/sbom-action emits CycloneDX SBOMs per GHCR image plus the
  source tree, attached to every release.

Note: the signing keypair was generated locally
(~/.tauri/primora-updater.key); the private half must be added to repo
secrets before the next release for updates to activate.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-24 20:41:16 +02:00
1 parent fd36389328
commit edef002beb
10 files changed
+645 -16

No files matched your search

+98
View File
@@ -146,6 +146,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Build Tauri bundles
# Updater artifacts are minisign-signed; without the key the build
# still succeeds but emits no .sig files (updater can't verify them).
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: npm run bundle --workspace @primora/desktop
- uses: actions/upload-artifact@v4
with:
@@ -155,8 +160,11 @@ jobs:
apps/desktop/src-tauri/target/release/bundle/**/*.deb
apps/desktop/src-tauri/target/release/bundle/**/*.rpm
apps/desktop/src-tauri/target/release/bundle/**/*.dmg
apps/desktop/src-tauri/target/release/bundle/**/*.app.tar.gz
apps/desktop/src-tauri/target/release/bundle/**/*.msi
apps/desktop/src-tauri/target/release/bundle/**/*.exe
apps/desktop/src-tauri/target/release/bundle/**/*.nsis.zip
apps/desktop/src-tauri/target/release/bundle/**/*.sig
if-no-files-found: error
npm-packages:
@@ -225,6 +233,48 @@ jobs:
flag
' CHANGELOG.md > notes.md
[ -s notes.md ] || echo "Release ${VERSION}" > notes.md
# Tauri updater manifest: one platform entry per signed updater
# artifact (AppImage.sig / app.tar.gz.sig / nsis.zip.sig). Missing
# sigs mean TAURI_SIGNING_PRIVATE_KEY isn't set — then there is no
# manifest and no update, which is correct (never ship unsigned).
- name: Build updater manifest (latest.json)
run: |
python3 - <<'PY'
import json, os, glob
base = "https://github.com/Dvorinka/Primora/releases/download/%s" % os.environ["VERSION"]
platforms = {}
for sig in glob.glob("dist-release/**/*.sig", recursive=True):
asset = sig[:-4]
name = os.path.basename(asset)
if name.endswith(".AppImage"):
plat = "linux-x86_64"
elif name.endswith(".app.tar.gz"):
plat = "darwin-aarch64" if "aarch64" in name else "darwin-x86_64"
if "aarch64" not in name and "x86_64" not in name:
plat = "darwin-aarch64" # macos-latest is arm64
elif name.endswith(".nsis.zip"):
plat = "windows-x86_64"
else:
continue
platforms[plat] = {
"signature": open(sig).read().strip(),
"url": f"{base}/{name}",
}
manifest = {
"version": os.environ["VERSION"],
"notes": "See the release notes.",
"pub_date": __import__("datetime").datetime.now(
__import__("datetime").timezone.utc
).strftime("%Y-%m-%dT%H:%M:%SZ"),
"platforms": platforms,
}
if platforms:
with open("dist-release/latest.json", "w") as f:
json.dump(manifest, f, indent=2)
print("latest.json:", ", ".join(platforms))
else:
print("no signed updater artifacts — skipping latest.json")
PY
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -234,6 +284,54 @@ jobs:
mapfile -t files < <(find dist-release -type f)
[ ${#files[@]} -gt 0 ] || { echo "no release assets found"; exit 1; }
gh release create "$VERSION" --title "$VERSION" --notes-file notes.md "${files[@]}"
# The manifest must be fetchable at /releases/latest/download/latest.json
if [ -f dist-release/latest.json ]; then
gh release upload "$VERSION" dist-release/latest.json --clobber
fi
# Software bill of materials — CycloneDX JSON per published image plus
# one for the source tree (npm + Go modules). Attached to the release
# next to the binaries; downstream consumers can verify provenance.
sbom:
name: SBOMs
needs: [verify, release]
if: needs.verify.outputs.needed == 'true'
runs-on: ubuntu-latest
permissions:
packages: read
env:
VERSION: ${{ needs.verify.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository_owner }}/primora-backend:${{ needs.verify.outputs.version }}
format: cyclonedx-json
output-file: dist-release/sbom-backend.cdx.json
- uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository_owner }}/primora-auth:${{ needs.verify.outputs.version }}
format: cyclonedx-json
output-file: dist-release/sbom-auth.cdx.json
- uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository_owner }}/primora-frontend:${{ needs.verify.outputs.version }}
format: cyclonedx-json
output-file: dist-release/sbom-frontend.cdx.json
- uses: anchore/sbom-action@v0
with:
path: .
format: cyclonedx-json
output-file: dist-release/sbom-source.cdx.json
- name: Attach SBOMs to the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "$VERSION" dist-release/sbom-*.cdx.json --clobber
npm-publish:
name: Publish to npm
+2
View File
@@ -26,6 +26,8 @@ All notable changes to Primora. Format follows [Keep a Changelog](https://keepac
- **S3-compatible object storage backend** — `BACKEND_STORAGE_DRIVER=s3` swaps the local disk store for any S3-compatible API (AWS, MinIO, Garage, R2) via a stdlib-only SigV4 implementation — no AWS SDK dependency. `S3_ENDPOINT`/`S3_REGION`/`S3_BUCKET`/credentials configure it; `S3_PATH_STYLE` (default on) covers MinIO/Garage, off for AWS virtual-hosted. The configured bucket is created on startup if missing; objects stream with computed SHA-256, `storage_path` records `s3://bucket/key`. Verified live end-to-end (bucket → put → get → copy → list → delete).
- **Functions** (`core.functions` + `core.function_runs`, migration 00015) — project-scoped JS/TS executed by a runtime binary on the backend host (`bun` or `deno`, per function). The JSON payload arrives on stdin and as `PRIMORA_PAYLOAD`; stdout/stderr are captured per run (capped at 64 KiB) with status, exit code and duration — `FUNCTIONS_RUNTIME`/`FUNCTIONS_TIMEOUT_SECONDS`/`FUNCTIONS_MAX_OUTPUT_BYTES` configure the runner. API: CRUD under `/projects/:id/functions`, `POST …/invoke` (502 + recorded run on failure), `GET …/runs`. Dashboard gains a Functions page: code editor, JSON payload invoke, live result, run history. Code executes with backend privileges — the documented self-hosted trust model.
- **Presigned object URLs** — `POST /buckets/:bucketID/object-presigns` mints SigV4 query-signed `GET`/`PUT` URLs (60 s–1 h TTL) so clients transfer large objects directly against S3 without proxying through the backend. Read role for downloads (object must exist — no key probing), write role for uploads. `S3_PUBLIC_ENDPOINT` rewrites the client-facing host for internal `S3_ENDPOINT`s; signatures bind the host so URLs are valid only where they were minted. The local driver returns a clear error — a presigned URL needs a real object API.
- **Desktop auto-updater** — `tauri-plugin-updater` wired end to end: minisign keypair, `createUpdaterArtifacts`, `latest.json` manifest built from signed artifacts on every release, and a silent background check on the connect screen offering "Update & restart" (download → verify → install → relaunch). Unsigned artifacts can never install — missing signature means no manifest, not a bypass. CI signs via `TAURI_SIGNING_PRIVATE_KEY` secrets; OS-level signing (notarization/Authenticode) remains open.
- **Release SBOMs** — CycloneDX SBOM for each published GHCR image (backend/auth/frontend) plus one for the source tree, attached to every GitHub Release via `anchore/sbom-action`.
- **Function triggers** (migration 00016) — three new ways to run a function, all through the same executor and run log. Scheduled jobs accept `function_id` instead of `url` (run status mirrors the exit code); inbound hooks gain `mode: "function"` — the received body becomes the function's stdin payload, acked immediately with the run recorded async; and `event_pattern` on a function (`document.*`, `*`, or an exact type) fires it detached whenever a matching domain event publishes — `{event, project_id, occurred_at, data}` as payload. Runs record trigger source: `manual`/`schedule`/`hook`/`event`. Dashboard: function event-trigger field, job target picker (URL or function), and inbound-hook function mode.
## [0.7.1] - 2026-09-23
+9 -4
View File
@@ -123,8 +123,11 @@ Follow-ups:
- Verify the macOS/Windows desktop bundles produced by the `v0.5.0` CI run
(host-verified on Linux only).
- Desktop bundle signing + auto-updater — unsigned artifacts today; add when
distribution matures.
- ~~Desktop bundle signing + auto-updater~~ ✅ — `tauri-plugin-updater`
wired: signed updater artifacts (`createUpdaterArtifacts`), minisign
keypair, `latest.json` manifest assembled on release, in-app
"Update & restart" on the connect screen. macOS/Windows OS-level code
signing (notarization/Authenticode) still open — needs paid certs.
## Phase 6 — Production hardening (v0.6.0) ✅
@@ -205,8 +208,10 @@ release pipeline with images + binaries + desktop bundles.
- ~~**arm64 images**~~ ✅ — release builds `linux/amd64,linux/arm64` via
QEMU + buildx.
- Carried forward from Phase 5 follow-ups: macOS/Windows bundle
verification, signing + auto-updater.
- Optional: SBOM + provenance attestation on release images.
verification; updater + minisign signing shipped (OS-level signing
remains open).
- ~~Optional: SBOM~~ ✅ — CycloneDX SBOMs per published image + source
tree, attached to every release (`anchore/sbom-action`).
### Docs
+18
View File
@@ -44,3 +44,21 @@ file, one crypto implementation — terminal and desktop always agree. Requires
Injection and the credential broker stay CLI-only (`primora inject`,
`primora agent`).
- Deep links / push notifications: deliberately omitted — nothing needs them yet.
## Auto-updates
`tauri-plugin-updater` checks `releases/latest/download/latest.json` on the
GitHub repo when the connect screen loads; if a newer signed release exists
it offers "Update & restart" — download, minisign verification, install,
relaunch. No update manifest or signature failure = silent no-op; unsigned
artifacts can never install.
Release side (already wired in `.github/workflows/release.yml`): the desktop
job signs updater artifacts with `TAURI_SIGNING_PRIVATE_KEY` (+ optional
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD`) repo secrets, uploads the `.sig`
files, and the release job assembles `latest.json` from them. Without the
secret the build still succeeds but emits no signatures — and no manifest.
The matching public key is embedded in `tauri.conf.json`. The private key
lives only in repo secrets and on the maintainer's machine — regenerate with
`npx tauri signer generate` and update both if it is ever lost.
+457 -8
View File
@@ -47,6 +47,15 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -506,6 +515,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -529,7 +548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
dependencies = [
"bitflags 2.13.2",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"libc",
@@ -542,7 +561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.13.2",
"core-foundation",
"core-foundation 0.10.1",
"libc",
]
@@ -713,6 +732,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -996,6 +1026,16 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.12"
@@ -1254,6 +1294,17 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -1530,6 +1581,21 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -1548,9 +1614,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@@ -1847,6 +1915,36 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.20",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.119",
]
[[package]]
name = "jni-sys"
version = "0.3.1"
@@ -2036,6 +2134,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -2288,6 +2392,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.2",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@@ -2303,6 +2408,18 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.13.2",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -2366,6 +2483,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -2382,6 +2505,20 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.20",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -2615,7 +2752,9 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-process",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
]
[[package]]
@@ -2804,15 +2943,20 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -2824,6 +2968,20 @@ dependencies = [
"web-sys",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-hash"
version = "2.1.3"
@@ -2852,6 +3010,79 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni 0.22.4",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.23"
@@ -2867,6 +3098,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "schemars"
version = "0.8.22"
@@ -2924,6 +3164,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.13.2",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "selectors"
version = "0.36.1"
@@ -3145,6 +3408,22 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "simd_cesu8"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "1.0.3"
@@ -3257,6 +3536,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.8"
@@ -3320,6 +3605,27 @@ dependencies = [
"syn 3.0.5",
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags 2.13.2",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -3341,7 +3647,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
dependencies = [
"bitflags 2.13.2",
"block2",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dbus",
@@ -3351,7 +3657,7 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
"jni",
"jni 0.21.1",
"libc",
"log",
"ndk",
@@ -3384,6 +3690,17 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "tar"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
@@ -3408,7 +3725,7 @@ dependencies = [
"heck 0.5.0",
"http",
"image",
"jni",
"jni 0.21.1",
"libc",
"log",
"mime",
@@ -3504,6 +3821,32 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020"
dependencies = [
"anyhow",
"glob",
"plist",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri-utils",
"walkdir",
]
[[package]]
name = "tauri-plugin-process"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.4.4"
@@ -3520,6 +3863,39 @@ dependencies = [
"zbus",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5cad8ed5948d988e1018ecd31e27cacedbf72ce3fb972940c3e4bf51639e4b"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.20",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
]
[[package]]
name = "tauri-runtime"
version = "2.11.3"
@@ -3530,7 +3906,7 @@ dependencies = [
"dpi",
"gtk",
"http",
"jni",
"jni 0.21.1",
"objc2",
"objc2-ui-kit",
"objc2-web-kit",
@@ -3553,7 +3929,7 @@ checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
dependencies = [
"gtk",
"http",
"jni",
"jni 0.21.1",
"log",
"objc2",
"objc2-app-kit",
@@ -3742,6 +4118,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-rustls"
version = "0.26.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.19"
@@ -4055,6 +4441,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.8"
@@ -4298,6 +4690,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"
@@ -4483,6 +4884,17 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -4528,6 +4940,15 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -4823,7 +5244,7 @@ dependencies = [
"gtk",
"http",
"javascriptcore-rs",
"jni",
"jni 0.21.1",
"libc",
"ndk",
"objc2",
@@ -4870,6 +5291,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]]
name = "yoke"
version = "0.8.3"
@@ -4984,6 +5415,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
version = "0.2.5"
@@ -5017,6 +5454,18 @@ dependencies = [
"syn 3.0.5",
]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.2",
"memchr",
]
[[package]]
name = "zlib-rs"
version = "0.6.8"
+2
View File
@@ -15,5 +15,7 @@ tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -3,5 +3,5 @@
"identifier": "default",
"description": "Default capabilities for the main window",
"windows": ["main", "vault"],
"permissions": ["core:default"]
"permissions": ["core:default", "updater:default", "process:default"]
}
+2
View File
@@ -200,6 +200,8 @@ pub fn run() {
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
show_main(app);
}))
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.invoke_handler(tauri::generate_handler![
get_server_url,
set_server_url,
+9
View File
@@ -23,9 +23,18 @@
"csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost https: http:; img-src 'self' data:; style-src 'self'"
}
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDVERjFDMzI1MURGMTlBNDAKUldSQW12RWRKY1B4WFpQN2N0RjdCemllN1ptcDhObFJQMVdHL3pLTXMzNEN6TlJoMEZhQUhDNG4K",
"endpoints": [
"https://github.com/Dvorinka/Primora/releases/latest/download/latest.json"
]
}
},
"bundle": {
"active": true,
"targets": "all",
"createUpdaterArtifacts": true,
"category": "DeveloperTool",
"shortDescription": "Primora desktop client",
"longDescription": "Thin desktop shell for a self-hosted Primora deployment. Renders the console served by your own server.",
+47 -3
View File
@@ -28,6 +28,10 @@
<p id="err" class="err" hidden></p>
<p class="hint">The desktop app renders the console served by your own server. Nothing is hosted by us.</p>
<p class="hint"><a href="#" id="vault-link" class="vlink">Open the local Secrets Vault</a></p>
<div id="update" hidden>
<p class="hint" id="update-text"></p>
<button type="button" id="update-install">Update &amp; restart</button>
</div>
</main>
<script type="module">
@@ -44,12 +48,52 @@
button.textContent = "Connect";
};
// Updater — check before auto-connecting: a saved URL redirects
// immediately, so the check must resolve first or it never shows.
// Bounded at 5s; failures are silent (dev builds, offline, no
// manifest). Unsigned artifacts can never install — minisign is
// enforced by the plugin against the pubkey baked into the bundle.
const updateP = (tauri?.updater && tauri?.process)
? Promise.race([
tauri.updater.check().catch(() => null),
new Promise((r) => setTimeout(() => r(null), 5000)),
])
: Promise.resolve(null);
if (!tauri) {
fail("This page is meant to run inside the Primora desktop app.");
} else {
// A saved deployment URL means "skip the form" on every launch.
const saved = await tauri.core.invoke("get_server_url").catch(() => null);
if (saved) location.replace(saved);
// A saved deployment URL means "skip the form" on every launch —
// unless an update is waiting, which wins the redirect.
const update = await updateP;
if (update) {
const box = document.getElementById("update");
const text = document.getElementById("update-text");
const install = document.getElementById("update-install");
text.textContent = `Update available — v${update.version} (you have ${update.currentVersion}).`;
box.hidden = false;
install.addEventListener("click", async () => {
install.disabled = true;
install.textContent = "Downloading…";
try {
await update.downloadAndInstall();
await tauri.process.relaunch();
} catch (e) {
install.disabled = false;
install.textContent = "Update & restart";
text.textContent = `Update failed: ${e}`;
}
});
// "Not now" continues to the saved server.
const later = document.createElement("button");
later.type = "button";
later.textContent = "Not now";
later.style.marginLeft = "0.5rem";
later.addEventListener("click", () => { const saved = _savedUrl; if (saved) location.replace(saved); });
box.appendChild(later);
}
const _savedUrl = await tauri.core.invoke("get_server_url").catch(() => null);
if (_savedUrl && !update) location.replace(_savedUrl);
}
document.getElementById("vault-link").addEventListener("click", (e) => {