diff --git a/CHANGELOG.md b/CHANGELOG.md index 177b6a123..545a56635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [1.4.47.3] — 2026-05-22 — APNs PEM diagnostic dump on parse failure + +v1.4.47.2 added defensive PEM normalisation but the production env-var was still rejected at `crypto.createPrivateKey` with `error:1E08010C:DECODER routines::unsupported`. The base64 body itself appears to be corrupted somewhere along the Coolify → docker-compose → `process.env` path; normalising the wrapping cannot recover it. + +This release adds a one-line diagnostic dump on parse failure so the operator can compare the in-container shape against the source `.p8`: + +``` +APNs key did not parse as PEM: [diag raw_len=… norm_len=… + escaped_newlines=… real_newlines=… begin_markers=… end_markers=… + base64_chars=… other_chars=… sha256_prefix=…] +``` + +No secret bytes leave the warning — only character-class counts, structural marker counts, and a 16-char SHA-256 prefix that the operator can compare against `shasum -a 256 AuthKey_*.p8` locally. + +### Changed + +- `src/lib/notifications/senders/apns.ts:loadApnsConfig` — on the existing `crypto.createPrivateKey` failure branch, emit a structured diagnostic warning. Channel disable behaviour unchanged. + +### Risk + +Zero. Behaviour-preserving — only the warning message gets richer when the parse already fails. + ## [1.4.47.2] — 2026-05-22 — APNs JWT signing repair (defensive PEM normalisation) Same-day follow-up to v1.4.47.1. Coolify runtime logs surfaced diff --git a/package.json b/package.json index e0988fac4..7ae03ebdf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.4.47.2", + "version": "1.4.47.3", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "AGPL-3.0-only", "homepage": "https://healthlog.dev", diff --git a/src/lib/notifications/senders/apns.ts b/src/lib/notifications/senders/apns.ts index 82c4d6bfc..45e1cbad3 100644 --- a/src/lib/notifications/senders/apns.ts +++ b/src/lib/notifications/senders/apns.ts @@ -21,7 +21,7 @@ * separately — that's what the library is for, and pinning a single * token to the process lifetime would expire ~50 minutes after boot. */ -import { createPrivateKey } from "node:crypto"; +import { createHash, createPrivateKey } from "node:crypto"; import { readFileSync } from "node:fs"; import apn from "@parse/node-apn"; @@ -219,7 +219,29 @@ export function loadApnsConfig(): ApnsConfig | null { } } catch (err) { const message = err instanceof Error ? err.message : "parse_failed"; - getEvent()?.addWarning(`APNs key did not parse as PEM: ${message}`); + // v1.4.47.3 — diagnostic dump on parse failure so operators can + // tell whether the env-var arrived in the expected shape. + // Logs nothing sensitive: only lengths, character-class counts, + // and a SHA-256 prefix of the normalised body. Never the key bytes. + const sha256 = createHash("sha256").update(signingKey).digest("hex"); + const escapedNewlineCount = (inlineKey.match(/\\n/g) || []).length; + const realNewlineCount = (signingKey.match(/\n/g) || []).length; + const beginCount = (signingKey.match(/-----BEGIN/g) || []).length; + const endCount = (signingKey.match(/-----END/g) || []).length; + const base64Chars = (signingKey.match(/[A-Za-z0-9+/=]/g) || []).length; + const otherChars = + signingKey.length - + base64Chars - + realNewlineCount - + (signingKey.match(/[-\s]/g) || []).length; + getEvent()?.addWarning( + `APNs key did not parse as PEM: ${message} ` + + `[diag raw_len=${inlineKey.length} norm_len=${signingKey.length} ` + + `escaped_newlines=${escapedNewlineCount} real_newlines=${realNewlineCount} ` + + `begin_markers=${beginCount} end_markers=${endCount} ` + + `base64_chars=${base64Chars} other_chars=${otherChars} ` + + `sha256_prefix=${sha256.slice(0, 16)}]`, + ); cachedConfig = null; return null; }