From fbc1e93363d4861d5b98b629c7e3fbaf0ad8a5fa Mon Sep 17 00:00:00 2001 From: Danilo Leone Date: Fri, 7 Aug 2026 13:28:15 -0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(scripts):=20verifica=20a=20instala?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20web=20push=20num=20site=20de=20fora?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web push depende de dois artefatos no lado do cliente, e meia instalação não falha de forma visível: o site simplesmente nunca capta assinatura, e nada no BMS indica isso. Conferir à mão exige abrir o site, o DevTools e comparar hashes na unha, o que é lento e fácil de errar logo depois de um deploy. O script roda o checklist de aceite por HTTP: sw.js responde na raiz com content-type de JavaScript, sem redirect para subpasta — o escopo do worker não sobe de diretório; o importScripts aponta para um /bms/push/.js que existe; o hash é o da conta esperada; e a página carrega bmsTrkOptions, o bmstrk.js e o mesmo hash. Sai 0 quando tudo passa e 1 na primeira falha real, então serve tanto para conferência manual quanto para amarrar num passo de deploy. --- scripts/verify-web-push-install.sh | 185 +++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100755 scripts/verify-web-push-install.sh diff --git a/scripts/verify-web-push-install.sh b/scripts/verify-web-push-install.sh new file mode 100755 index 00000000..82343f16 --- /dev/null +++ b/scripts/verify-web-push-install.sh @@ -0,0 +1,185 @@ +#!/bin/bash +# Checks a customer site's web-push installation from the outside. +# +# Web push needs two artifacts on the customer side, and half an install fails +# silently: the site simply never captures a subscription, and nothing in the +# BMS says so. This runs the acceptance checklist over HTTP so the answer is one +# command instead of a browser session. +# +# Checks: +# 1. /sw.js responds 200 with a JavaScript content-type +# 2. it lives at the ROOT (a redirect into a subpath is a failure — service +# worker scope does not climb directories) +# 3. it importScripts() a BMS /bms/push/.js URL +# 4. that accountHash is the expected account (when --account is given) +# 5. the worker URL it points at actually resolves +# 6. the page carries the snippet: bmsTrkOptions, the same accountHash, and +# bmstrk.js (when a page URL is given) +# +# Usage: +# verify-web-push-install.sh [--account ] [--page ] +# +# verify-web-push-install.sh https://site.example.com +# verify-web-push-install.sh https://site.example.com --account 2 +# verify-web-push-install.sh https://site.example.com --account 2 --page https://site.example.com/noticias +# +# Exit: 0 all checks passed, 1 something failed, 2 bad usage. +# Requires: bash, curl, sha256sum. + +set -uo pipefail + +SITE="" +ACCOUNT_ID="" +PAGE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --account) + ACCOUNT_ID="${2:-}" + shift 2 + ;; + --page) + PAGE="${2:-}" + shift 2 + ;; + -h | --help) + sed -n '2,30p' "$0" + exit 0 + ;; + *) + if [[ -z "$SITE" ]]; then SITE="$1"; else + echo "argumento inesperado: $1" >&2 + exit 2 + fi + shift + ;; + esac +done + +if [[ -z "$SITE" ]]; then + echo "uso: $(basename "$0") [--account ] [--page ]" >&2 + exit 2 +fi + +SITE="${SITE%/}" +SW_URL="$SITE/sw.js" +FALHAS=0 + +ok() { printf ' \033[32mok\033[0m %s\n' "$1"; } +falha() { + printf ' \033[31mfalha\033[0m %s\n' "$1" + FALHAS=$((FALHAS + 1)) +} +aviso() { printf ' \033[33maviso\033[0m %s\n' "$1"; } + +echo "Verificando $SITE" +echo + +# --- 1/2. o arquivo na raiz ------------------------------------------------ +echo "sw.js na raiz" + +# -L para seguir redirect, mas guardamos a URL final para detectar movimento. +RESP="$(curl -sS -L -o /tmp/sw-verify.$$ -w '%{http_code}\t%{content_type}\t%{url_effective}' "$SW_URL" 2>/dev/null)" +CODIGO="$(cut -f1 <<<"$RESP")" +TIPO="$(cut -f2 <<<"$RESP" | cut -d';' -f1)" +URL_FINAL="$(cut -f3 <<<"$RESP")" +CORPO="$(cat /tmp/sw-verify.$$ 2>/dev/null)" +rm -f /tmp/sw-verify.$$ + +if [[ "$CODIGO" == "200" ]]; then + ok "$SW_URL responde 200" +else + falha "$SW_URL respondeu $CODIGO" + echo + echo "$FALHAS falha(s) — o resto da verificação depende deste arquivo." + exit 1 +fi + +case "$TIPO" in + application/javascript | text/javascript | application/x-javascript) + ok "content-type $TIPO" + ;; + *) + falha "content-type $TIPO — o navegador recusa registrar um worker que não é servido como JavaScript" + ;; +esac + +if [[ "$URL_FINAL" != "$SW_URL" ]]; then + falha "redirecionado para $URL_FINAL — o escopo do service worker não sobe de diretório, o arquivo tem que responder na raiz" +else + ok "sem redirecionamento — está na raiz" +fi + +# --- 3/4/5. para onde o shim aponta ---------------------------------------- +echo +echo "destino do importScripts" + +WORKER_URL="$(grep -oE 'importScripts\(["'"'"']([^"'"'"']+)' <<<"$CORPO" | head -1 | sed -E 's/.*["'"'"']//')" + +if [[ -z "$WORKER_URL" ]]; then + falha "não achei um importScripts() no arquivo — ele deveria ser um shim de duas linhas" +else + ok "importScripts para $WORKER_URL" + + HASH_ENCONTRADO="$(grep -oE '/bms/push/[a-f0-9]{64}\.js' <<<"$WORKER_URL" | grep -oE '[a-f0-9]{64}')" + if [[ -z "$HASH_ENCONTRADO" ]]; then + falha "a URL não tem o formato /bms/push/.js" + else + if [[ -n "$ACCOUNT_ID" ]]; then + HASH_ESPERADO="$(printf '%s' "$ACCOUNT_ID" | sha256sum | cut -d' ' -f1)" + if [[ "$HASH_ENCONTRADO" == "$HASH_ESPERADO" ]]; then + ok "accountHash confere com a conta $ACCOUNT_ID" + else + falha "accountHash é de outra conta (esperado para $ACCOUNT_ID: $HASH_ESPERADO)" + fi + else + aviso "accountHash não conferido — passe --account para validar" + fi + fi + + CODIGO_WORKER="$(curl -sS -o /dev/null -w '%{http_code}' "$WORKER_URL" 2>/dev/null)" + if [[ "$CODIGO_WORKER" == "200" ]]; then + ok "o worker no BMS responde 200" + else + falha "o worker no BMS respondeu $CODIGO_WORKER — o shim aponta para um arquivo que não existe" + fi +fi + +# --- 6. o snippet na página ------------------------------------------------ +echo +echo "snippet na página" + +if [[ -z "$PAGE" ]]; then + aviso "não verificado — passe --page com uma página onde o push deve ser oferecido" +else + HTML="$(curl -sS -L "$PAGE" 2>/dev/null)" + if grep -q 'bmsTrkOptions' <<<"$HTML"; then + ok "bmsTrkOptions presente" + + if grep -q 'bmstrk.js' <<<"$HTML"; then + ok "bmstrk.js carregado" + else + falha "bmstrk.js não é carregado na página" + fi + + # Só compara o hash quando há snippet: numa página sem snippet a divergência + # é consequência da ausência, e reportá-la sugeriria conta errada. + if [[ -n "${HASH_ENCONTRADO:-}" ]]; then + if grep -q "$HASH_ENCONTRADO" <<<"$HTML"; then + ok "accountHash da página bate com o do sw.js" + else + falha "o accountHash da página diverge do que está no sw.js — são contas diferentes" + fi + fi + else + falha "bmsTrkOptions ausente — sem o snippet nada registra, por mais correto que o sw.js esteja" + fi +fi + +echo +if [[ "$FALHAS" -eq 0 ]]; then + echo "tudo certo." + exit 0 +fi +echo "$FALHAS verificação(ões) falharam." +exit 1 From 4e14809093340d7ff75903d74b2bfb32830e3281 Mon Sep 17 00:00:00 2001 From: Danilo Leone Date: Tue, 11 Aug 2026 15:41:11 -0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(scripts):=20valida=20snippet=20por=20?= =?UTF-8?q?=C3=A2ncora=20e=20usa=20temp=20seguro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substring solto sobre o documento inteiro deixava passar um snippet comentado/stale contendo bmsTrkOptions, o hash e bmstrk.js — instalação quebrada reportada como correta. Passa a remover comentários HTML e checar o bloco de configuração real e o src do script. /tmp/sw-verify.$$ era previsível: outro usuário local podia pré-criar como symlink e o curl sobrescrever o alvo. Troca por mktemp -d + trap. sha256sum não existe no macOS; adiciona fallback para shasum -a 256. --- scripts/verify-web-push-install.sh | 40 ++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/scripts/verify-web-push-install.sh b/scripts/verify-web-push-install.sh index 82343f16..fabe7833 100755 --- a/scripts/verify-web-push-install.sh +++ b/scripts/verify-web-push-install.sh @@ -24,10 +24,18 @@ # verify-web-push-install.sh https://site.example.com --account 2 --page https://site.example.com/noticias # # Exit: 0 all checks passed, 1 something failed, 2 bad usage. -# Requires: bash, curl, sha256sum. +# Requires: bash, curl, and sha256sum (Linux) or shasum (macOS). set -uo pipefail +sha256_hex() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum | cut -d' ' -f1 + else + shasum -a 256 | cut -d' ' -f1 + fi +} + SITE="" ACCOUNT_ID="" PAGE="" @@ -65,6 +73,9 @@ SITE="${SITE%/}" SW_URL="$SITE/sw.js" FALHAS=0 +TMPD="$(mktemp -d)" +trap 'rm -rf "$TMPD"' EXIT + ok() { printf ' \033[32mok\033[0m %s\n' "$1"; } falha() { printf ' \033[31mfalha\033[0m %s\n' "$1" @@ -79,12 +90,11 @@ echo echo "sw.js na raiz" # -L para seguir redirect, mas guardamos a URL final para detectar movimento. -RESP="$(curl -sS -L -o /tmp/sw-verify.$$ -w '%{http_code}\t%{content_type}\t%{url_effective}' "$SW_URL" 2>/dev/null)" +RESP="$(curl -sS -L -o "$TMPD/sw.js" -w '%{http_code}\t%{content_type}\t%{url_effective}' "$SW_URL" 2>/dev/null)" CODIGO="$(cut -f1 <<<"$RESP")" TIPO="$(cut -f2 <<<"$RESP" | cut -d';' -f1)" URL_FINAL="$(cut -f3 <<<"$RESP")" -CORPO="$(cat /tmp/sw-verify.$$ 2>/dev/null)" -rm -f /tmp/sw-verify.$$ +CORPO="$(cat "$TMPD/sw.js" 2>/dev/null)" if [[ "$CODIGO" == "200" ]]; then ok "$SW_URL responde 200" @@ -126,7 +136,7 @@ else falha "a URL não tem o formato /bms/push/.js" else if [[ -n "$ACCOUNT_ID" ]]; then - HASH_ESPERADO="$(printf '%s' "$ACCOUNT_ID" | sha256sum | cut -d' ' -f1)" + HASH_ESPERADO="$(printf '%s' "$ACCOUNT_ID" | sha256_hex)" if [[ "$HASH_ENCONTRADO" == "$HASH_ESPERADO" ]]; then ok "accountHash confere com a conta $ACCOUNT_ID" else @@ -153,19 +163,33 @@ if [[ -z "$PAGE" ]]; then aviso "não verificado — passe --page com uma página onde o push deve ser oferecido" else HTML="$(curl -sS -L "$PAGE" 2>/dev/null)" - if grep -q 'bmsTrkOptions' <<<"$HTML"; then + # Comentado ou texto solto não conta: um snippet velho deixado em não + # registra nada, mas bateria em qualquer checagem por substring simples. Junta + # tudo numa linha (bash puro, sem depender de sed multi-line) para o strip de + # comentário cruzar quebras de linha. + HTML_1L="${HTML//$'\n'/$'\x01'}" + HTML_1L="$(sed -E 's///g' <<<"$HTML_1L")" + HTML_LIMPO="${HTML_1L//$'\x01'/$'\n'}" + + if grep -qE 'bmsTrkOptions[[:space:]]*=[[:space:]]*\{' <<<"$HTML_LIMPO"; then ok "bmsTrkOptions presente" - if grep -q 'bmstrk.js' <<<"$HTML"; then + if grep -qE ']+src=["'"'"'][^"'"'"']*bmstrk\.js' <<<"$HTML_LIMPO"; then ok "bmstrk.js carregado" else falha "bmstrk.js não é carregado na página" fi + # Bloco de configuração isolado: o hash tem que estar DENTRO do objeto + # bmsTrkOptions, não em qualquer lugar do documento — senão um hash de outra + # conta perdido no HTML (comentário removido acima já cobre o caso óbvio, + # isto cobre o resto) passaria como coincidência. + BLOCO_CONFIG="$(awk '/bmsTrkOptions[[:space:]]*=[[:space:]]*\{/{f=1} f{print} f&&/\}/{exit}' <<<"$HTML_LIMPO")" + # Só compara o hash quando há snippet: numa página sem snippet a divergência # é consequência da ausência, e reportá-la sugeriria conta errada. if [[ -n "${HASH_ENCONTRADO:-}" ]]; then - if grep -q "$HASH_ENCONTRADO" <<<"$HTML"; then + if grep -q "$HASH_ENCONTRADO" <<<"$BLOCO_CONFIG"; then ok "accountHash da página bate com o do sw.js" else falha "o accountHash da página diverge do que está no sw.js — são contas diferentes"