13. Notificaciones push

Versión para imprimir.

1. Introduccion

2. Diagrama entidad relación

Diagrama entidad relación

3. Diagrama de despliegue

Diagrama de despliegue

4. Hazlo funcionar (con videos)

  1. Prueba el ejemplo en https://notipush.rf.gd/.

  2. Copia la url de la app y pégala en varios navegadores y dispositivos.

  3. Suscríbanse y envíen notificaciones ntre los dispositivos conectados.

  4. Descarga el archivo /src/notipush.zip y descompáctalo.

  5. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

  6. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

  7. Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

  8. Importa el proyecto de GitHub a Visual Studio Code

  9. Edita los archivos que desees.

  10. Prueba tu sitio localmente.

  11. Necesitas un hosting. En este ejemplo se muestra como usar el hosting. https://infinityfree.com/ Si no lo has usado, lo primero que tienes que hacer es entrar a registrar tu email con el botón Registrar. Si ya tienes tu email registrado, omite este paso.

  12. Crea una cuenta. Si ya tienes cuenta, entra a ella y crea un nuevo dominio. En este ejemplo no se crean los archivos directamente en el hosting.

  13. Sube tus archivos al hosting usando ftp.

  14. Sube tus archivos a GitHub. En este ejemplo no hay archivo sw.js ni necesitas esperar 11 o más minutos.

5. Hazlo funcionar (texto)

  1. Este ejercicio usa la librería WebPush para PHP. Puedes profundizar en este tema en la URL https://github.com/web-push-libs/web-push-php

  2. Prueba el ejemplo en https://notipush.rf.gd/.

  3. Copia la url de la app y pégala en varios navegadores y dispositivos.

  4. Suscríbanse y envíen notificaciones ntre los dispositivos conectados.

  5. Descarga el archivo /src/notipush.zip y descompáctalo.

  6. Crea tu proyecto en GitHub:

    1. Crea una cuenta de email, por ejemplo, pepito@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.

      • Mantén la selección Public para que otros usuarios puedan ver tu proyecto.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

  7. Importa el proyecto en GitHub:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.

  8. Edita los archivos que desees.

  9. El ejercicio usa una llave pública en el servidor y la repite el código de JavaScript en el navegador. También se requiere la correspondiente llave privada que se almacena solo en el servidor.

  10. Las llaves se pueden generar con el enlace Genera llaves del ejemplo. Para copiar los valores despliega el código fuente de la página, porque en el navegador algunos caracteres pueden tener interpretación diferente.

  11. El proyecto ya contiene la carpeta vendor y el archivo composer.lock, pero es posible crearlos con estos pasos:

    1. Instalar composer. Para Windows, usa el instalador de https://getcomposer.org/download/.

    2. Abre una terminal y ejecuta el comando
      composer update

  12. Haz clic derecho en index.html, selecciona PHP Server: serve project y se abre el navegador para que puedas probar localmente el ejemplo.

  13. Para depurar paso a paso haz lo siguiente:

    1. En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.

    2. Recarga la página, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    3. Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).

    4. Selecciona el archivo donde vas a empezar a depurar.

    5. Haz clic en el número de la línea donde vas a empezar a depurar.

    6. En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.

    7. Haz clic en Run and Debug .

    8. Si no está configurada la depuración, haz clic en create a launch json file.

    9. Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe decir Listen for Xdebug .

    10. Aparece un cuadro con los controles de depuración.

    11. Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.

    12. Regresa al navegador, recarga la página de manera normal y empieza a usarla.

    13. Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.

  14. Sube el proyecto al hosting que elijas.

    1. Crea una nueva carpeta para crear un nuevo proyecto que estará conectado directamente al servidor web por ftp.

    2. Abre la nueva carpeta con Visual Studio Code.

    3. Tecle al mismo Mayúsculas+Control+P y selecciona SFTP: Config. Aparece un archivo de configuración de FTP. Llena los datos con la configuración de FTP de tu servidor, excepto la contraseña.

    4. Cliquea el botón de SFTP y luego haz clic en la URL de tu servidos. En la barra superior te pide la contraseña y ENTER.

    5. Pásate a la parte de archivos y coloca tus archivos.

    6. Cliquea con el botón derecho en la sección de archivos y selecciona Sync: Local -> Remote.

  15. Abre un navegador y prueba el proyecto en tu hosting.

  16. En el hosting InfinityFree, la primera vez que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.

  17. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push

6. Archivos

Haz clic en los triángulos para expandir las carpetas

7. index.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Notificaciones Push</title>
10
11
 <script type="module" src="js/lib/manejaErrores.js"></script>
12
13
</head>
14
15
<body>
16
17
 <h1>Notificaciones Push</h1>
18
19
 <menu style="display: flex; list-style: none; flex-wrap: wrap; gap: 0.5rem;">
20
  <li>
21
   <button id="btnSuscribe" type="button" hidden>
22
    Suscríbete
23
   </button>
24
  </li>
25
  <li>
26
   <button id="btnCancela" type="button" hidden>
27
    Cancela suscripción
28
   </button>
29
  </li>
30
  <li>
31
   <button id="btnNotifica" type="button" hidden>
32
    Notifica
33
   </button>
34
  </li>
35
  <li>
36
   <a href="srv/genera-llaves.php" target="_blank">Genera llaves</a>
37
  </li>
38
 </menu>
39
40
 <p>
41
  <output id="outEstado">
42
   <progress max="100">Cargando…</progress>
43
  </output>
44
 </p>
45
46
 <fieldset>
47
  <legend>Reporte de envío a endpoints</legend>
48
49
  <dl id="reporte"></dl>
50
51
 </fieldset>
52
53
 <script type="module">
54
55
  import {
56
   activaNotificacionesPush
57
  } from "./js/lib/activaNotificacionesPush.js"
58
  import { getSuscripcionPush } from "./js/lib/getSuscripcionPush.js"
59
  import { suscribeAPush } from "./js/lib/suscribeAPush.js"
60
  import { cancelaSuscripcionPush } from "./js/lib/cancelaSuscripcionPush.js"
61
  import { urlBase64ToUint8Array } from "./js/lib/urlBase64ToUint8Array.js"
62
  import {
63
   calculaDtoParaSuscripcion
64
  } from "./js/lib/calculaDtoParaSuscripcion.js"
65
  import { consume } from "./js/lib/consume.js"
66
  import { enviaJsonRecibeJson } from "./js/lib/enviaJsonRecibeJson.js"
67
  import { descargaVista } from "./js/lib/descargaVista.js"
68
  import { muestraError } from "./js/lib/muestraError.js"
69
70
  const applicationServerKey = urlBase64ToUint8Array(
71
   "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0")
72
  /** @enum {string} */
73
  const Estado = {
74
   CALCULANDO: "Calculando…",
75
   SUSCRITO: "Suscrito",
76
   DESUSCRITO: "Sin suscripción",
77
   INCOMPATIBLE: "Incompatible"
78
  }
79
80
  preparaVista()
81
  btnSuscribe.addEventListener("click", suscribe)
82
  btnCancela.addEventListener("click", cancela)
83
  btnNotifica.addEventListener("click", notificaDesdeElServidor)
84
85
  async function preparaVista() {
86
   try {
87
    await activaNotificacionesPush("sw.js")
88
    const suscripcion = await getSuscripcionPush()
89
    if (suscripcion === null) {
90
     muestraEstado(Estado.DESUSCRITO)
91
    } else {
92
     // Modifica la suscripción en el servidor,
93
     const dto = calculaDtoParaSuscripcion(suscripcion)
94
     await consume(enviaJsonRecibeJson("php/suscripcion-modifica.php", dto))
95
     muestraEstado(Estado.SUSCRITO)
96
    }
97
   } catch (error) {
98
    muestraEstado(Estado.INCOMPATIBLE)
99
    muestraError(error)
100
   }
101
  }
102
103
  async function notificaDesdeElServidor() {
104
   reporte.innerHTML =
105
     /* html */ `<progress max="100">Cargando…</progress>`
106
   await descargaVista("php/notifica.php", "POST")
107
  }
108
109
  async function suscribe() {
110
   muestraEstado(Estado.CALCULANDO)
111
   const suscripcion = await suscribeAPush(applicationServerKey)
112
   // Agrega la suscripción al servidor,
113
   const dto = calculaDtoParaSuscripcion(suscripcion)
114
   await consume(enviaJsonRecibeJson("php/suscripcion-modifica.php", dto)
115
   )
116
   muestraEstado(Estado.SUSCRITO)
117
  }
118
119
  async function cancela() {
120
   muestraEstado(Estado.CALCULANDO)
121
   const suscripcion = await cancelaSuscripcionPush()
122
   if (suscripcion !== null) {
123
    // Elimina la suscripción en el servidor,
124
    const dto = calculaDtoParaSuscripcion(suscripcion)
125
    await consume(enviaJsonRecibeJson("php/suscripcion-elimina.php", dto))
126
    muestraEstado(Estado.DESUSCRITO)
127
   }
128
  }
129
130
  /**
131
   * @param {Estado} estado
132
   */
