@hasna/recordings 0.2.10 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/cli/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/storage.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/scripts/install_macos_app.sh +70 -46
- package/src/native/Recordings/build.sh +227 -3
package/README.md
CHANGED
|
@@ -11,6 +11,41 @@ Speech-to-text recording tool with MCP and CLI — records, transcribes, and opt
|
|
|
11
11
|
npm install -g @hasna/recordings
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## macOS app, code signing & permissions
|
|
15
|
+
|
|
16
|
+
On macOS the package builds a native `Recordings.app` (into
|
|
17
|
+
`~/.hasna/recordings/Recordings.app`) during `postinstall` and requires
|
|
18
|
+
Microphone and Accessibility grants. Those grants are bound by macOS (TCC) to
|
|
19
|
+
the app's **code-signing designated requirement**, so signing has to stay stable
|
|
20
|
+
across updates or macOS re-prompts on every install.
|
|
21
|
+
|
|
22
|
+
- **Stable signing by default — no configuration needed.** If
|
|
23
|
+
`RECORDINGS_CODESIGN_IDENTITY` is set (e.g. a Developer ID or a station signing
|
|
24
|
+
certificate), it is used. Otherwise the build creates, once, a per-machine
|
|
25
|
+
self-signed code-signing certificate named **"Hasna Recordings Signing"** and
|
|
26
|
+
reuses it for every subsequent build. Because the same certificate is reused,
|
|
27
|
+
the designated requirement is certificate-based and constant across rebuilds,
|
|
28
|
+
so your Microphone/Accessibility grants survive updates. The build never falls
|
|
29
|
+
back to ad-hoc signing (which would change the requirement on every rebuild and
|
|
30
|
+
force re-authorization).
|
|
31
|
+
- **Works headless (over SSH).** The signing identity lives in a **dedicated**
|
|
32
|
+
keychain (`~/.hasna/recordings/signing/recordings-signing.keychain-db`) whose
|
|
33
|
+
password is generated once and stored in the secrets vault
|
|
34
|
+
(`hasna/machine/<host>/recordings/signing/keychain_password`). The build
|
|
35
|
+
unlocks that keychain with the known password and runs
|
|
36
|
+
`security set-key-partition-list -S apple-tool:,apple:` so `codesign` gets
|
|
37
|
+
non-interactive key access with no prompt — it never depends on the login
|
|
38
|
+
keychain being unlocked. Requires the `secrets` vault CLI on the machine.
|
|
39
|
+
- **Fails closed without entitlements.** If `RecordingsLib/Recordings.entitlements`
|
|
40
|
+
is missing the build aborts rather than shipping an unsigned/unentitled app.
|
|
41
|
+
- **Rebuild only on real changes.** Reinstalling the same version does not
|
|
42
|
+
rebuild the app: the installer compares a hash of the native sources plus the
|
|
43
|
+
package version (recorded at `~/.hasna/recordings/.recordings-source-hash`) and
|
|
44
|
+
skips the rebuild when nothing changed, leaving the already-granted app
|
|
45
|
+
untouched. A genuine update rebuilds and re-signs with the same certificate, so
|
|
46
|
+
grants still survive. The installer never resets TCC permissions.
|
|
47
|
+
- **Force a rebuild** with `RECORDINGS_FORCE_APP_REINSTALL=1 recordings app install`.
|
|
48
|
+
|
|
14
49
|
## CLI Usage
|
|
15
50
|
|
|
16
51
|
```bash
|
package/dist/cli/index.js
CHANGED
package/dist/index.js
CHANGED
package/dist/mcp/index.js
CHANGED
package/dist/server/index.js
CHANGED
package/dist/storage.js
CHANGED
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.11";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -52,6 +52,67 @@ warn_or_fail() {
|
|
|
52
52
|
exit 1
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
# Deterministic, source-freshness-based rebuild skip.
|
|
56
|
+
#
|
|
57
|
+
# We rebuild only when the app SOURCES or the package VERSION actually changed.
|
|
58
|
+
# The decision is a hash of the native app source tree plus the package version
|
|
59
|
+
# — NOT the app's signature. This is deliberate:
|
|
60
|
+
# * Identical reinstall (same source/version): keep the already-installed,
|
|
61
|
+
# already-granted app untouched, so macOS permission grants survive.
|
|
62
|
+
# * Genuine app update (source or version changed): rebuild and re-sign with
|
|
63
|
+
# the same stable per-machine certificate. Its certificate-based designated
|
|
64
|
+
# requirement is unchanged, so the grants survive the update too — no
|
|
65
|
+
# update-starvation and no force flag needed.
|
|
66
|
+
STAMP_FILE="${DATA_DIR}/.recordings-source-hash"
|
|
67
|
+
|
|
68
|
+
# Keep the ~/Applications launch point pointed at the current build and restart
|
|
69
|
+
# a running instance. A second, stale copy in ~/Applications splits TCC
|
|
70
|
+
# permissions and leaves users on an old build, so we refresh it from the
|
|
71
|
+
# canonical app on EVERY path — including the skip-rebuild path, where the
|
|
72
|
+
# canonical app is the already-installed APP_DEST.
|
|
73
|
+
refresh_alt_copy_and_restart() {
|
|
74
|
+
local source_app="$1"
|
|
75
|
+
[ -d "$source_app" ] || return 0
|
|
76
|
+
|
|
77
|
+
local alt_copy="${HOME}/Applications/Recordings.app"
|
|
78
|
+
if [ -d "$alt_copy" ]; then
|
|
79
|
+
rm -rf "$alt_copy"
|
|
80
|
+
cp -R "$source_app" "$alt_copy" || true
|
|
81
|
+
echo "Updated stale copy at ${alt_copy} to the current app."
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# If an old instance is running, restart it on the current build.
|
|
85
|
+
if pgrep -x Recordings >/dev/null 2>&1; then
|
|
86
|
+
pkill -x Recordings || true
|
|
87
|
+
sleep 1
|
|
88
|
+
open "$APP_DEST" || true
|
|
89
|
+
echo "Restarted Recordings.app on the current build."
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
compute_source_hash() {
|
|
94
|
+
command -v shasum >/dev/null 2>&1 || return 1
|
|
95
|
+
[ -d "$NATIVE_DIR" ] || return 1
|
|
96
|
+
{
|
|
97
|
+
# Version-aware: the package version line participates in the hash.
|
|
98
|
+
grep '"version"' "${PACKAGE_ROOT}/package.json" 2>/dev/null
|
|
99
|
+
# Source-aware: hash every native source file (relative paths, so the hash
|
|
100
|
+
# is independent of the install location), excluding build artifacts.
|
|
101
|
+
( cd "$NATIVE_DIR" && find . -type f ! -path './.build/*' -exec shasum {} + 2>/dev/null | sort )
|
|
102
|
+
} | shasum | awk '{print $1}'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
SOURCE_HASH="$(compute_source_hash)"
|
|
106
|
+
|
|
107
|
+
if [ -d "$APP_DEST" ] && [ "${RECORDINGS_FORCE_APP_REINSTALL:-0}" != "1" ] && \
|
|
108
|
+
[ -n "$SOURCE_HASH" ] && [ -f "$STAMP_FILE" ] && \
|
|
109
|
+
[ "$(cat "$STAMP_FILE" 2>/dev/null)" = "$SOURCE_HASH" ]; then
|
|
110
|
+
echo "Recordings.app at ${APP_DEST} is already built from the current sources; skipping rebuild to preserve macOS permission grants."
|
|
111
|
+
echo "Set RECORDINGS_FORCE_APP_REINSTALL=1 to force a rebuild."
|
|
112
|
+
refresh_alt_copy_and_restart "$APP_DEST"
|
|
113
|
+
exit 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
55
116
|
if ! command -v swift >/dev/null 2>&1; then
|
|
56
117
|
warn_or_fail "Swift toolchain not found"
|
|
57
118
|
fi
|
|
@@ -73,53 +134,16 @@ rm -rf "$APP_DEST"
|
|
|
73
134
|
mkdir -p "$DATA_DIR"
|
|
74
135
|
cp -R "$APP_SOURCE" "$APP_DEST" || warn_or_fail "failed to copy app bundle"
|
|
75
136
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
tcc_csreq_hex() {
|
|
81
|
-
local db_path="$1"
|
|
82
|
-
local service="$2"
|
|
83
|
-
if [ ! -r "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
|
|
84
|
-
return 0
|
|
85
|
-
fi
|
|
86
|
-
sqlite3 "$db_path" \
|
|
87
|
-
"SELECT hex(csreq) FROM access WHERE service = '${service}' AND client = 'com.hasna.recordings' ORDER BY last_modified DESC LIMIT 1;" \
|
|
88
|
-
2>/dev/null || true
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
reset_stale_permission() {
|
|
92
|
-
local service="$1"
|
|
93
|
-
local tcc_service="$2"
|
|
94
|
-
local db_path="$3"
|
|
95
|
-
local cdhash="$4"
|
|
96
|
-
local csreq_hex
|
|
97
|
-
csreq_hex="$(tcc_csreq_hex "$db_path" "$tcc_service" | tr '[:lower:]' '[:upper:]')"
|
|
98
|
-
if [ -n "$cdhash" ] && [ -n "$csreq_hex" ] && [[ "$csreq_hex" != *"$cdhash"* ]]; then
|
|
99
|
-
tccutil reset "$service" com.hasna.recordings >/dev/null 2>&1 || true
|
|
100
|
-
echo "Reset stale ${service} permission for the newly installed Recordings.app."
|
|
101
|
-
fi
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
APP_CDHASH="$(current_cdhash "$APP_DEST" || true)"
|
|
105
|
-
reset_stale_permission "Microphone" "kTCCServiceMicrophone" "${HOME}/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
|
|
106
|
-
reset_stale_permission "Accessibility" "kTCCServiceAccessibility" "/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
|
|
107
|
-
|
|
108
|
-
# A second copy in ~/Applications splits TCC permissions and leaves users running
|
|
109
|
-
# stale builds. Keep that launch point but always point it at the fresh build.
|
|
110
|
-
ALT_COPY="${HOME}/Applications/Recordings.app"
|
|
111
|
-
if [ -d "$ALT_COPY" ]; then
|
|
112
|
-
rm -rf "$ALT_COPY"
|
|
113
|
-
cp -R "$APP_SOURCE" "$ALT_COPY" || true
|
|
114
|
-
echo "Updated stale copy at ${ALT_COPY} to the freshly built app."
|
|
137
|
+
# Record the source/version hash this build was produced from so an identical
|
|
138
|
+
# future reinstall is skipped (see the rebuild-skip guard above).
|
|
139
|
+
if [ -n "$SOURCE_HASH" ]; then
|
|
140
|
+
printf '%s\n' "$SOURCE_HASH" > "$STAMP_FILE"
|
|
115
141
|
fi
|
|
116
142
|
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
echo "Restarted Recordings.app on the new build."
|
|
123
|
-
fi
|
|
143
|
+
# NOTE: this installer must never modify TCC permission state. Automatically
|
|
144
|
+
# resetting "stale" grants deleted the user's Microphone/Accessibility
|
|
145
|
+
# approvals on every update; users must always keep their existing decisions.
|
|
146
|
+
|
|
147
|
+
refresh_alt_copy_and_restart "$APP_DEST"
|
|
124
148
|
|
|
125
149
|
echo "Installed Recordings.app from package: ${APP_DEST}"
|
|
@@ -8,6 +8,200 @@ MODE="${1:-release}"
|
|
|
8
8
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
9
|
cd "$SCRIPT_DIR"
|
|
10
10
|
|
|
11
|
+
# A STABLE certificate identity keeps the app's TCC designated requirement
|
|
12
|
+
# constant across rebuilds so macOS permission grants (Microphone /
|
|
13
|
+
# Accessibility) persist. Ad-hoc ("-") pins the identity to this exact binary's
|
|
14
|
+
# CDHash and forces re-authorization after every build, so we NEVER default to
|
|
15
|
+
# it. When no explicit identity is provided we create (once) and reuse a
|
|
16
|
+
# per-machine self-signed code-signing certificate held in a DEDICATED signing
|
|
17
|
+
# keychain, so the default postinstall path is self-contained AND works over a
|
|
18
|
+
# headless SSH session — no out-of-band env and no unlocked login keychain
|
|
19
|
+
# required.
|
|
20
|
+
SIGNING_CN="Hasna Recordings Signing"
|
|
21
|
+
SIGNING_DIR="${HOME}/.hasna/recordings/signing"
|
|
22
|
+
# Dedicated code-signing keychain. We NEVER touch the login keychain: over SSH
|
|
23
|
+
# the login keychain is locked and codesign would block on an interactive
|
|
24
|
+
# unlock/allow prompt. A dedicated keychain with a KNOWN password (stored in the
|
|
25
|
+
# vault) can be created, unlocked, and key-partition-listed non-interactively.
|
|
26
|
+
SIGNING_KEYCHAIN="${SIGNING_DIR}/recordings-signing.keychain-db"
|
|
27
|
+
|
|
28
|
+
# Resolve the macOS keychain/codesign tools by ABSOLUTE path when present, so a
|
|
29
|
+
# same-named shim earlier on PATH (e.g. the Hasna `security` CLI installed in
|
|
30
|
+
# ~/.bun/bin) cannot shadow the real /usr/bin/security. On hosts without those
|
|
31
|
+
# absolute paths (e.g. the Linux unit-test sandbox) we fall back to PATH so the
|
|
32
|
+
# tests can stub them.
|
|
33
|
+
SECURITY="/usr/bin/security"; [ -x "$SECURITY" ] || SECURITY="security"
|
|
34
|
+
CODESIGN="/usr/bin/codesign"; [ -x "$CODESIGN" ] || CODESIGN="codesign"
|
|
35
|
+
# SHA-1 hash of the signing identity, resolved by ensure_local_signing_identity.
|
|
36
|
+
SIGNING_IDENTITY=""
|
|
37
|
+
|
|
38
|
+
# Vault keys are host-scoped so each machine owns its own signing material.
|
|
39
|
+
signing_host() {
|
|
40
|
+
hostname -s 2>/dev/null || hostname 2>/dev/null || echo unknown
|
|
41
|
+
}
|
|
42
|
+
vault_prefix() {
|
|
43
|
+
printf 'hasna/machine/%s/recordings/signing' "$(signing_host)"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Append the dedicated keychain to the USER search list (never replacing it) so
|
|
47
|
+
# codesign / find-identity can see the identity while leaving login and other
|
|
48
|
+
# keychains intact.
|
|
49
|
+
add_signing_keychain_to_search_list() {
|
|
50
|
+
local target="$SIGNING_KEYCHAIN"
|
|
51
|
+
local -a current=()
|
|
52
|
+
local line trimmed
|
|
53
|
+
while IFS= read -r line; do
|
|
54
|
+
# `security list-keychains` prints each path indented and quoted.
|
|
55
|
+
trimmed="${line#"${line%%[![:space:]]*}"}" # strip leading whitespace
|
|
56
|
+
trimmed="${trimmed%\"}"; trimmed="${trimmed#\"}"
|
|
57
|
+
[ -n "$trimmed" ] || continue
|
|
58
|
+
[ "$trimmed" = "$target" ] && return 0 # already in the list
|
|
59
|
+
current+=("$trimmed")
|
|
60
|
+
done < <("$SECURITY" list-keychains -d user 2>/dev/null)
|
|
61
|
+
"$SECURITY" list-keychains -d user -s "$target" ${current[@]+"${current[@]}"} >/dev/null 2>&1 \
|
|
62
|
+
|| echo "WARN: could not add $target to the keychain search list" >&2
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Create/reuse the "Hasna Recordings Signing" identity inside the DEDICATED
|
|
66
|
+
# signing keychain, unlocking it non-interactively with the known vault-stored
|
|
67
|
+
# password so codesign can sign over SSH without any prompt. Reusing the same
|
|
68
|
+
# certificate is what makes the designated requirement (certificate-based)
|
|
69
|
+
# stable across rebuilds so TCC grants survive. Returns 0 when usable.
|
|
70
|
+
ensure_local_signing_identity() {
|
|
71
|
+
command -v "$SECURITY" >/dev/null 2>&1 || { echo "ERROR: 'security' not found" >&2; return 1; }
|
|
72
|
+
command -v openssl >/dev/null 2>&1 || { echo "ERROR: 'openssl' not found" >&2; return 1; }
|
|
73
|
+
# The dedicated keychain's password lives in the vault; without the vault CLI
|
|
74
|
+
# we cannot manage it non-interactively, so fail closed rather than fall back
|
|
75
|
+
# to the (headless-hostile) login keychain.
|
|
76
|
+
command -v secrets >/dev/null 2>&1 || { echo "ERROR: 'secrets' vault CLI not found; cannot manage the dedicated signing keychain password" >&2; return 1; }
|
|
77
|
+
|
|
78
|
+
local prefix pw_key cert_key key_key
|
|
79
|
+
prefix="$(vault_prefix)"
|
|
80
|
+
pw_key="${prefix}/keychain_password"
|
|
81
|
+
cert_key="${prefix}/certificate_pem_b64"
|
|
82
|
+
key_key="${prefix}/private_key_pem_b64"
|
|
83
|
+
|
|
84
|
+
mkdir -p "$SIGNING_DIR" && chmod 700 "$SIGNING_DIR" 2>/dev/null || true
|
|
85
|
+
|
|
86
|
+
# 1. Known keychain password: generate once, persist in the vault, reuse.
|
|
87
|
+
local kc_pw
|
|
88
|
+
kc_pw="$(secrets get "$pw_key" 2>/dev/null || true)"
|
|
89
|
+
if [ -z "$kc_pw" ]; then
|
|
90
|
+
kc_pw="$(openssl rand -base64 24 | tr -d '\n')"
|
|
91
|
+
if ! secrets set "$pw_key" "$kc_pw" --type password \
|
|
92
|
+
--label "Recordings signing keychain password ($(signing_host))" >/dev/null 2>&1; then
|
|
93
|
+
echo "ERROR: failed to store signing keychain password in the vault ($pw_key)" >&2
|
|
94
|
+
return 1
|
|
95
|
+
fi
|
|
96
|
+
echo "Generated dedicated signing keychain password (stored in vault: $pw_key)."
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# 2. Create the dedicated keychain if missing; ALWAYS unlock it with the
|
|
100
|
+
# known password. Disable auto-lock so the key stays reachable headless.
|
|
101
|
+
if [ ! -f "$SIGNING_KEYCHAIN" ]; then
|
|
102
|
+
echo "Creating dedicated code-signing keychain: $SIGNING_KEYCHAIN"
|
|
103
|
+
if ! "$SECURITY" create-keychain -p "$kc_pw" "$SIGNING_KEYCHAIN" >/dev/null 2>&1; then
|
|
104
|
+
echo "ERROR: failed to create dedicated signing keychain" >&2; return 1
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
if ! "$SECURITY" unlock-keychain -p "$kc_pw" "$SIGNING_KEYCHAIN" >/dev/null 2>&1; then
|
|
108
|
+
echo "ERROR: failed to unlock dedicated signing keychain (stale vault password?)" >&2; return 1
|
|
109
|
+
fi
|
|
110
|
+
# No timeout / no lock-on-sleep: keep it usable across a headless build.
|
|
111
|
+
"$SECURITY" set-keychain-settings "$SIGNING_KEYCHAIN" >/dev/null 2>&1 || true
|
|
112
|
+
|
|
113
|
+
# 3. Make the dedicated keychain discoverable to codesign/find-identity.
|
|
114
|
+
add_signing_keychain_to_search_list
|
|
115
|
+
|
|
116
|
+
# 4. Reuse the identity if it is already present in the dedicated keychain.
|
|
117
|
+
# NOTE: no -v (valid-only) filter. A self-signed identity is untrusted
|
|
118
|
+
# (CSSMERR_TP_NOT_TRUSTED) — and marking it trusted needs GUI auth that is
|
|
119
|
+
# unavailable over SSH — so it is invisible to `find-identity -v`. codesign
|
|
120
|
+
# can nonetheless sign with it, so we locate it by its SHA-1 HASH (which is
|
|
121
|
+
# also what we pass to `codesign --sign`, decoupling signing from the
|
|
122
|
+
# certificate's common name).
|
|
123
|
+
SIGNING_IDENTITY="$("$SECURITY" find-identity -p codesigning "$SIGNING_KEYCHAIN" 2>/dev/null | grep -oE '[0-9A-F]{40}' | head -1)"
|
|
124
|
+
if [ -n "$SIGNING_IDENTITY" ]; then
|
|
125
|
+
# Re-assert non-interactive key access with the KNOWN password.
|
|
126
|
+
"$SECURITY" set-key-partition-list -S apple-tool:,apple: -k "$kc_pw" "$SIGNING_KEYCHAIN" >/dev/null 2>&1 || true
|
|
127
|
+
return 0
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# 5. Materialize cert+key: reuse the vault copy if present (keeps the DR — and
|
|
131
|
+
# the TCC grants bound to it — stable even if the keychain is recreated),
|
|
132
|
+
# otherwise mint a new self-signed identity and persist it to the vault.
|
|
133
|
+
local work key crt p12 vault_cert_b64 vault_key_b64
|
|
134
|
+
work="$(mktemp -d)" || return 1
|
|
135
|
+
key="$work/key.pem"; crt="$work/cert.pem"; p12="$work/identity.p12"
|
|
136
|
+
|
|
137
|
+
vault_cert_b64="$(secrets get "$cert_key" 2>/dev/null || true)"
|
|
138
|
+
vault_key_b64="$(secrets get "$key_key" 2>/dev/null || true)"
|
|
139
|
+
if [ -n "$vault_cert_b64" ] && [ -n "$vault_key_b64" ]; then
|
|
140
|
+
printf '%s' "$vault_cert_b64" | openssl base64 -d -A > "$crt" 2>/dev/null || true
|
|
141
|
+
printf '%s' "$vault_key_b64" | openssl base64 -d -A > "$key" 2>/dev/null || true
|
|
142
|
+
[ -s "$crt" ] && [ -s "$key" ] && echo "Reusing signing certificate from vault ($cert_key)."
|
|
143
|
+
fi
|
|
144
|
+
if [ ! -s "$crt" ] || [ ! -s "$key" ]; then
|
|
145
|
+
echo "Minting new self-signed code-signing certificate \"$SIGNING_CN\" (one-time)..."
|
|
146
|
+
cat > "$work/req.cnf" <<EOF
|
|
147
|
+
[ req ]
|
|
148
|
+
distinguished_name = dn
|
|
149
|
+
x509_extensions = ext
|
|
150
|
+
prompt = no
|
|
151
|
+
[ dn ]
|
|
152
|
+
CN = $SIGNING_CN
|
|
153
|
+
[ ext ]
|
|
154
|
+
basicConstraints = critical,CA:false
|
|
155
|
+
keyUsage = critical,digitalSignature
|
|
156
|
+
extendedKeyUsage = critical,codeSigning
|
|
157
|
+
EOF
|
|
158
|
+
if ! openssl req -x509 -newkey rsa:2048 -nodes -keyout "$key" -out "$crt" \
|
|
159
|
+
-days 3650 -config "$work/req.cnf" >/dev/null 2>&1; then
|
|
160
|
+
rm -rf "$work"; echo "ERROR: failed to generate self-signed signing certificate" >&2; return 1
|
|
161
|
+
fi
|
|
162
|
+
# Persist so future rebuilds — even after keychain re-creation or machine
|
|
163
|
+
# re-provisioning — reuse the SAME identity and keep the DR stable.
|
|
164
|
+
secrets set "$cert_key" "$(openssl base64 -A -in "$crt")" --type certificate \
|
|
165
|
+
--label "Recordings signing cert ($(signing_host))" >/dev/null 2>&1 \
|
|
166
|
+
|| echo "WARN: could not persist signing certificate to vault ($cert_key)" >&2
|
|
167
|
+
secrets set "$key_key" "$(openssl base64 -A -in "$key")" --type private_key \
|
|
168
|
+
--label "Recordings signing key ($(signing_host))" >/dev/null 2>&1 \
|
|
169
|
+
|| echo "WARN: could not persist signing key to vault ($key_key)" >&2
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Package as PKCS#12 for `security import`. macOS's Security framework cannot
|
|
173
|
+
# read the PBKDF2/AES encoding OpenSSL 3 emits by default, so prefer -legacy
|
|
174
|
+
# (RC2/3DES + SHA1 MAC) and fall back for OpenSSL builds without it. Use a
|
|
175
|
+
# non-empty transport passphrase (the keychain password): an empty p12
|
|
176
|
+
# password trips "MAC verification failed" on macOS import.
|
|
177
|
+
if ! openssl pkcs12 -export -legacy -inkey "$key" -in "$crt" -name "$SIGNING_CN" \
|
|
178
|
+
-out "$p12" -passout pass:"$kc_pw" >/dev/null 2>&1; then
|
|
179
|
+
if ! openssl pkcs12 -export -inkey "$key" -in "$crt" -name "$SIGNING_CN" \
|
|
180
|
+
-out "$p12" -passout pass:"$kc_pw" >/dev/null 2>&1; then
|
|
181
|
+
rm -rf "$work"; echo "ERROR: failed to package signing identity as PKCS#12" >&2; return 1
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
184
|
+
# Import into the DEDICATED keychain (never the login keychain), allowlisting
|
|
185
|
+
# codesign so it may use the private key.
|
|
186
|
+
if ! "$SECURITY" import "$p12" -k "$SIGNING_KEYCHAIN" -P "$kc_pw" -T /usr/bin/codesign >/dev/null 2>&1; then
|
|
187
|
+
rm -rf "$work"; echo "ERROR: failed to import signing identity into $SIGNING_KEYCHAIN" >&2; return 1
|
|
188
|
+
fi
|
|
189
|
+
# Trust the self-signed root for code signing so `codesign --verify` passes.
|
|
190
|
+
"$SECURITY" add-trusted-cert -r trustRoot -p codeSign -k "$SIGNING_KEYCHAIN" "$crt" >/dev/null 2>&1 || true
|
|
191
|
+
# THE headless fix: grant codesign non-interactive access to the imported key
|
|
192
|
+
# using the KNOWN keychain password (never an empty password, never the login
|
|
193
|
+
# keychain), so signing works over SSH with no interactive allow prompt.
|
|
194
|
+
if ! "$SECURITY" set-key-partition-list -S apple-tool:,apple: -k "$kc_pw" "$SIGNING_KEYCHAIN" >/dev/null 2>&1; then
|
|
195
|
+
echo "WARN: set-key-partition-list did not complete; codesign may prompt for key access" >&2
|
|
196
|
+
fi
|
|
197
|
+
rm -rf "$work"
|
|
198
|
+
|
|
199
|
+
# Locate the freshly imported identity by hash (again, no -v: it is an
|
|
200
|
+
# untrusted self-signed cert). codesign signs by this hash.
|
|
201
|
+
SIGNING_IDENTITY="$("$SECURITY" find-identity -p codesigning "$SIGNING_KEYCHAIN" 2>/dev/null | grep -oE '[0-9A-F]{40}' | head -1)"
|
|
202
|
+
[ -n "$SIGNING_IDENTITY" ]
|
|
203
|
+
}
|
|
204
|
+
|
|
11
205
|
echo "Building Recordings.app ($MODE)..."
|
|
12
206
|
swift build -c "$MODE" --product App
|
|
13
207
|
|
|
@@ -34,9 +228,39 @@ for bundle in "$BUILD_DIR"/*.resources "$BUILD_DIR"/*.bundle .build/*/"$MODE"/*.
|
|
|
34
228
|
ditto "$bundle" "$RESOURCES/$(basename "$bundle")"
|
|
35
229
|
done
|
|
36
230
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
231
|
+
# FAIL CLOSED on missing entitlements. Without the entitlements file the app
|
|
232
|
+
# would be signed with no Microphone/Accessibility entitlements (or skipped and
|
|
233
|
+
# left effectively unsigned) — a silent, safety-critical regression. Refuse to
|
|
234
|
+
# build instead.
|
|
235
|
+
if [ ! -f RecordingsLib/Recordings.entitlements ]; then
|
|
236
|
+
echo "ERROR: RecordingsLib/Recordings.entitlements is missing." >&2
|
|
237
|
+
echo "Refusing to build: the app must be signed with its entitlements;" >&2
|
|
238
|
+
echo "skipping signing would ship an unsigned/unentitled Recordings.app." >&2
|
|
239
|
+
exit 1
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# Sign with entitlements using a STABLE identity. A signing failure must fail
|
|
243
|
+
# the build: silently falling back to ad-hoc would reintroduce identity churn
|
|
244
|
+
# and invalidate existing TCC grants.
|
|
245
|
+
#
|
|
246
|
+
# Precedence:
|
|
247
|
+
# 1. RECORDINGS_CODESIGN_IDENTITY — explicit identity (Developer ID, station
|
|
248
|
+
# signing cert, or "-" for an intentional ad-hoc build). An explicit
|
|
249
|
+
# identity that fails to sign fails the build.
|
|
250
|
+
# 2. The per-machine "Hasna Recordings Signing" self-signed certificate in the
|
|
251
|
+
# DEDICATED signing keychain, created once and reused, so the default build
|
|
252
|
+
# is stably signed non-interactively (works over SSH) with no out-of-band
|
|
253
|
+
# configuration. We never fall back to ad-hoc by default.
|
|
254
|
+
if [ -n "${RECORDINGS_CODESIGN_IDENTITY:-}" ]; then
|
|
255
|
+
"$CODESIGN" --force --sign "$RECORDINGS_CODESIGN_IDENTITY" --entitlements RecordingsLib/Recordings.entitlements "$APP_DIR"
|
|
256
|
+
elif ensure_local_signing_identity; then
|
|
257
|
+
"$CODESIGN" --force --keychain "$SIGNING_KEYCHAIN" --sign "$SIGNING_IDENTITY" --entitlements RecordingsLib/Recordings.entitlements "$APP_DIR"
|
|
258
|
+
else
|
|
259
|
+
echo "ERROR: no stable code-signing identity is available and refusing to" >&2
|
|
260
|
+
echo "ad-hoc sign: ad-hoc signatures change the app's designated requirement" >&2
|
|
261
|
+
echo "on every build and break macOS Microphone/Accessibility grants." >&2
|
|
262
|
+
echo "Set RECORDINGS_CODESIGN_IDENTITY to a valid signing identity and retry." >&2
|
|
263
|
+
exit 1
|
|
40
264
|
fi
|
|
41
265
|
|
|
42
266
|
echo "✓ Built $APP_DIR"
|