feat(handler_ws): pluginLifecycle en copias locales acepta secreto_local del .acai

Segunda via de autenticacion de los ganchos de ciclo de vida en copias
locales de Forge: ademas del par token+tokenHash del .acai, acepta el
campo secretoLocal del cuerpo si coincide (hash_equals) con
secreto_local del .acai. Las cadenas vacias o no escalares nunca
coinciden y mode=production sigue rechazando.

La via token+tokenHash queda como transitoria hasta que el .acai deje
de llevar el token (fase 1d).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jordan Diaz
2026-10-19 20:35:25 +00:00
co-authored by Claude Opus 5.5
parent dabdfa1354
commit 5709c5c856
+25 -8
View File
@@ -406,10 +406,13 @@ function _renameFile() {
* (install/uninstall/enable/disable) quedaban inservibles ahi con un * (install/uninstall/enable/disable) quedaban inservibles ahi con un
* "Token no valido" que no tenia nada que ver con el token. * "Token no valido" que no tenia nada que ver con el token.
* *
* No es un bypass: se compara el par token+tokenHash recibido con el que la * No es un bypass: se compara un secreto recibido en el cuerpo con el que la
* propia web guarda en su .acai, que es exactamente el que Forge envia. Sin * propia web guarda en su .acai. Sin .acai (produccion), con mode=production,
* .acai (produccion), con mode=production, o sin coincidencia exacta, no * o sin coincidencia exacta, no autoriza nada. Dos formas de coincidir:
* autoriza nada. * - secretoLocal (cuerpo) == secreto_local (.acai): el secreto de la copia,
* que no es una credencial de ninguna persona. Es la via buena.
* - token+tokenHash (cuerpo) == token+tokenHash (.acai): la de siempre.
* Las cadenas vacias nunca coinciden. Los secretos no se registran nunca.
*/ */
function _lifecycleTokenLocalValido($datosAuth) { function _lifecycleTokenLocalValido($datosAuth) {
$acaiPath = __DIR__ . "/../../../.acai"; $acaiPath = __DIR__ . "/../../../.acai";
@@ -420,10 +423,24 @@ function _lifecycleTokenLocalValido($datosAuth) {
if (!is_array($acai) || @$acai["mode"] === "production") { if (!is_array($acai) || @$acai["mode"] === "production") {
return false; return false;
} }
$token = (string) @$datosAuth["token"]; if (!is_array($datosAuth)) {
$hash = (string) @$datosAuth["tokenHash"]; return false;
$tokenLocal = (string) @$acai["token"]; }
$hashLocal = (string) @$acai["tokenHash"];
// Via buena: el secreto propio de la copia local.
$secretoLocal = @$acai["secreto_local"];
$secreto = @$datosAuth["secretoLocal"];
if (is_string($secretoLocal) && $secretoLocal !== ""
&& is_scalar($secreto) && (string) $secreto !== ""
&& hash_equals($secretoLocal, (string) $secreto)) {
return true;
}
// transitorio: quitar cuando el .acai ya no lleve token (fase 1d)
$token = is_scalar(@$datosAuth["token"]) ? (string) $datosAuth["token"] : "";
$hash = is_scalar(@$datosAuth["tokenHash"]) ? (string) $datosAuth["tokenHash"] : "";
$tokenLocal = is_scalar(@$acai["token"]) ? (string) $acai["token"] : "";
$hashLocal = is_scalar(@$acai["tokenHash"]) ? (string) $acai["tokenHash"] : "";
if ($token === "" || $hash === "" || $tokenLocal === "" || $hashLocal === "") { if ($token === "" || $hash === "" || $tokenLocal === "" || $hashLocal === "") {
return false; return false;
} }