133
  function muestraEstado(estado) {
134
   outEstado.value = estado
135
   if (estado === Estado.INCOMPATIBLE || estado === Estado.CALCULANDO) {
136
    btnSuscribe.hidden = true
137
    btnCancela.hidden = true
138
    btnNotifica.hidden = true
139
   } else if (estado === Estado.SUSCRITO) {
140
    btnSuscribe.hidden = true
141
    btnCancela.hidden = false
142
    btnNotifica.hidden = false
143
   } else if (estado === Estado.DESUSCRITO) {
144
    btnSuscribe.hidden = false
145
    btnCancela.hidden = true
146
    btnNotifica.hidden = true
147
   }
148
  }
149
150
 </script>
151
152
 </body>
153
154
</html>

8. sw.js

1
const URL_SERVIDOR = "https://notipush.rf.gd"
2
3
if (self instanceof ServiceWorkerGlobalScope) {
4
5
 // El siguiente código se activa cuando llega una notificación push.
6
 self.addEventListener("push", (/** @type {PushEvent} */ event) => {
7
  const notificacion = event.data
8
  /* Si el navegador tiene permitido mostrar notificaciones push,
9
   * nuestra la que se ha recibido. */
10
  if (notificacion !== null && self.Notification.permission === 'granted') {
11
   event.waitUntil(muestraNotificacion(notificacion))
12
  }
13
 })
14
15
 self.addEventListener("notificationclick",
16
  (/** @type {NotificationEvent} */ event) => {
17
   event.notification.close()
18
   event.waitUntil(muestraVentana())
19
  })
20
}
21
22
/**
23
 * @param {PushMessageData} notificacion
24
 */
25
async function muestraNotificacion(notificacion) {
26
 if (self instanceof ServiceWorkerGlobalScope) {
27
  const mensaje = notificacion.text()
28
  await self.registration.showNotification(mensaje)
29
 }
30
}
31
32
async function muestraVentana() {
33
 if (self instanceof ServiceWorkerGlobalScope) {
34
  const clientes = await self.clients.matchAll({ type: "window" })
35
  for (const cliente of clientes) {
36
   if (cliente.url.startsWith(URL_SERVIDOR)) {
37
    return cliente.focus()
38
   }
39
  }
40
  return self.clients.openWindow("/")
41
 }
42
}

9. Carpeta « php »

Versión para imprimir.

A. php / Bd.php

1
<?php
2
3
class Bd
4
{
5
6
 private static ?PDO $pdo = null;
7
8
 static function pdo(): PDO
9
 {
10
  if (self::$pdo === null) {
11
12
   self::$pdo = new PDO(
13
    // cadena de conexión
14
    "sqlite:" . __DIR__ . "/notipush.db",
15
    // usuario
16
    null,
17
    // contraseña
18
    null,
19
    // Opciones: pdos no persistentes y lanza excepciones.
20
    [PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
21
   );
22
23
   self::$pdo->exec(
24
    'CREATE TABLE IF NOT EXISTS SUSCRIPCION (
25
       SUS_ENDPOINT TEXT NOT NULL,
26
       SUS_PUB_KEY TEXT NOT NULL,
27
       SUS_AUT_TOK TEXT NOT NULL,
28
       SUS_CONT_ENCOD TEXT NOT NULL,
29
      CONSTRAINT SUS_PK
30
       PRIMARY KEY(SUS_ENDPOINT),
31
      CONSTRAINT SUS_ENDPNT_NV
32
       CHECK(LENGTH(SUS_ENDPOINT) > 0)
33
     )'
34
   );
35
  }
36
37
  return self::$pdo;
38
 }
39
}
40

B. php / genera-llaves.php

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Llaves VAPID</title>
10
11
</head>
12
13
<body>
14
15
 <h1>Llaves VAPID</h1>
16
17
 <pre>
18
 <?php
19
20
 require __DIR__ . "/../vendor/autoload.php";
21
22
 use Minishlink\WebPush\VAPID;
23
24
 var_dump(VAPID::createVapidKeys());
25
26
 ?>
27
 </pre>
28
29
</body>
30
31
</html>

C. php / notifica.php

1
<?php
2
3
require_once __DIR__ . "/lib/manejaErrores.php";
4
require_once __DIR__ . "/../vendor/autoload.php";
5
require_once  __DIR__ . "/lib/devuelveJson.php";
6
require_once  __DIR__ . "/Bd.php";
7
require_once __DIR__ . "/Suscripcion.php";
8
require_once __DIR__ . "/suscripcionElimina.php";
9
10
use Minishlink\WebPush\WebPush;
11
12
const AUTH = [
13
 "VAPID" => [
14
  "subject" => "https://notipush.rf.gd/",
15
  "publicKey" => "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0",
16
  "privateKey" => "vplfkITvu0cwHqzK9Kj-DYStbCH_9AhGx9LqMyaeI6w"
17
 ]
18
];
19
20
$webPush = new WebPush(AUTH);
21
$mensaje = "Hola! 👋";
22
23
// Envia el mensaje a todas las suscripciones.
24
25
$bd = Bd::pdo();
26
$stmt = $bd->query("SELECT * FROM SUSCRIPCION");
27
$suscripciones =
28
 $stmt->fetchAll(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, Suscripcion::class);
29
30
foreach ($suscripciones as $suscripcion) {
31
 $webPush->queueNotification($suscripcion, $mensaje);
32
}
33
$reportes = $webPush->flush();
34
35
// Genera el reporte de envio a cada suscripcion.
36
$reporteDeEnvios = "";
37
foreach ($reportes as $reporte) {
38
 $endpoint = $reporte->getRequest()->getUri();
39
 $htmlEndpoint = htmlentities($endpoint);
40
 if ($reporte->isSuccess()) {
41
  // Reporte de éxito.
42
  $reporteDeEnvios .= "<dt>$htmlEndpoint</dt><dd>Éxito</dd>";
43
 } else {
44
  if ($reporte->isSubscriptionExpired()) {
45
   suscripcionElimina($bd, $endpoint);
46
  }
47
  // Reporte de fallo.
48
  $explicacion = htmlentities($reporte->getReason());
49
  $reporteDeEnvios .= "<dt>$endpoint</dt><dd>Fallo: $explicacion</dd>";
50
 }
51
}
52
53
devuelveJson(["reporte" => ["innerHTML" => $reporteDeEnvios]]);
54

D. php / recibeSuscripcion.php

1
<?php
2
3
require_once __DIR__ . "/lib/BAD_REQUEST.php";
4
require_once __DIR__ . "/lib/recibeJson.php";
5
require_once __DIR__ . "/lib/ProblemDetailsException.php";
6
7
function recibeSuscripcion()
8
{
9
10
 $objeto = recibeJson();
11
12
 if (
13
  !isset($objeto->authToken)
14
  || !is_string($objeto->authToken)
15
  || $objeto->authToken === ""
16
 )
17
  throw new ProblemDetailsException([
18
   "status" => BAD_REQUEST,
19
   "title" => "El authToken debe ser texto que no esté en blanco.",
20
   "type" => "/errors/authtokenincorrecto.html",
21
  ]);
22
23
 if (
24
  !isset($objeto->contentEncoding)
25
  || !is_string($objeto->contentEncoding)
26
  || $objeto->contentEncoding === ""
27
 )
28
  throw new ProblemDetailsException([
29
   "status" => BAD_REQUEST,
30
   "title" => "La contentEncoding debe ser texto que no esté en blanco.",
31
   "type" => "/errors/contentencodingincorrecta.html",
32
  ]);
33
34
 if (
35
  !isset($objeto->endpoint)
36
  || !is_string($objeto->endpoint)
37
  || $objeto->endpoint === ""
38
 )
39
  throw new ProblemDetailsException([
40
   "status" => BAD_REQUEST,
41
   "title" => "El endpoint debe ser texto que no esté en blanco.",
42
   "type" => "/errors/endpointincorrecto.html",
43
  ]);
44
45
 if (
46
  !isset($objeto->publicKey)
47
  || !is_string($objeto->publicKey)
48
  || $objeto->publicKey === ""
49
 )
50
  throw new ProblemDetailsException([
51
   "status" => BAD_REQUEST,
52
   "title" => "La publicKey debe ser texto que no esté en blanco.",
53
   "type" => "/errors/publickeyincorrecta.html",
54
  ]);
55
56
 return [
57
  "SUS_AUT_TOK" => $objeto->authToken,
58
  "SUS_CONT_ENCOD" => $objeto->contentEncoding,
59
  "SUS_ENDPOINT" => $objeto->endpoint,
60
  "SUS_PUB_KEY" => $objeto->publicKey,
61
 ];
62
}
63

E. php / suscripcion-elimina.php

1
<?php
2
3
require_once __DIR__ . "/lib/manejaErrores.php";
4
require_once __DIR__ . "/lib/devuelveNoContent.php";
5
require_once  __DIR__ . "/Bd.php";
6
require_once  __DIR__ . "/recibeSuscripcion.php";
7
require_once  __DIR__ . "/suscripcionElimina.php";
8
9
$modelo = recibeSuscripcion();
10
suscripcionElimina(Bd::pdo(), $modelo["SUS_ENDPOINT"]);
11
devuelveNoContent();
12

F. php / suscripcion-modifica.php

1
<?php
2
3
require_once __DIR__ . "/lib/manejaErrores.php";
4
require_once __DIR__ . "/lib/devuelveCreated.php";
5
require_once __DIR__ . "/lib/devuelveJson.php";
6
require_once  __DIR__ . "/Bd.php";
7
require_once  __DIR__ . "/recibeSuscripcion.php";
8
9
$modelo = recibeSuscripcion();
10
11
$bd = Bd::pdo();
12
13
$stmt =
14
 $bd->prepare("SELECT * FROM SUSCRIPCION WHERE SUS_ENDPOINT = :SUS_ENDPOINT");
