Comparar commits
1 Commits
| Autor | SHA1 | Fecha | |
|---|---|---|---|
|
|
a37a0bcc0b |
86
.github/workflows/build.yml
vendido
86
.github/workflows/build.yml
vendido
@@ -54,6 +54,62 @@ jobs:
|
|||||||
- name: Probar el cliente
|
- name: Probar el cliente
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
|
# ── FIRMA DE macOS ──────────────────────────────────────────────────────────────────
|
||||||
|
# Hasta el 2026-09-19 este build NO firmaba la app de macOS: no se le pasaba ninguna
|
||||||
|
# identidad, así que el bundler de Tauri se saltaba `codesign` entero y el .dmg salía con
|
||||||
|
# la firma ad hoc que deja el enlazador. Se veía así en la app instalada:
|
||||||
|
# flags=0x20002(adhoc,linker-signed) · Info.plist=not bound · Sealed Resources=none
|
||||||
|
# Consecuencias: los entitlements (micrófono, cámara, JIT) NO se aplicaban —el
|
||||||
|
# Entitlements.plist del repo era decoración—, el Info.plist y los recursos no iban
|
||||||
|
# sellados, y la app no se podía notarizar de ninguna manera.
|
||||||
|
#
|
||||||
|
# `APPLE_SIGNING_IDENTITY` decide qué pasa, y por eso se calcula aquí y no en el `env:`
|
||||||
|
# del paso de abajo: un secreto que no existe llega como cadena VACÍA, y Tauri
|
||||||
|
# interpreta `APPLE_CERTIFICATE=""` como «hay certificado» y revienta al importarlo.
|
||||||
|
# Solo se exporta lo que de verdad tiene valor.
|
||||||
|
#
|
||||||
|
# · CON los secretos de Developer ID → firma real + notarizado + grapado.
|
||||||
|
# · SIN ellos → identidad "-", que es la firma AD HOC: no engaña a Gatekeeper (habrá
|
||||||
|
# que seguir abriendo con clic derecho), pero SÍ sella los recursos, ata el
|
||||||
|
# Info.plist y aplica los entitlements. Estrictamente mejor que no firmar.
|
||||||
|
#
|
||||||
|
# El hardened runtime no se configura: en Tauri v2 `bundle.macOS.hardenedRuntime` ya
|
||||||
|
# viene en true, y es lo que hace que los entitlements signifiquen algo.
|
||||||
|
- name: Preparar la firma de macOS
|
||||||
|
if: matrix.platform == 'macos-latest'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CERT: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
CERT_PW: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
NOTARIZA_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
NOTARIZA_PW: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
NOTARIZA_TEAM: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Los valores van por heredoc con delimitador: un base64 largo o con saltos rompería
|
||||||
|
# la forma `CLAVE=valor`. Nada se imprime: los secretos no pasan por el log.
|
||||||
|
exportar() { printf '%s<<__FIN__\n%s\n__FIN__\n' "$1" "$2" >> "$GITHUB_ENV"; }
|
||||||
|
if [ -n "${CERT:-}" ] && [ -n "${CERT_PW:-}" ]; then
|
||||||
|
exportar APPLE_CERTIFICATE "$CERT"
|
||||||
|
exportar APPLE_CERTIFICATE_PASSWORD "$CERT_PW"
|
||||||
|
exportar APPLE_SIGNING_IDENTITY "${IDENTITY:-Developer ID Application}"
|
||||||
|
echo "Firma: Developer ID (certificado del repo)."
|
||||||
|
else
|
||||||
|
exportar APPLE_SIGNING_IDENTITY "-"
|
||||||
|
echo "Firma: AD HOC. Sella recursos y aplica entitlements, pero Gatekeeper seguirá avisando."
|
||||||
|
echo "Para firma de verdad, añade los secrets APPLE_CERTIFICATE y APPLE_CERTIFICATE_PASSWORD."
|
||||||
|
fi
|
||||||
|
# Notarizar solo tiene sentido con Developer ID; las tres claves van juntas o ninguna.
|
||||||
|
if [ -n "${CERT:-}" ] && [ -n "${NOTARIZA_ID:-}" ] && [ -n "${NOTARIZA_PW:-}" ] && [ -n "${NOTARIZA_TEAM:-}" ]; then
|
||||||
|
exportar APPLE_ID "$NOTARIZA_ID"
|
||||||
|
exportar APPLE_PASSWORD "$NOTARIZA_PW"
|
||||||
|
exportar APPLE_TEAM_ID "$NOTARIZA_TEAM"
|
||||||
|
echo "Notarizado: activado."
|
||||||
|
else
|
||||||
|
echo "Notarizado: omitido (faltan APPLE_ID, APPLE_PASSWORD o APPLE_TEAM_ID, o no hay certificado)."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Construir, firmar y publicar
|
- name: Construir, firmar y publicar
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
@@ -61,6 +117,7 @@ jobs:
|
|||||||
# Firma del updater: la clave privada y su contraseña van como secrets del repo.
|
# Firma del updater: la clave privada y su contraseña van como secrets del repo.
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
# Las APPLE_* las puso el paso de arriba en $GITHUB_ENV, solo si tenían valor.
|
||||||
with:
|
with:
|
||||||
# En un tag usa su nombre (v1.0.0); en ejecución manual, deriva vX.Y.Z de la versión.
|
# En un tag usa su nombre (v1.0.0); en ejecución manual, deriva vX.Y.Z de la versión.
|
||||||
tagName: ${{ github.ref_type == 'tag' && github.ref_name || 'v__VERSION__' }}
|
tagName: ${{ github.ref_type == 'tag' && github.ref_name || 'v__VERSION__' }}
|
||||||
@@ -70,6 +127,35 @@ jobs:
|
|||||||
prerelease: false
|
prerelease: false
|
||||||
args: ${{ matrix.args }}
|
args: ${{ matrix.args }}
|
||||||
|
|
||||||
|
# ── LA RED QUE FALTABA ──────────────────────────────────────────────────────────────
|
||||||
|
# Que el build no firmara la app de macOS no lo dijo nada durante meses: el .dmg salía,
|
||||||
|
# se instalaba y funcionaba. Se descubrió a mano, con `codesign -dv`, persiguiendo otra
|
||||||
|
# cosa. Este paso hace esa comprobación en cada publicación y TUMBA el build si los
|
||||||
|
# entitlements no llegaron: es barato y es el único aviso que habría existido.
|
||||||
|
- name: Comprobar la firma de macOS
|
||||||
|
if: matrix.platform == 'macos-latest'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
APP=$(find src-tauri/target/universal-apple-darwin/release/bundle/macos -maxdepth 1 -name "*.app" | head -1)
|
||||||
|
if [ -z "$APP" ]; then echo "::error::No apareció ningún .app donde se esperaba"; exit 1; fi
|
||||||
|
echo "== $APP =="
|
||||||
|
codesign -dv --verbose=4 "$APP" 2>&1 || true
|
||||||
|
echo "== entitlements =="
|
||||||
|
codesign -d --entitlements - --xml "$APP" 2>/dev/null | plutil -convert xml1 -o - - || true
|
||||||
|
echo "== integridad del bundle =="
|
||||||
|
codesign --verify --strict --verbose=2 "$APP"
|
||||||
|
# Gatekeeper: con firma ad hoc esto SIEMPRE dice «rejected», y es esperado. Se
|
||||||
|
# informa sin tumbar nada; lo que sí es obligatorio es lo de abajo.
|
||||||
|
echo "== Gatekeeper (informativo) =="
|
||||||
|
spctl -a -t exec -vv "$APP" 2>&1 || true
|
||||||
|
echo "== obligatorio: los entitlements tienen que estar =="
|
||||||
|
if ! codesign -d --entitlements - --xml "$APP" 2>/dev/null | grep -q "com.apple.security.cs.allow-jit"; then
|
||||||
|
echo "::error::La app salió SIN entitlements. Revisa el paso «Preparar la firma de macOS» y bundle.macOS.entitlements en tauri.conf.json."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Entitlements aplicados."
|
||||||
|
|
||||||
activar:
|
activar:
|
||||||
needs: publish
|
needs: publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
78
README.md
78
README.md
@@ -229,20 +229,72 @@ Es la forma de mirar un cambio visual —la barra, los colores— sin sacárselo
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Avisos de seguridad del sistema (instalación interna sin firmar)
|
## Firma de macOS
|
||||||
|
|
||||||
Como los instaladores **no están firmados** con certificado de pago, la primera
|
### Lo que pasaba hasta la 1.15.0
|
||||||
vez cada sistema muestra una advertencia. Es normal para apps internas:
|
|
||||||
|
|
||||||
- **Windows (SmartScreen):** pantalla azul *"Windows protegió su PC"* →
|
El build **no firmaba** la app de macOS. No se le pasaba ninguna identidad, así que
|
||||||
**Más información → Ejecutar de todos modos**.
|
el bundler de Tauri se saltaba `codesign` entero y el `.dmg` salía con la firma ad hoc
|
||||||
- **macOS (Gatekeeper):** *"no se puede abrir porque proviene de un desarrollador
|
que deja el enlazador. En la app instalada se veía así:
|
||||||
no identificado"* → clic derecho sobre la app → **Abrir** → **Abrir**. (Si
|
|
||||||
insiste, ejecutar una vez: `xattr -cr "/Applications/Labstream OS.app"`.)
|
|
||||||
|
|
||||||
Si en el futuro se distribuye a clientes externos, conviene firmar:
|
```
|
||||||
*code signing* en Windows (~150–400 USD/año) y un *Apple Developer ID* + notarizado
|
$ codesign -dv "/Applications/Labstream OS.app"
|
||||||
en Mac (99 USD/año) para quitar estos avisos.
|
Identifier=labstream_desktop-0a5819ba37119701
|
||||||
|
CodeDirectory ... flags=0x20002(adhoc,linker-signed)
|
||||||
|
Info.plist=not bound
|
||||||
|
Sealed Resources=none
|
||||||
|
TeamIdentifier=not set
|
||||||
|
```
|
||||||
|
|
||||||
|
Tres cosas mal, y ninguna avisaba:
|
||||||
|
|
||||||
|
- **`Entitlements.plist` no se aplicaba.** Los permisos de micrófono, cámara y JIT
|
||||||
|
eran decoración: se escriben al firmar, y no se firmaba.
|
||||||
|
- **`Info.plist=not bound` y `Sealed Resources=none`.** Ni el Info.plist ni los
|
||||||
|
recursos iban dentro de la firma: cualquiera podía cambiarlos sin romperla.
|
||||||
|
- **Identificador de enlazador** (`labstream_desktop-<hash>`) en vez del de la app
|
||||||
|
(`co.labstream.os`), que es por lo que macOS recuerda los permisos concedidos.
|
||||||
|
|
||||||
|
### Lo que hace ahora (desde la 1.16.0)
|
||||||
|
|
||||||
|
`build.yml` decide según los secrets que haya en el repo:
|
||||||
|
|
||||||
|
| Secrets | Resultado |
|
||||||
|
| --- | --- |
|
||||||
|
| ninguno | Firma **ad hoc** (`-s -`) con hardened runtime y los entitlements. Gatekeeper sigue avisando, pero el bundle va sellado y los permisos se aplican. |
|
||||||
|
| `APPLE_CERTIFICATE` + `APPLE_CERTIFICATE_PASSWORD` | Firma real con **Developer ID**. Gatekeeper deja de avisar en cualquier Mac. |
|
||||||
|
| …y además `APPLE_ID` + `APPLE_PASSWORD` + `APPLE_TEAM_ID` | Firma real + **notarizado y grapado**. Ni el primer arranque avisa. |
|
||||||
|
|
||||||
|
Y hay un paso que **tumba la publicación** si los entitlements no llegaron al `.app`.
|
||||||
|
Ese paso es la lección: que esto estuviera roto no lo dijo nada durante meses.
|
||||||
|
|
||||||
|
### Pasar a Developer ID (99 USD/año)
|
||||||
|
|
||||||
|
1. Alta en el Apple Developer Program.
|
||||||
|
2. En *Certificates* crear uno de tipo **Developer ID Application** y exportarlo del
|
||||||
|
Llavero como `.p12` con contraseña.
|
||||||
|
3. Convertirlo a texto: `base64 -i certificado.p12 | pbcopy`.
|
||||||
|
4. En GitHub → *Settings ▸ Secrets and variables ▸ Actions*, añadir:
|
||||||
|
- `APPLE_CERTIFICATE` — lo que quedó en el portapapeles.
|
||||||
|
- `APPLE_CERTIFICATE_PASSWORD` — la contraseña del `.p12`.
|
||||||
|
- `APPLE_SIGNING_IDENTITY` — opcional; por defecto `Developer ID Application`.
|
||||||
|
5. Para notarizar, además:
|
||||||
|
- `APPLE_ID` — el correo de la cuenta de desarrollador.
|
||||||
|
- `APPLE_PASSWORD` — una **contraseña específica de app** (appleid.apple.com, no la
|
||||||
|
del Apple ID).
|
||||||
|
- `APPLE_TEAM_ID` — el identificador de equipo de 10 caracteres.
|
||||||
|
|
||||||
|
No hay que tocar ni un archivo: en cuanto los secrets existan, la siguiente etiqueta
|
||||||
|
sale firmada.
|
||||||
|
|
||||||
|
### Mientras siga ad hoc
|
||||||
|
|
||||||
|
- **macOS (Gatekeeper):** *"no se puede abrir porque proviene de un desarrollador no
|
||||||
|
identificado"* → clic derecho sobre la app → **Abrir** → **Abrir**. (Si insiste:
|
||||||
|
`xattr -cr "/Applications/Labstream OS.app"`.)
|
||||||
|
- **Windows (SmartScreen):** *"Windows protegió su PC"* → **Más información →
|
||||||
|
Ejecutar de todos modos**. Windows es aparte: su firma cuesta 150–400 USD/año y no
|
||||||
|
la toca nada de lo de arriba.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -257,10 +309,10 @@ labstream-desktop/
|
|||||||
│ ├── tauri.conf.json nombre, versión, iconos, instaladores, entitlements
|
│ ├── tauri.conf.json nombre, versión, iconos, instaladores, entitlements
|
||||||
│ ├── Cargo.toml dependencias Rust + versión
|
│ ├── Cargo.toml dependencias Rust + versión
|
||||||
│ ├── Info.plist textos de permiso (micrófono/cámara) en macOS
|
│ ├── Info.plist textos de permiso (micrófono/cámara) en macOS
|
||||||
│ ├── Entitlements.plist entitlements de macOS (audio-input, cámara, red)
|
│ ├── Entitlements.plist entitlements de macOS (micrófono, cámara, JIT)
|
||||||
│ ├── build.rs
|
│ ├── build.rs
|
||||||
│ ├── capabilities/default.json
|
│ ├── capabilities/default.json
|
||||||
│ ├── icons/ se generan desde app-icon.png
|
│ ├── icons/ se generan desde app-icon.png
|
||||||
│ └── src/{main.rs,lib.rs} lib.rs crea la ventana + SERVER_URL + tray
|
│ └── src/{main.rs,lib.rs} lib.rs crea la ventana + SERVER_URL + tray
|
||||||
└── .github/workflows/build.yml build automático de .exe (Windows) y .dmg (macOS)
|
└── .github/workflows/build.yml build de .exe (Windows) y .dmg (macOS) + firma de macOS
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "labstream-desktop",
|
"name": "labstream-desktop",
|
||||||
"version": "1.15.0",
|
"version": "1.16.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.",
|
"description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generado
2
src-tauri/Cargo.lock
generado
@@ -2366,7 +2366,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "labstream-desktop"
|
name = "labstream-desktop"
|
||||||
version = "1.15.0"
|
version = "1.16.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"active-win-pos-rs",
|
"active-win-pos-rs",
|
||||||
"device_query",
|
"device_query",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "labstream-desktop"
|
name = "labstream-desktop"
|
||||||
version = "1.15.0"
|
version = "1.16.0"
|
||||||
description = "Cliente de escritorio de Labstream OS"
|
description = "Cliente de escritorio de Labstream OS"
|
||||||
authors = ["Labstream SAS"]
|
authors = ["Labstream SAS"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,17 +1,39 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<!--
|
||||||
|
ENTITLEMENTS DE macOS.
|
||||||
|
|
||||||
|
OJO AL ESCRIBIR AQUÍ: nada de dos guiones seguidos dentro de un comentario. XML los prohíbe,
|
||||||
|
y el que lee este archivo NO es plutil (que los deja pasar) sino AMFI, dentro de codesign,
|
||||||
|
que se planta con «Failed to parse entitlements». Por eso abajo las opciones de codesign se
|
||||||
|
nombran sin sus guiones.
|
||||||
|
|
||||||
|
Estas claves solo sirven de algo si la app se FIRMA pasándole este archivo y con el hardened
|
||||||
|
runtime puesto. Hasta el 2026-09-19 no era el caso: el build no firmaba nada y la app salía
|
||||||
|
con la firma ad hoc del enlazador (flags=0x20002 adhoc linker-signed, Info.plist sin atar,
|
||||||
|
recursos sin sellar). O sea, este archivo era decoración. Lo que lo arregla está en
|
||||||
|
.github/workflows/build.yml.
|
||||||
|
|
||||||
|
Son entitlements del HARDENED RUNTIME, no del App Sandbox (esta app no usa sandbox). El
|
||||||
|
hardened runtime cierra por defecto el micrófono, la cámara y la memoria ejecutable; cada
|
||||||
|
clave vuelve a abrir una, y sin ella la función no falla con un error claro: simplemente no
|
||||||
|
hay permiso.
|
||||||
|
|
||||||
|
Se cayó com.apple.security.network.client: esa es exclusiva del App Sandbox y, sin
|
||||||
|
com.apple.security.app-sandbox, no hace absolutamente nada. Si algún día se mete la app en el
|
||||||
|
sandbox, vuelve, junto con las de acceso a archivos que harán falta.
|
||||||
|
-->
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<!-- Micrófono: notas de voz del chat (getUserMedia en el webview). -->
|
<!-- Micrófono: notas de voz del chat (getUserMedia en el webview). El texto que ve la
|
||||||
|
persona al conceder el permiso vive en Info.plist (NSMicrophoneUsageDescription). -->
|
||||||
<key>com.apple.security.device.audio-input</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
<!-- Cámara: adjuntar foto/video en el chat. -->
|
<!-- Cámara: adjuntar foto y video en el chat (NSCameraUsageDescription). -->
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
<!-- Cliente de red (conecta con os.labstreamsas.com). -->
|
<!-- WKWebView compila JavaScript en caliente. Bajo hardened runtime eso es memoria
|
||||||
<key>com.apple.security.network.client</key>
|
ejecutable y hay que pedirlo explícitamente. -->
|
||||||
<true/>
|
|
||||||
<!-- Requerido por WKWebView (JIT de JavaScript) bajo hardened runtime. -->
|
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Labstream OS",
|
"productName": "Labstream OS",
|
||||||
"version": "1.15.0",
|
"version": "1.16.0",
|
||||||
"identifier": "co.labstream.os",
|
"identifier": "co.labstream.os",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user