Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Signing the desktop installers

Until the installers are signed, macOS says it "could not verify" Duct and Windows SmartScreen warns before it runs.
Both go away with signing. This is what to buy and set up; once the secrets below exist, the release workflow
(`.github/workflows/installers.yml`) signs on every release.

Prices and rules were checked in October 2026 and change; confirm them on the vendors' pages before paying.

## macOS: Apple Developer Program (about $99 a year)

1. **D-U-N-S Number for Tensflare Ltd.** Apple requires one for organisations. Look it up first (the company may
already have one) with Apple's D-U-N-S lookup during enrolment; requesting one is free but can take up to about 30
business days (Dun & Bradstreet sells faster processing).
2. **Enrol as an organisation** at developer.apple.com/programs/enroll with an Apple ID that has two-factor
authentication, a work email on the company's domain (tensflare.com) and a public website. The person enrolling must
be able to sign contracts for the company. Apple phones to verify.
3. **Create a "Developer ID Application" certificate** (Certificates, Identifiers & Profiles), export it with its
private key as a `.p12`, and **create an App Store Connect API key** (Users and Access › Integrations) for
notarization: you get a `.p8` file, a key ID and an issuer ID.
4. **Add GitHub secrets** to docfide/duct: `CSC_LINK` (the `.p12`, base64), `CSC_KEY_PASSWORD`, `APPLE_API_KEY`
(the `.p8` contents), `APPLE_API_KEY_ID`, `APPLE_API_ISSUER`.

Then the Mac builds are signed with the hardened runtime and notarized by Apple, and open without a warning.

## Windows: an OV code-signing certificate with cloud signing (about $130 a year plus $15 a month)

Since June 2023, code-signing keys must live in a hardware token or a cloud HSM, so a certificate file alone won't do,
and a USB token can't be used by GitHub's build machines. Microsoft's own low-cost service (Azure Artifact Signing,
about $10 a month) is only open to organisations in the US, Canada, the EU and the UK, and Tensflare Ltd is registered
in Nigeria.

Recommended: **SSL.com OV code signing with eSigner cloud signing.**

1. Buy an **OV code-signing certificate** for Tensflare Ltd (organisation validation: company documents and a phone
check) and the **eSigner** plan for code (Tier 1: 240 signatures a month is plenty; each release signs a handful of
files). New certificates include 30 days of eSigner free, so we can test before paying monthly.
2. **Add GitHub secrets:** `ES_USERNAME`, `ES_PASSWORD`, `ES_CREDENTIAL_ID`, `ES_TOTP_SECRET` (from the eSigner
enrolment).
3. Tell the engineering team; the release workflow then signs the Windows installer and app through eSigner (a small
signing script, since electron-builder has no built-in eSigner support).

An EV certificate costs more and no longer gives instant SmartScreen trust (Microsoft changed this in 2024): with OV,
warnings fade as people download and run each release. DigiCert (KeyLocker) is the premium alternative.

## Linux

Nothing to buy. AppImage and .deb don't need signing to run; we can publish checksums with each release.
23 changes: 5 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,14 @@
"dmg",
"zip"
],
"icon": "electron/icon.icns",
"files": [
"!node_modules/onnxruntime-node/bin/napi-v*/linux/**",
"!node_modules/onnxruntime-node/bin/napi-v*/win32/**"
]
"icon": "electron/icon.icns"
},
"win": {
"target": [
"nsis",
"zip"
],
"icon": "electron/icon.png",
"files": [
"!node_modules/onnxruntime-node/bin/napi-v*/linux/**",
"!node_modules/onnxruntime-node/bin/napi-v*/darwin/**",
"!node_modules/onnxruntime-node/bin/napi-v*/win32/arm64/**"
]
"icon": "electron/icon.png"
},
"linux": {
"target": [
Expand All @@ -87,12 +78,7 @@
],
"category": "Office",
"icon": "electron/icon.png",
"executableName": "duct",
"files": [
"!node_modules/onnxruntime-node/bin/napi-v*/darwin/**",
"!node_modules/onnxruntime-node/bin/napi-v*/win32/**",
"!node_modules/onnxruntime-node/bin/napi-v*/linux/arm64/**"
]
"executableName": "duct"
},
"deb": {
"packageName": "duct",
Expand All @@ -105,7 +91,8 @@
"assets/ocr/**",
"node_modules/ffmpeg-static/**",
"node_modules/onnxruntime-node/**"
]
],
"afterPack": "scripts/after-pack.cjs"
},
"dependencies": {
"@huggingface/transformers": "^4.3.1",
Expand Down
28 changes: 28 additions & 0 deletions scripts/after-pack.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// electron-builder afterPack: keep only this platform's ONNX Runtime binaries (the speech engine for audio search).
// onnxruntime-node ships every platform's (about 300 MB); file patterns can't pick by platform, so the others are
// deleted from the packed app here.
const { existsSync, readdirSync, rmSync } = require('node:fs')
const { join } = require('node:path')
const { Arch } = require('builder-util')

exports.default = async function afterPack(context) {
const platform = context.electronPlatformName // darwin, linux, win32
const arch = Arch[context.arch] // x64, arm64…
const resources = platform === 'darwin'
? join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents', 'Resources')
: join(context.appOutDir, 'resources')
const bin = join(resources, 'app.asar.unpacked', 'node_modules', 'onnxruntime-node', 'bin')
if (!existsSync(bin)) throw new Error(`ONNX Runtime binaries are missing from the packed app (${bin})`)
let kept = 0
for (const napi of readdirSync(bin)) {
for (const os of readdirSync(join(bin, napi))) {
for (const cpu of readdirSync(join(bin, napi, os))) {
if (os === platform && cpu === arch) kept++
else rmSync(join(bin, napi, os, cpu), { recursive: true, force: true })
}
if (!readdirSync(join(bin, napi, os)).length) rmSync(join(bin, napi, os), { recursive: true, force: true })
}
}
// Intel Macs have no ONNX Runtime build; audio search says so there (src/speech.ts).
if (!kept && !(platform === 'darwin' && arch === 'x64')) throw new Error(`No ONNX Runtime binaries for ${platform}-${arch}`)
}
Loading