15
$stmt->execute([":SUS_ENDPOINT" => $modelo["SUS_ENDPOINT"]]);
16
$anterior = $stmt->fetch(PDO::FETCH_ASSOC);
17
18
if ($anterior === false) {
19
20
 $stmt = $bd->prepare(
21
  "INSERT INTO SUSCRIPCION (
22
    SUS_ENDPOINT, SUS_PUB_KEY, SUS_AUT_TOK, SUS_CONT_ENCOD
23
   ) values (
24
    :SUS_ENDPOINT, :SUS_PUB_KEY, :SUS_AUT_TOK, :SUS_CONT_ENCOD
25
   )"
26
 );
27
 $stmt->execute([
28
  ":SUS_ENDPOINT" => $modelo["SUS_ENDPOINT"],
29
  ":SUS_PUB_KEY" => $modelo["SUS_PUB_KEY"],
30
  ":SUS_AUT_TOK" => $modelo["SUS_AUT_TOK"],
31
  ":SUS_CONT_ENCOD" => $modelo["SUS_CONT_ENCOD"],
32
 ]);
33
34
 devuelveCreated("", $modelo);
35
} else {
36
37
 $stmt = $bd->prepare(
38
  "UPDATE SUSCRIPCION
39
   SET
40
    SUS_PUB_KEY = :SUS_PUB_KEY,
41
    SUS_AUT_TOK = :SUS_AUT_TOK,
42
    SUS_CONT_ENCOD = :SUS_CONT_ENCOD
43
   WHERE
44
    SUS_ENDPOINT = :SUS_ENDPOINT"
45
 );
46
 $stmt->execute([
47
  ":SUS_PUB_KEY" => $modelo["SUS_PUB_KEY"],
48
  ":SUS_AUT_TOK" => $modelo["SUS_AUT_TOK"],
49
  ":SUS_CONT_ENCOD" => $modelo["SUS_CONT_ENCOD"],
50
  ":SUS_ENDPOINT" => $modelo["SUS_ENDPOINT"],
51
 ]);
52
53
 devuelveJson($modelo);
54
}
55

G. php / Suscripcion.php

1
<?php
2
3
require_once __DIR__ . "/../vendor/autoload.php";
4
5
use Minishlink\WebPush\SubscriptionInterface;
6
7
class Suscripcion implements SubscriptionInterface
8
{
9
10
 public string $SUS_ENDPOINT;
11
 public string $SUS_PUB_KEY;
12
 public string $SUS_AUT_TOK;
13
 public string $SUS_CONT_ENCOD;
14
15
 public function __construct(
16
  string $SUS_ENDPOINT = "",
17
  string $SUS_PUB_KEY = "",
18
  string $SUS_AUT_TOK = "",
19
  string $SUS_CONT_ENCOD = ""
20
 ) {
21
  $this->SUS_ENDPOINT = $SUS_ENDPOINT;
22
  $this->SUS_PUB_KEY = $SUS_PUB_KEY;
23
  $this->SUS_AUT_TOK = $SUS_AUT_TOK;
24
  $this->SUS_CONT_ENCOD = $SUS_CONT_ENCOD;
25
 }
26
27
 public function getEndpoint(): string
28
 {
29
  return $this->SUS_ENDPOINT;
30
 }
31
32
 public function getPublicKey(): ?string
33
 {
34
  return $this->SUS_PUB_KEY;
35
 }
36
37
 public function getAuthToken(): ?string
38
 {
39
  return $this->SUS_AUT_TOK;
40
 }
41
42
 public function getContentEncoding(): ?string
43
 {
44
  return $this->SUS_CONT_ENCOD;
45
 }
46
}
47

H. php / suscripcionElimina.php

1
<?php
2
3
function suscripcionElimina(\PDO $bd, string $endpoint)
4
{
5
 $stmt =
6
  $bd->prepare("DELETE FROM SUSCRIPCION WHERE SUS_ENDPOINT = :SUS_ENDPOINT");
7
 $stmt->execute([":SUS_ENDPOINT" => $endpoint]);
8
}
9

I. Carpeta « php / lib »

1. php / lib / BAD_REQUEST.php

1
<?php
2
3
const BAD_REQUEST = 400;
4

2. php / lib / devuelveCreated.php

1
<?php
2
3
require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5
function devuelveCreated($urlDelNuevo, $resultado)
6
{
7
 $json = json_encode($resultado);
8
 if ($json === false) {
9
  devuelveResultadoNoJson();
10
 } else {
11
  http_response_code(201);
12
  header("Location: $urlDelNuevo");
13
  header("Content-Type: application/json; charset=utf-8");
14
  echo $json;
15
 }
16
}
17

3. php / lib / devuelveJson.php

1
<?php
2
3
require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5
function devuelveJson($resultado)
6
{
7
 $json = json_encode($resultado);
8
 if ($json === false) {
9
  devuelveResultadoNoJson();
10
 } else {
11
  header("Content-Type: application/json; charset=utf-8");
12
  echo $json;
13
 }
14
 exit();
15
}
16

4. php / lib / devuelveNoContent.php

1
<?php
2
3
function devuelveNoContent()
4
{
5
 http_response_code(204);
6
}
7

5. php / lib / devuelveResultadoNoJson.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5
function devuelveResultadoNoJson()
6
{
7
 http_response_code(INTERNAL_SERVER_ERROR);
8
 header("Content-Type: application/problem+json; charset=utf-8");
9
10
 echo '{' .
11
  "status: " . INTERNAL_SERVER_ERROR .
12
  '"title": "El resultado no puede representarse como JSON."' .
13
  '"type": "/errors/resultadonojson.html"' .
14
  '}';
15
}
16

6. php / lib / INTERNAL_SERVER_ERROR.php

1
<?php
2
3
const INTERNAL_SERVER_ERROR = 500;

7. php / lib / manejaErrores.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
require_once __DIR__ . "/ProblemDetailsException.php";
5
6
// Hace que se lance una excepción automáticamente cuando se genere un error.
7
set_error_handler(function ($severity, $message, $file, $line) {
8
 throw new ErrorException($message, 0, $severity, $file, $line);
9
});
10
11
// Código cuando una excepción no es atrapada.
12
set_exception_handler(function (Throwable $excepcion) {
13
 if ($excepcion instanceof ProblemDetailsException) {
14
  devuelveProblemDetails($excepcion->problemDetails);
15
 } else {
16
  devuelveProblemDetails([
17
   "status" => INTERNAL_SERVER_ERROR,
18
   "title" => "Error interno del servidor",
19
   "detail" => $excepcion->getMessage(),
20
   "type" => "/errors/errorinterno.html",
21
  ]);
22
 }
23
 exit();
24
});
25
26
function devuelveProblemDetails(array $array)
27
{
28
 $json = json_encode($array);
29
 if ($json === false) {
30
  devuelveResultadoNoJson();
31
 } else {
32
  http_response_code(isset($array["status"]) ? $array["status"] : 500);
33
  header("Content-Type: application/problem+json; charset=utf-8");
34
  echo $json;
35
 }
36
}
37

8. php / lib / ProblemDetailsException.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5
/**
6
 * Detalle de los errores devueltos por un servicio.
7
 */
8
class ProblemDetailsException extends Exception
9
{
10
11
 public array $problemDetails;
12
13
 public function __construct(
14
  array $problemDetails,
15
 ) {
16
  
17
  parent::__construct(
18
   isset($problemDetails["detail"])
19
    ? $problemDetails["detail"]
20
    : (isset($problemDetails["title"])
21
     ? $problemDetails["title"]
22
     : "Error"),
23
   $problemDetails["status"]
24
    ? $problemDetails["status"]
25
    : INTERNAL_SERVER_ERROR
26
  );
27
28
  $this->problemDetails = $problemDetails;
29
 }
30
}
31

9. php / lib / recibeJson.php

1
<?php
2
3
require_once __DIR__ . "/BAD_REQUEST.php";
4
5
function recibeJson()
6
{
7
 $json = json_decode(file_get_contents("php://input"));
8
9
 if ($json === null) {
10
11
  http_response_code(BAD_REQUEST);
12
  header("Content-Type: application/problem+json; charset=utf-8");
13
14
  echo '{' .
15
   "status: " . BAD_REQUEST .
16
   '"title": "Los datos recibidos no están en formato JSON."' .
17
   '"type": "/errors/datosnojson.html"' .
18
   '}';
19
20
  exit();
21
 }
22
23
 return $json;
24
}
25

10. Carpeta « js »

Versión para imprimir.

A. Carpeta « js / lib »

1. js / lib / activaNotificacionesPush.js

1
/**
2
 * @param { string | URL } urlDeServiceWorkerQueRecibeNotificaciones
3
 */
4
export async function activaNotificacionesPush(
5
6
 urlDeServiceWorkerQueRecibeNotificaciones) {
7
8
 // Valida que el navegador soporte notificaciones push.
9
 if (!('PushManager' in window))
10
  throw new Error("Este navegador no soporta notificaciones push.")
11
12
 // Valida que el navegador soporte notificaciones,
13
 if (!("Notification" in window))
14
  throw new Error("Este navegador no soporta notificaciones push.")
15
16
 // Valida que el navegador soporte service workers,
17
 if (!("serviceWorker" in navigator))
18
  throw new Error("Este navegador no soporta service workers.")
19
20
 // Recupera el permiso para usar notificaciones
21
 let permiso = Notification.permission
22
 if (permiso === "default") {
23
  // Permiso no asignado. Pide al usuario su autorización.
24
  permiso = await Notification.requestPermission()
25
 }
26
27
 // Valida que el usuario haya permitido usar notificaciones..
28
 if (permiso === "denied")
29
  throw new Error("Notificaciones bloqueadas.")
30
31
 const registro = await navigator.serviceWorker.register(
32
  urlDeServiceWorkerQueRecibeNotificaciones)
33
 console.log(urlDeServiceWorkerQueRecibeNotificaciones, "registrado.")
34
 console.log(registro)
35
36
 if (!("showNotification" in registro))
37
  throw new Error("Este navegador no soporta notificaciones.")
38
}

2. js / lib / calculaDtoParaSuscripcion.js

1
/**
2
 * Devuelve una literal de objeto que se puede usar para enviar
3
 * en formato JSON al servidor.
4
 * DTO es un acrónimo para Data Transder Object, u
5
 * objeto para transferencia de datos.
6
 * @param { PushSubscription } suscripcion
7
 */
8
export function calculaDtoParaSuscripcion(suscripcion) {
9
 const key = suscripcion.getKey("p256dh")
10
 const token = suscripcion.getKey("auth")
11
 const supported = PushManager.supportedContentEncodings
12
 const encodings = Array.isArray(supported) && supported.length > 0
13
  ? supported
14
  : ["aesgcm"]
15
 const endpoint = suscripcion.endpoint
16
 const publicKey = key === null
17
  ? null
18
  : btoa(String.fromCharCode.apply(null, new Uint8Array(key)))
19
 const authToken = token === null
20
  ? null
21
  : btoa(String.fromCharCode.apply(null, new Uint8Array(token)))
22
 const contentEncoding = encodings[0]
23
 return {
24
  endpoint,
25
  publicKey,
26
  authToken,
27
  contentEncoding
28
 }
29
}
30

3. js / lib / cancelaSuscripcionPush.js

1
import { getSuscripcionPush } from "./getSuscripcionPush.js"
2
3
export async function cancelaSuscripcionPush() {
4
 const suscripcion = await getSuscripcionPush()
5
 const resultado = suscripcion === null
6
  ? false
7
  : await suscripcion.unsubscribe()
8
 return resultado === true ? suscripcion : null
9
}

4. js / lib / consume.js

1
import { ProblemDetailsError } from "./ProblemDetailsError.js"
2
3
/**
4
 * Espera a que la promesa de un fetch termine. Si
5
 * hay error, lanza una excepción.
6
 * 
7
 * @param {Promise<Response> } servicio
8
 */
9
export async function consume(servicio) {
10
 const respuesta = await servicio
11
 if (respuesta.ok) {
12
  return respuesta
13
 } else {
14
  const contentType = respuesta.headers.get("Content-Type")
15
  if (
16
   contentType !== null && contentType.startsWith("application/problem+json")
17
  )
18
   throw new ProblemDetailsError(await respuesta.json())
19
  else
20
   throw new Error(respuesta.statusText)
21
 }
22
}

5. js / lib / descargaVista.js

1
import { consume } from "./consume.js"
2
import { muestraObjeto } from "./muestraObjeto.js"
3
import { recibeJson } from "./recibeJson.js"
4
5
/**
6
 * @param {string} url
7
 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
8
 *  | "CONNECT" | "HEAD" } metodoHttp
9
 */
10
export async function descargaVista(url, metodoHttp = "GET") {
11
 const respuesta = await consume(recibeJson(url, metodoHttp))
12
 const json = await respuesta.json()
13
 muestraObjeto(document, json)
14
 return json
15
}

6. js / lib / enviaJsonRecibeJson.js

1
2
/**
3
 * @param { string } url
4
 * @param { Object } body
5
 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
6
 *  | "CONNECT" | "HEAD" } metodoHttp
7
 */
8
export async function enviaJsonRecibeJson(url, body, metodoHttp = "POST") {
9
 return fetch(
10
  url,
11
  {
12
   method: metodoHttp,
13
   headers: {
14
    "Content-Type": "application/json",
15
    "Accept": "application/json, application/problem+json"
16
   },
17
   body: JSON.stringify(body)
18
  }
19
 )
20
}

7. js / lib / getSuscripcionPush.js

1
export async function getSuscripcionPush() {
2
 // Recupera el service worker registrado.
3
 const registro = await navigator.serviceWorker.ready
4
 return registro.pushManager.getSubscription()
5
}

8. js / lib / manejaErrores.js

1
import { muestraError } from "./muestraError.js"
2
3
/**
4
 * Intercepta Response.prototype.json para capturar errores de parseo
5
 * y asegurar que se reporten correctamente en navegadores Chromium.
6
 */
7
{
8
 const originalJson = Response.prototype.json
9
10
 Response.prototype.json = function () {
11
  // Llamamos al método original usando el contexto (this) de la respuesta
12
  return originalJson.apply(this, arguments)
13
   .catch((/** @type {any} */ error) => {
14
    // Corrige un error de Chrome que evita el manejo correcto de errores.
15
    throw new Error(error)
16
   })
17
 }
18
}
19
20
window.onerror = function (
21
  /** @type {string} */ _message,
22
  /** @type {string} */ _url,
23
  /** @type {number} */ _line,
24
  /** @type {number} */ _column,
25
  /** @type {Error} */ errorObject
26
) {
27
 muestraError(errorObject)
28
 return true
29
}
30
31
window.addEventListener('unhandledrejection', event => {
32
 muestraError(event.reason)
33
 event.preventDefault()
34
})
35

9. js / lib / muestraError.js

1
import { ProblemDetailsError } from "./ProblemDetailsError.js"
2
3
/**
4
 * Muestra los datos de una Error en la consola y en un cuadro de alerta.
5
 * @param { ProblemDetailsError | Error | null } error descripción del error.
6
 */
7
export function muestraError(error) {
8
9
 if (error === null) {
10
11
  console.error("Error")
12
  alert("Error")
13
14
 } else if (error instanceof ProblemDetailsError) {
15
16
  const problemDetails = error.problemDetails
17
18
  let mensaje =
19
   typeof problemDetails["title"] === "string" ? problemDetails["title"] : ""
20
  if (typeof problemDetails["detail"] === "string") {
21
   if (mensaje !== "") {
22
    mensaje += "\n\n"
23
   }
24
   mensaje += problemDetails["detail"]
25
  }
26
  if (mensaje === "") {
27
   mensaje = "Error"
28
  }
29
  console.error(error, problemDetails)
30
  alert(mensaje)
31
32
 } else {
33
34
  console.error(error)
35
  alert(error.message)
36
37
 }
38
39
}

10. js / lib / muestraObjeto.js

1
/**
2
 * @param {Document | HTMLElement | ShadowRoot} raizHtml
3
 * @param { any } objeto
4
 */
5
export function muestraObjeto(raizHtml, objeto) {
6
 for (const [nombre, definiciones] of Object.entries(objeto)) {
7
  if (Array.isArray(definiciones)) {
8
   muestraArray(raizHtml, nombre, definiciones)
9
  } else if (definiciones !== undefined && definiciones !== null) {
10
   muestraElemento(raizHtml, nombre, definiciones)
11
  }
12
 }
13
}
14
15
/**
16
 * @param { string } nombre
17
 */
18
export function selectorDeNombre(nombre) {
19
 return `[id="${nombre}"],[name="${nombre}"],[data-name="${nombre}"]`
20
}
21
22
/**
23
 * @param { Document | HTMLElement | ShadowRoot } raizHtml
24
 * @param { string } propiedad
25
 * @param {any[]} valores
26
 */
27
function muestraArray(raizHtml, propiedad, valores) {
28
 const conjunto = new Set(valores)
29
 const elementos = raizHtml.querySelectorAll(selectorDeNombre(propiedad))
30
 if (elementos.length === 1 && elementos[0] instanceof HTMLSelectElement) {
31
  muestraOptions(elementos[0], conjunto)
32
 } else {
33
  muestraInputs(elementos, conjunto)
34
 }
35
36
}
37
38
/**
39
 * @param {HTMLSelectElement} select
40
 * @param {Set<any>} conjunto
41
 */
42
function muestraOptions(select, conjunto) {
43
 for (let i = 0, options = select.options, len = options.length; i < len; i++) {
44
  const option = options[i]
45
  option.selected = conjunto.has(option.value)
46
 }
47
}
48
49
/**
50
 * @param {NodeListOf<Element>} elementos
51
 * @param {Set<any>} conjunto
52
 */
53
function muestraInputs(elementos, conjunto) {
54
 for (let i = 0, len = elementos.length; i < len; i++) {
55
  const elemento = elementos[i]
56
  if (elemento instanceof HTMLInputElement) {
57
   elemento.checked = conjunto.has(elemento.value)
58
  }
59
 }
60
}
61
62
const data_ = "data-"
63
const data_Length = data_.length
64
65
/**
66
 * @param {Document | HTMLElement | ShadowRoot} raizHtml
67
 * @param {string} nombre
68
 * @param {{ [s: string]: any; } } definiciones
69
 */
70
function muestraElemento(raizHtml, nombre, definiciones) {
71
 const elemento = raizHtml.querySelector(selectorDeNombre(nombre))
72
 if (elemento !== null) {
73
  for (const [propiedad, valor] of Object.entries(definiciones)) {
74
   if (propiedad in elemento) {
75
    elemento[propiedad] = valor
76
   } else if (
77
    propiedad.length > data_Length
78
    && propiedad.startsWith(data_)
79
    && elemento instanceof HTMLElement
80
   ) {
81
    elemento.dataset[propiedad.substring(data_Length)] = valor
82
   }
83
  }
84
 }
85
}

11. js / lib / ProblemDetailsError.js

1
export class ProblemDetailsError extends Error {
2
3
 /**
4
  * Detalle de los errores devueltos por un servicio.
5
  * Crea una instancia de ProblemDetailsError.
6
  * @param {object} problemDetails Objeto con la descripcipon del error.
7
  */
8
 constructor(problemDetails) {
9
10
  super(typeof problemDetails["detail"] === "string"
11
   ? problemDetails["detail"]
12
   : (typeof problemDetails["title"] === "string"
13
    ? problemDetails["title"]
14
    : "Error"))
15
16
  this.problemDetails = problemDetails
17
18
 }
19
20
}

12. js / lib / recibeJson.js

1
2
/**
3
 * @param {string} url
4
 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
5
 *  | "CONNECT" | "HEAD" } metodoHttp
6
 */
7
export async function recibeJson(url, metodoHttp = "GET") {
8
 return fetch(
9
  url,
10
  {
11
   method: metodoHttp,
12
   headers: { "Accept": "application/json, application/problem+json" }
13
  }
14
 )
15
}

13. js / lib / suscribeAPush.js

1
/**
2
 * @param { ArrayBuffer } applicationServerKey
3
 */
4
export async function suscribeAPush(applicationServerKey) {
5
 // Recupera el service worker registrado.
6
 const registro = await navigator.serviceWorker.ready
7
 return registro.pushManager.subscribe({
8
  userVisibleOnly: true,
9
  applicationServerKey
10
 })
11
}

14. js / lib / urlBase64ToUint8Array.js

1
/**
2
 * @param { string } base64String
3
 */
4
export function urlBase64ToUint8Array(base64String) {
5
 const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
6
 const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/')
7
 const rawData = atob(base64)
8
 const outputArray = new Uint8Array(rawData.length)
9
 for (let i = 0; i < rawData.length; ++i) {
10
  outputArray[i] = rawData.charCodeAt(i)
11
 }
12
 return outputArray
13
}

11. Carpeta « errors »

Versión para imprimir.

A. errors / authtokenincorrecto.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El authToken debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>El authToken debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

B. errors / contentencodingincorrecta.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>La contentEncoding debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>La contentEncoding debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

C. errors / datosnojson.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Los datos recibidos no están en formato JSON</title>
10
11
</head>
12
13
<body>
14
15
 <h1>Los datos recibidos no están en formato JSON</h1>
16
17
</body>
18
19
</html>

D. errors / endpointincorrecto.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El endpoint debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>El endpoint debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

E. errors / errorinterno.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Error interno del servidor</title>
10
11
</head>
12
13
<body>
14
15
 <h1>Error interno del servidor</h1>
16
17
 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19
</body>
20
21
</html>

F. errors / publickeyincorrecta.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>La publicKey debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>La publicKey debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

G. errors / resultadonojson.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El resultado no puede representarse como JSON</title>
10
11
</head>
12
13
<body>
14
15
 <h1>El resultado no puede representarse como JSON</h1>
16
17
 <p>
18
  Debido a un error interno del servidor, el resultado generado, no se puede
19
  recuperar.
20
 </p>
21
22
</body>
23
24
</html>

12. composer.json

1
{
2
 "require": {
3
  "minishlink/web-push": "^10.0.1"
4
 }
5
}

13. composer.lock

1
{
2
    "_readme": [
3
        "This file locks the dependencies of your project to a known state",
4
        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5
        "This file is @generated automatically"
6
    ],
7
    "content-hash": "5d03d83a0c68b97c1b883f5e5d450861",
8
    "packages": [
9
        {
10
            "name": "brick/math",
11
            "version": "0.14.1",
12
            "source": {
13
                "type": "git",
14
                "url": "https://github.com/brick/math.git",
15
                "reference": "f05858549e5f9d7bb45875a75583240a38a281d0"
16
            },
17
            "dist": {
18
                "type": "zip",
19
                "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0",
20
                "reference": "f05858549e5f9d7bb45875a75583240a38a281d0",
21
                "shasum": ""
22
            },
23
            "require": {
24
                "php": "^8.2"
25
            },
26
            "require-dev": {
27
                "php-coveralls/php-coveralls": "^2.2",
28
                "phpstan/phpstan": "2.1.22",
29
                "phpunit/phpunit": "^11.5"
30
            },
31
            "type": "library",
32
            "autoload": {
33
                "psr-4": {
34
                    "Brick\\Math\\": "src/"
35
                }
36
            },
37
            "notification-url": "https://packagist.org/downloads/",
38
            "license": [
39
                "MIT"
40
            ],
41
            "description": "Arbitrary-precision arithmetic library",
42
            "keywords": [
43
                "Arbitrary-precision",
44
                "BigInteger",
45
                "BigRational",
46
                "arithmetic",
47
                "bigdecimal",
48
                "bignum",
49
                "bignumber",
50
                "brick",
51
                "decimal",
52
                "integer",
53
                "math",
54
                "mathematics",
55
                "rational"
56
            ],
57
            "support": {
58
                "issues": "https://github.com/brick/math/issues",
59
                "source": "https://github.com/brick/math/tree/0.14.1"
60
            },
61
            "funding": [
62
                {
63
                    "url": "https://github.com/BenMorel",
64
                    "type": "github"
65
                }
66
            ],
67
            "time": "2025-11-24T14:40:29+00:00"
68
        },
69
        {
70
            "name": "guzzlehttp/guzzle",
71
            "version": "7.10.0",
72
            "source": {
73
                "type": "git",
74
                "url": "https://github.com/guzzle/guzzle.git",
75
                "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
76
            },
77
            "dist": {
78
                "type": "zip",
79
                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
80
                "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
81
                "shasum": ""
82
            },
83
            "require": {
84
                "ext-json": "*",
85
                "guzzlehttp/promises": "^2.3",
86
                "guzzlehttp/psr7": "^2.8",
87
                "php": "^7.2.5 || ^8.0",
88
                "psr/http-client": "^1.0",
89
                "symfony/deprecation-contracts": "^2.2 || ^3.0"
90
            },
91
            "provide": {
92
                "psr/http-client-implementation": "1.0"
93
            },
94
            "require-dev": {
95
                "bamarni/composer-bin-plugin": "^1.8.2",
96
                "ext-curl": "*",
97
                "guzzle/client-integration-tests": "3.0.2",
98
                "php-http/message-factory": "^1.1",
99
                "phpunit/phpunit": "^8.5.39 || ^9.6.20",
100
                "psr/log": "^1.1 || ^2.0 || ^3.0"
101
            },
102
            "suggest": {
103
                "ext-curl": "Required for CURL handler support",
104
                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
105
                "psr/log": "Required for using the Log middleware"
106
            },
107
            "type": "library",
108
            "extra": {
109
                "bamarni-bin": {
110
                    "bin-links": true,
111
                    "forward-command": false
112
                }
113
            },
114
            "autoload": {
115
                "files": [
116
                    "src/functions_include.php"
117
                ],
118
                "psr-4": {
119
                    "GuzzleHttp\\": "src/"
120
                }
121
            },
122
            "notification-url": "https://packagist.org/downloads/",
123
            "license": [
124
                "MIT"
125
            ],
126
            "authors": [
127
                {
128
                    "name": "Graham Campbell",
129
                    "email": "hello@gjcampbell.co.uk",
130
                    "homepage": "https://github.com/GrahamCampbell"
131
                },
132
                {
133
                    "name": "Michael Dowling",
134
                    "email": "mtdowling@gmail.com",
135
                    "homepage": "https://github.com/mtdowling"
136
                },
137
                {
138
                    "name": "Jeremy Lindblom",
139
                    "email": "jeremeamia@gmail.com",
140
                    "homepage": "https://github.com/jeremeamia"
141
                },
142
                {
143
                    "name": "George Mponos",
144
                    "email": "gmponos@gmail.com",
145
                    "homepage": "https://github.com/gmponos"
146
                },
147
                {
148
                    "name": "Tobias Nyholm",
149
                    "email": "tobias.nyholm@gmail.com",
150
                    "homepage": "https://github.com/Nyholm"
151
                },
152
                {
153
                    "name": "Márk Sági-Kazár",
154
                    "email": "mark.sagikazar@gmail.com",
155
                    "homepage": "https://github.com/sagikazarmark"
156
                },
157
                {
158
                    "name": "Tobias Schultze",
159
                    "email": "webmaster@tubo-world.de",
160
                    "homepage": "https://github.com/Tobion"
161
                }
162
            ],
163
            "description": "Guzzle is a PHP HTTP client library",
164
            "keywords": [
165
                "client",
166
                "curl",
167
                "framework",
168
                "http",
169
                "http client",
170
                "psr-18",
171
                "psr-7",
172
                "rest",
173
                "web service"
174
            ],
175
            "support": {
176
                "issues": "https://github.com/guzzle/guzzle/issues",
177
                "source": "https://github.com/guzzle/guzzle/tree/7.10.0"
178
            },
179
            "funding": [
180
                {
181
                    "url": "https://github.com/GrahamCampbell",
182
                    "type": "github"
183
                },
184
                {
185
                    "url": "https://github.com/Nyholm",
186
                    "type": "github"
187
                },
188
                {
189
                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
190
                    "type": "tidelift"
191
                }
192
            ],
193
            "time": "2025-08-23T22:36:01+00:00"
194
        },
195
        {
196
            "name": "guzzlehttp/promises",
197
            "version": "2.3.0",
198
            "source": {
199
                "type": "git",
200
                "url": "https://github.com/guzzle/promises.git",
201
                "reference": "481557b130ef3790cf82b713667b43030dc9c957"
202
            },
203
            "dist": {
204
                "type": "zip",
205
                "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
206
                "reference": "481557b130ef3790cf82b713667b43030dc9c957",
207
                "shasum": ""
208
            },
209
            "require": {
210
                "php": "^7.2.5 || ^8.0"
211
            },
212
            "require-dev": {
213
                "bamarni/composer-bin-plugin": "^1.8.2",
214
                "phpunit/phpunit": "^8.5.44 || ^9.6.25"
215
            },
216
            "type": "library",
217
            "extra": {
218
                "bamarni-bin": {
219
                    "bin-links": true,
220
                    "forward-command": false
221
                }
222
            },
223
            "autoload": {
224
                "psr-4": {
225
                    "GuzzleHttp\\Promise\\": "src/"
226
                }
227
            },
228
            "notification-url": "https://packagist.org/downloads/",
229
            "license": [
230
                "MIT"
231
            ],
232
            "authors": [
233
                {
234
                    "name": "Graham Campbell",
235
                    "email": "hello@gjcampbell.co.uk",
236
                    "homepage": "https://github.com/GrahamCampbell"
237
                },
238
                {
239
                    "name": "Michael Dowling",
240
                    "email": "mtdowling@gmail.com",
241
                    "homepage": "https://github.com/mtdowling"
242
                },
243
                {
244
                    "name": "Tobias Nyholm",
245
                    "email": "tobias.nyholm@gmail.com",
246
                    "homepage": "https://github.com/Nyholm"
247
                },
248
                {
249
                    "name": "Tobias Schultze",
250
                    "email": "webmaster@tubo-world.de",
251
                    "homepage": "https://github.com/Tobion"
252
                }
253
            ],
254
            "description": "Guzzle promises library",
255
            "keywords": [
256
                "promise"
257
            ],
258
            "support": {
259
                "issues": "https://github.com/guzzle/promises/issues",
260
                "source": "https://github.com/guzzle/promises/tree/2.3.0"
261
            },
262
            "funding": [
263
                {
264
                    "url": "https://github.com/GrahamCampbell",
265
                    "type": "github"
266
                },
267
                {
268
                    "url": "https://github.com/Nyholm",
269
                    "type": "github"
270
                },
271
                {
272
                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
273
                    "type": "tidelift"
274
                }
275
            ],
276
            "time": "2025-08-22T14:34:08+00:00"
277
        },
278
        {
279
            "name": "guzzlehttp/psr7",
280
            "version": "2.8.0",
281
            "source": {
282
                "type": "git",
283
                "url": "https://github.com/guzzle/psr7.git",
284
                "reference": "21dc724a0583619cd1652f673303492272778051"
285
            },
286
            "dist": {
287
                "type": "zip",
288
                "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
289
                "reference": "21dc724a0583619cd1652f673303492272778051",
290
                "shasum": ""
291
            },
292
            "require": {
293
                "php": "^7.2.5 || ^8.0",
294
                "psr/http-factory": "^1.0",
295
                "psr/http-message": "^1.1 || ^2.0",
296
                "ralouphie/getallheaders": "^3.0"
297
            },
298
            "provide": {
299
                "psr/http-factory-implementation": "1.0",
300
                "psr/http-message-implementation": "1.0"
301
            },
302
            "require-dev": {
303
                "bamarni/composer-bin-plugin": "^1.8.2",
304
                "http-interop/http-factory-tests": "0.9.0",
305
                "phpunit/phpunit": "^8.5.44 || ^9.6.25"
306
            },
307
            "suggest": {
308
                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
309
            },
310
            "type": "library",
311
            "extra": {
312
                "bamarni-bin": {
313
                    "bin-links": true,
314
                    "forward-command": false
315
                }
316
            },
317
            "autoload": {
318
                "psr-4": {
319
                    "GuzzleHttp\\Psr7\\": "src/"
320
                }
321
            },
322
            "notification-url": "https://packagist.org/downloads/",
323
            "license": [
324
                "MIT"
325
            ],
326
            "authors": [
327
                {
328
                    "name": "Graham Campbell",
329
                    "email": "hello@gjcampbell.co.uk",
330
                    "homepage": "https://github.com/GrahamCampbell"
331
                },
332
                {
333
                    "name": "Michael Dowling",
334
                    "email": "mtdowling@gmail.com",
335
                    "homepage": "https://github.com/mtdowling"
336
                },
337
                {
338
                    "name": "George Mponos",
339
                    "email": "gmponos@gmail.com",
340
                    "homepage": "https://github.com/gmponos"
341
                },
342
                {
343
                    "name": "Tobias Nyholm",
344
                    "email": "tobias.nyholm@gmail.com",
345
                    "homepage": "https://github.com/Nyholm"
346
                },
347
                {
348
                    "name": "Márk Sági-Kazár",
349
                    "email": "mark.sagikazar@gmail.com",
350
                    "homepage": "https://github.com/sagikazarmark"
351
                },
352
                {
353
                    "name": "Tobias Schultze",
354
                    "email": "webmaster@tubo-world.de",
355
                    "homepage": "https://github.com/Tobion"
356
                },
357
                {
358
                    "name": "Márk Sági-Kazár",
359
                    "email": "mark.sagikazar@gmail.com",
360
                    "homepage": "https://sagikazarmark.hu"
361
                }
362
            ],
363
            "description": "PSR-7 message implementation that also provides common utility methods",
364
            "keywords": [
365
                "http",
366
                "message",
367
                "psr-7",
368
                "request",
369
                "response",
370
                "stream",
371
                "uri",
372
                "url"
373
            ],
374
            "support": {
375
                "issues": "https://github.com/guzzle/psr7/issues",
376
                "source": "https://github.com/guzzle/psr7/tree/2.8.0"
377
            },
378
            "funding": [
379
                {
380
                    "url": "https://github.com/GrahamCampbell",
381
                    "type": "github"
382
                },
383
                {
384
                    "url": "https://github.com/Nyholm",
385
                    "type": "github"
386
                },
387
                {
388
                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
389
                    "type": "tidelift"
390
                }
391
            ],
392
            "time": "2025-08-23T21:21:41+00:00"
393
        },
394
        {
395
            "name": "minishlink/web-push",
396
            "version": "v10.0.1",
397
            "source": {
398
                "type": "git",
399
                "url": "https://github.com/web-push-libs/web-push-php.git",
400
                "reference": "08463189d3501cbd78a8625c87ab6680a7397aad"
401
            },
402
            "dist": {
403
                "type": "zip",
404
                "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/08463189d3501cbd78a8625c87ab6680a7397aad",
405
                "reference": "08463189d3501cbd78a8625c87ab6680a7397aad",
406
                "shasum": ""
407
            },
408
            "require": {
409
                "ext-curl": "*",
410
                "ext-json": "*",
411
                "ext-mbstring": "*",
412
                "ext-openssl": "*",
413
                "guzzlehttp/guzzle": "^7.9.2",
414
                "php": ">=8.2",
415
                "spomky-labs/base64url": "^2.0.4",
416
                "web-token/jwt-library": "^3.4.9|^4.0.6"
417
            },
418
            "require-dev": {
419
                "friendsofphp/php-cs-fixer": "^v3.91.3",
420
                "phpstan/phpstan": "^2.1.33",
421
                "phpstan/phpstan-strict-rules": "^2.0",
422
                "phpunit/phpunit": "^11.5.46|^12.5.2",
423
                "symfony/polyfill-iconv": "^1.33"
424
            },
425
            "suggest": {
426
                "ext-bcmath": "Optional for performance.",
427
                "ext-gmp": "Optional for performance."
428
            },
429
            "type": "library",
430
            "autoload": {
431
                "psr-4": {
432
                    "Minishlink\\WebPush\\": "src"
433
                }
434
            },
435
            "notification-url": "https://packagist.org/downloads/",
436
            "license": [
437
                "MIT"
438
            ],
439
            "authors": [
440
                {
441
                    "name": "Louis Lagrange",
442
                    "email": "lagrange.louis@gmail.com",
443
                    "homepage": "https://github.com/Minishlink"
444
                }
445
            ],
446
            "description": "Web Push library for PHP",
447
            "homepage": "https://github.com/web-push-libs/web-push-php",
448
            "keywords": [
449
                "Push API",
450
                "WebPush",
451
                "notifications",
452
                "push",
453
                "web"
454
            ],
455
            "support": {
456
                "issues": "https://github.com/web-push-libs/web-push-php/issues",
457
                "source": "https://github.com/web-push-libs/web-push-php/tree/v10.0.1"
458
            },
459
            "time": "2025-12-15T10:04:28+00:00"
460
        },
461
        {
462
            "name": "psr/clock",
463
            "version": "1.0.0",
464
            "source": {
465
                "type": "git",
466
                "url": "https://github.com/php-fig/clock.git",
467
                "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
468
            },
469
            "dist": {
470
                "type": "zip",
471
                "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
472
                "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
473
                "shasum": ""
474
            },
475
            "require": {
476
                "php": "^7.0 || ^8.0"
477
            },
478
            "type": "library",
479
            "autoload": {
480
                "psr-4": {
481
                    "Psr\\Clock\\": "src/"
482
                }
483
            },
484
            "notification-url": "https://packagist.org/downloads/",
485
            "license": [
486
                "MIT"
487
            ],
488
            "authors": [
489
                {
490
                    "name": "PHP-FIG",
491
                    "homepage": "https://www.php-fig.org/"
492
                }
493
            ],
494
            "description": "Common interface for reading the clock.",
495
            "homepage": "https://github.com/php-fig/clock",
496
            "keywords": [
497
                "clock",
498
                "now",
499
                "psr",
500
                "psr-20",
501
                "time"
502
            ],
503
            "support": {
504
                "issues": "https://github.com/php-fig/clock/issues",
505
                "source": "https://github.com/php-fig/clock/tree/1.0.0"
506
            },
507
            "time": "2022-11-25T14:36:26+00:00"
508
        },
509
        {
510
            "name": "psr/http-client",
511
            "version": "1.0.3",
512
            "source": {
513
                "type": "git",
514
                "url": "https://github.com/php-fig/http-client.git",
515
                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
516
            },
517
            "dist": {
518
                "type": "zip",
519
                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
520
                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
521
                "shasum": ""
522
            },
523
            "require": {
524
                "php": "^7.0 || ^8.0",
525
                "psr/http-message": "^1.0 || ^2.0"
526
            },
527
            "type": "library",
528
            "extra": {
529
                "branch-alias": {
530
                    "dev-master": "1.0.x-dev"
531
                }
532
            },
533
            "autoload": {
534
                "psr-4": {
535
                    "Psr\\Http\\Client\\": "src/"
536
                }
537
            },
538
            "notification-url": "https://packagist.org/downloads/",
539
            "license": [
540
                "MIT"
541
            ],
542
            "authors": [
543
                {
544
                    "name": "PHP-FIG",
545
                    "homepage": "https://www.php-fig.org/"
546
                }
547
            ],
548
            "description": "Common interface for HTTP clients",
549
            "homepage": "https://github.com/php-fig/http-client",
550
            "keywords": [
551
                "http",
552
                "http-client",
553
                "psr",
554
                "psr-18"
555
            ],
556
            "support": {
557
                "source": "https://github.com/php-fig/http-client"
558
            },
559
            "time": "2023-09-23T14:17:50+00:00"
560
        },
561
        {
562
            "name": "psr/http-factory",
563
            "version": "1.1.0",
564
            "source": {
565
                "type": "git",
566
                "url": "https://github.com/php-fig/http-factory.git",
567
                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
568
            },
569
            "dist": {
570
                "type": "zip",
571
                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
572
                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
573
                "shasum": ""
574
            },
575
            "require": {
576
                "php": ">=7.1",
577
                "psr/http-message": "^1.0 || ^2.0"
578
            },
579
            "type": "library",
580
            "extra": {
581
                "branch-alias": {
582
                    "dev-master": "1.0.x-dev"
583
                }
584
            },
585
            "autoload": {
586
                "psr-4": {
587
                    "Psr\\Http\\Message\\": "src/"
588
                }
589
            },
590
            "notification-url": "https://packagist.org/downloads/",
591
            "license": [
592
                "MIT"
593
            ],
594
            "authors": [
595
                {
596
                    "name": "PHP-FIG",
597
                    "homepage": "https://www.php-fig.org/"
598
                }
599
            ],
600
            "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
601
            "keywords": [
602
                "factory",
603
                "http",
604
                "message",
605
                "psr",
606
                "psr-17",
607
                "psr-7",
608
                "request",
609
                "response"
610
            ],
611
            "support": {
612
                "source": "https://github.com/php-fig/http-factory"
613
            },
614
            "time": "2024-04-15T12:06:14+00:00"
615
        },
616
        {
617
            "name": "psr/http-message",
618
            "version": "2.0",
619
            "source": {
620
                "type": "git",
621
                "url": "https://github.com/php-fig/http-message.git",
622
                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
623
            },
624
            "dist": {
625
                "type": "zip",
626
                "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
627
                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
628
                "shasum": ""
629
            },
630
            "require": {
631
                "php": "^7.2 || ^8.0"
632
            },
633
            "type": "library",
634
            "extra": {
635
                "branch-alias": {
636
                    "dev-master": "2.0.x-dev"
637
                }
638
            },
639
            "autoload": {
640
                "psr-4": {
641
                    "Psr\\Http\\Message\\": "src/"
642
                }
643
            },
644
            "notification-url": "https://packagist.org/downloads/",
645
            "license": [
646
                "MIT"
647
            ],
648
            "authors": [
649
                {
650
                    "name": "PHP-FIG",
651
                    "homepage": "https://www.php-fig.org/"
652
                }
653
            ],
654
            "description": "Common interface for HTTP messages",
655
            "homepage": "https://github.com/php-fig/http-message",
656
            "keywords": [
657
                "http",
658
                "http-message",
659
                "psr",
660
                "psr-7",
661
                "request",
662
                "response"
663
            ],
664
            "support": {
665
                "source": "https://github.com/php-fig/http-message/tree/2.0"
666
            },
667
            "time": "2023-04-04T09:54:51+00:00"
668
        },
669
        {
670
            "name": "ralouphie/getallheaders",
671
            "version": "3.0.3",
672
            "source": {
673
                "type": "git",
674
                "url": "https://github.com/ralouphie/getallheaders.git",
675
                "reference": "120b605dfeb996808c31b6477290a714d356e822"
676
            },
677
            "dist": {
678
                "type": "zip",
679
                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
680
                "reference": "120b605dfeb996808c31b6477290a714d356e822",
681
                "shasum": ""
682
            },
683
            "require": {
684
                "php": ">=5.6"
685
            },
686
            "require-dev": {
687
                "php-coveralls/php-coveralls": "^2.1",
688
                "phpunit/phpunit": "^5 || ^6.5"
689
            },
690
            "type": "library",
691
            "autoload": {
692
                "files": [
693
                    "src/getallheaders.php"
694
                ]
695
            },
696
            "notification-url": "https://packagist.org/downloads/",
697
            "license": [
698
                "MIT"
699
            ],
700
            "authors": [
701
                {
702
                    "name": "Ralph Khattar",
703
                    "email": "ralph.khattar@gmail.com"
704
                }
705
            ],
706
            "description": "A polyfill for getallheaders.",
707
            "support": {
708
                "issues": "https://github.com/ralouphie/getallheaders/issues",
709
                "source": "https://github.com/ralouphie/getallheaders/tree/develop"
710
            },
711
            "time": "2019-03-08T08:55:37+00:00"
712
        },
713
        {
714
            "name": "spomky-labs/base64url",
715
            "version": "v2.0.4",
716
            "source": {
717
                "type": "git",
718
                "url": "https://github.com/Spomky-Labs/base64url.git",
719
                "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d"
720
            },
721
            "dist": {
722
                "type": "zip",
723
                "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d",
724
                "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d",
725
                "shasum": ""
726
            },
727
            "require": {
728
                "php": ">=7.1"
729
            },
730
            "require-dev": {
731
                "phpstan/extension-installer": "^1.0",
732
                "phpstan/phpstan": "^0.11|^0.12",
733
                "phpstan/phpstan-beberlei-assert": "^0.11|^0.12",
734
                "phpstan/phpstan-deprecation-rules": "^0.11|^0.12",
735
                "phpstan/phpstan-phpunit": "^0.11|^0.12",
736
                "phpstan/phpstan-strict-rules": "^0.11|^0.12"
737
            },
738
            "type": "library",
739
            "autoload": {
740
                "psr-4": {
741
                    "Base64Url\\": "src/"
742
                }
743
            },
744
            "notification-url": "https://packagist.org/downloads/",
745
            "license": [
746
                "MIT"
747
            ],
748
            "authors": [
749
                {
750
                    "name": "Florent Morselli",
751
                    "homepage": "https://github.com/Spomky-Labs/base64url/contributors"
752
                }
753
            ],
754
            "description": "Base 64 URL Safe Encoding/Decoding PHP Library",
755
            "homepage": "https://github.com/Spomky-Labs/base64url",
756
            "keywords": [
757
                "base64",
758
                "rfc4648",
759
                "safe",
760
                "url"
761
            ],
762
            "support": {
763
                "issues": "https://github.com/Spomky-Labs/base64url/issues",
764
                "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4"
765
            },
766
            "funding": [
767
                {
768
                    "url": "https://github.com/Spomky",
769
                    "type": "github"
770
                },
771
                {
772
                    "url": "https://www.patreon.com/FlorentMorselli",
773
                    "type": "patreon"
774
                }
775
            ],
776
            "time": "2020-11-03T09:10:25+00:00"
777
        },
778
        {
779
            "name": "spomky-labs/pki-framework",
780
            "version": "1.4.1",
781
            "source": {
782
                "type": "git",
783
                "url": "https://github.com/Spomky-Labs/pki-framework.git",
784
                "reference": "f0e9a548df4e3942886adc9b7830581a46334631"
785
            },
786
            "dist": {
787
                "type": "zip",
788
                "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/f0e9a548df4e3942886adc9b7830581a46334631",
789
                "reference": "f0e9a548df4e3942886adc9b7830581a46334631",
790
                "shasum": ""
791
            },
792
            "require": {
793
                "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14",
794
                "ext-mbstring": "*",
795
                "php": ">=8.1"
796
            },
797
            "require-dev": {
798
                "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0",
799
                "ext-gmp": "*",
800
                "ext-openssl": "*",
801
                "infection/infection": "^0.28|^0.29|^0.31",
802
                "php-parallel-lint/php-parallel-lint": "^1.3",
803
                "phpstan/extension-installer": "^1.3|^2.0",
804
                "phpstan/phpstan": "^1.8|^2.0",
805
                "phpstan/phpstan-deprecation-rules": "^1.0|^2.0",
806
                "phpstan/phpstan-phpunit": "^1.1|^2.0",
807
                "phpstan/phpstan-strict-rules": "^1.3|^2.0",
808
                "phpunit/phpunit": "^10.1|^11.0|^12.0",
809
                "rector/rector": "^1.0|^2.0",
810
                "roave/security-advisories": "dev-latest",
811
                "symfony/string": "^6.4|^7.0|^8.0",
812
                "symfony/var-dumper": "^6.4|^7.0|^8.0",
813
                "symplify/easy-coding-standard": "^12.0"
814
            },
815
            "suggest": {
816
                "ext-bcmath": "For better performance (or GMP)",
817
                "ext-gmp": "For better performance (or BCMath)",
818
                "ext-openssl": "For OpenSSL based cyphering"
819
            },
820
            "type": "library",
821
            "autoload": {
822
                "psr-4": {
823
                    "SpomkyLabs\\Pki\\": "src/"
824
                }
825
            },
826
            "notification-url": "https://packagist.org/downloads/",
827
            "license": [
828
                "MIT"
829
            ],
830
            "authors": [
831
                {
832
                    "name": "Joni Eskelinen",
833
                    "email": "jonieske@gmail.com",
834
                    "role": "Original developer"
835
                },
836
                {
837
                    "name": "Florent Morselli",
838
                    "email": "florent.morselli@spomky-labs.com",
839
                    "role": "Spomky-Labs PKI Framework developer"
840
                }
841
            ],
842
            "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.",
843
            "homepage": "https://github.com/spomky-labs/pki-framework",
844
            "keywords": [
845
                "DER",
846
                "Private Key",
847
                "ac",
848
                "algorithm identifier",
849
                "asn.1",
850
                "asn1",
851
                "attribute certificate",
852
                "certificate",
853
                "certification request",
854
                "cryptography",
855
                "csr",
856
                "decrypt",
857
                "ec",
858
                "encrypt",
859
                "pem",
860
                "pkcs",
861
                "public key",
862
                "rsa",
863
                "sign",
864
                "signature",
865
                "verify",
866
                "x.509",
867
                "x.690",
868
                "x509",
869
                "x690"
870
            ],
871
            "support": {
872
                "issues": "https://github.com/Spomky-Labs/pki-framework/issues",
873
                "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.1"
874
            },
875
            "funding": [
876
                {
877
                    "url": "https://github.com/Spomky",
878
                    "type": "github"
879
                },
880
                {
881
                    "url": "https://www.patreon.com/FlorentMorselli",
882
                    "type": "patreon"
883
                }
884
            ],
885
            "time": "2025-12-20T12:57:40+00:00"
886
        },
887
        {
888
            "name": "symfony/deprecation-contracts",
889
            "version": "v3.6.0",
890
            "source": {
891
                "type": "git",
892
                "url": "https://github.com/symfony/deprecation-contracts.git",
893
                "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
894
            },
895
            "dist": {
896
                "type": "zip",
897
                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
898
                "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
899
                "shasum": ""
900
            },
901
            "require": {
902
                "php": ">=8.1"
903
            },
904
            "type": "library",
905
            "extra": {
906
                "thanks": {
907
                    "url": "https://github.com/symfony/contracts",
908
                    "name": "symfony/contracts"
909
                },
910
                "branch-alias": {
911
                    "dev-main": "3.6-dev"
912
                }
913
            },
914
            "autoload": {
915
                "files": [
916
                    "function.php"
917
                ]
918
            },
919
            "notification-url": "https://packagist.org/downloads/",
920
            "license": [
921
                "MIT"
922
            ],
923
            "authors": [
924
                {
925
                    "name": "Nicolas Grekas",
926
                    "email": "p@tchwork.com"
927
                },
928
                {
929
                    "name": "Symfony Community",
930
                    "homepage": "https://symfony.com/contributors"
931
                }
932
            ],
933
            "description": "A generic function and convention to trigger deprecation notices",
934
            "homepage": "https://symfony.com",
935
            "support": {
936
                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
937
            },
938
            "funding": [
939
                {
940
                    "url": "https://symfony.com/sponsor",
941
                    "type": "custom"
942
                },
943
                {
944
                    "url": "https://github.com/fabpot",
945
                    "type": "github"
946
                },
947
                {
948
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
949
                    "type": "tidelift"
950
                }
951
            ],
952
            "time": "2024-09-25T14:21:43+00:00"
953
        },
954
        {
955
            "name": "web-token/jwt-library",
956
            "version": "4.1.3",
957
            "source": {
958
                "type": "git",
959
                "url": "https://github.com/web-token/jwt-library.git",
960
                "reference": "690d4dd47b78f423cb90457f858e4106e1deb728"
961
            },
962
            "dist": {
963
                "type": "zip",
964
                "url": "https://api.github.com/repos/web-token/jwt-library/zipball/690d4dd47b78f423cb90457f858e4106e1deb728",
965
                "reference": "690d4dd47b78f423cb90457f858e4106e1deb728",
966
                "shasum": ""
967
            },
968
            "require": {
969
                "brick/math": "^0.12|^0.13|^0.14",
970
                "php": ">=8.2",
971
                "psr/clock": "^1.0",
972
                "spomky-labs/pki-framework": "^1.2.1"
973
            },
974
            "conflict": {
975
                "spomky-labs/jose": "*"
976
            },
977
            "suggest": {
978
                "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance",
979
                "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance",
980
                "ext-openssl": "For key management (creation, optimization, etc.) and some algorithms (AES, RSA, ECDSA, etc.)",
981
                "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
982
                "paragonie/sodium_compat": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
983
                "spomky-labs/aes-key-wrap": "For all Key Wrapping algorithms (AxxxKW, AxxxGCMKW, PBES2-HSxxx+AyyyKW...)",
984
                "symfony/console": "Needed to use console commands",
985
                "symfony/http-client": "To enable JKU/X5U support."
986
            },
987
            "type": "library",
988
            "autoload": {
989
                "psr-4": {
990
                    "Jose\\Component\\": ""
991
                }
992
            },
993
            "notification-url": "https://packagist.org/downloads/",
994
            "license": [
995
                "MIT"
996
            ],
997
            "authors": [
998
                {
999
                    "name": "Florent Morselli",
1000
                    "homepage": "https://github.com/Spomky"
1001
                },
1002
                {
1003
                    "name": "All contributors",
1004
                    "homepage": "https://github.com/web-token/jwt-framework/contributors"
1005
                }
1006
            ],
1007
            "description": "JWT library",
1008
            "homepage": "https://github.com/web-token",
1009
            "keywords": [
1010
                "JOSE",
1011
                "JWE",
1012
                "JWK",
1013
                "JWKSet",
1014
                "JWS",
1015
                "Jot",
1016
                "RFC7515",
1017
                "RFC7516",
1018
                "RFC7517",
1019
                "RFC7518",
1020
                "RFC7519",
1021
                "RFC7520",
1022
                "bundle",
1023
                "jwa",
1024
                "jwt",
1025
                "symfony"
1026
            ],
1027
            "support": {
1028
                "issues": "https://github.com/web-token/jwt-library/issues",
1029
                "source": "https://github.com/web-token/jwt-library/tree/4.1.3"
1030
            },
1031
            "funding": [
1032
                {
1033
                    "url": "https://github.com/Spomky",
1034
                    "type": "github"
1035
                },
1036
                {
1037
                    "url": "https://www.patreon.com/FlorentMorselli",
1038
                    "type": "patreon"
1039
                }
1040
            ],
1041
            "time": "2025-12-18T14:27:35+00:00"
1042
        }
1043
    ],
1044
    "packages-dev": [],
1045
    "aliases": [],
1046
    "minimum-stability": "stable",
1047
    "stability-flags": [],
1048
    "prefer-stable": false,
1049
    "prefer-lowest": false,
1050
    "platform": [],
1051
    "platform-dev": [],
1052
    "plugin-api-version": "2.6.0"
1053
}
1054

14. Carpeta « vendor »

Versión para imprimir.

A. vendor / -- No se muestra el contenido de esta carpeta --

1

15. jsconfig.json

1
{
2
 "compilerOptions": {
3
  "checkJs": true,
4
  "strictNullChecks": true,
5
  "target": "ES6",
6
  "module": "Node16",
7
  "moduleResolution": "Node16",
8
  "lib": [
9
   "ES2017",
10
   "WebWorker",
11
   "DOM"
12
  ]
18
}

16. Resumen