MasStack API β€” Feasibility v1

πŸ” NEBA FTTH Coverage Tester

Herramienta para comprobar cobertura de fibra Γ³ptica NEBA en cualquier direcciΓ³n, con visualizaciΓ³n en tiempo real de cada llamada a la API.

βš™οΈ

Estado de la configuraciΓ³n

πŸ“–

ΒΏCΓ³mo funciona?

El sistema consulta la API MasFeasibility v1 de MasStack para determinar si una direcciΓ³n postal tiene cobertura de fibra Γ³ptica NEBA/FTTH disponible. El proceso siempre sigue estos 3 pasos:

πŸ”‘

1. AutenticaciΓ³n JWT

Firma JWT con clave RSA privada y obtiene token OAuth 2.0

🏒

2. Buscar edificio

Localiza el edificio en el catΓ‘logo por direcciΓ³n postal

πŸ“‘

3. Consultar cobertura

Obtiene datos detallados de cobertura para ese edificio

βœ…

4. Resultado

TecnologΓ­as disponibles, operadores y velocidades

ProducciΓ³n Β· feasibility.masstack.com
πŸ—ΊοΈ

Consultar cobertura por direcciΓ³n

πŸ“Š

Resultado de cobertura

πŸ”¬

Detalle de llamadas a la API

1
AutenticaciΓ³n OAuth 2.0
Genera un JWT firmado con RS256 y lo intercambia por un access token
Pendiente β–Ό
2
BΓΊsqueda de edificio
POST /orgs/{orgId}/coverage/buildings/search
Pendiente β–Ό
3
Datos de cobertura del edificio
GET /orgs/{orgId}/coverage/buildings/{buildingId}?unit_to_data=true
Pendiente β–Ό
🐘

CΓ³mo implementarlo en PHP

<?php

/**
 * MasStack - Paso 1: AutenticaciΓ³n OAuth 2.0 con JWT Bearer
 *
 * La API usa el flujo "JWT Bearer Grant" del estΓ‘ndar OAuth 2.0.
 * Necesitas firmar un JWT con tu clave RSA privada (RS256) y enviarlo
 * al endpoint de tokens para recibir un access token Bearer.
 *
 * Credenciales necesarias (del fichero service_account.json):
 *   - private_key      β†’ tu clave RSA privada
 *   - private_key_id   β†’ el identificador de la clave (kid)
 *   - client_email     β†’ ej: thirdparty@XXXX.auth.masmovil.com
 *   - token_uri        β†’ https://authn.masstack.com/v1/oauth/token  (PROD)
 *                     β†’ https://authn.sta.masstack.com/v1/oauth/token (STA)
 */

function generateJwtAssertion(
    string $privateKey,
    string $privateKeyId,
    string $clientEmail,
    string $tokenUri
): string {
    $header = base64UrlEncode(json_encode([
        'alg' => 'RS256',
        'typ' => 'JWT',
        'kid' => $privateKeyId,
    ]));

    $now = time();
    $payload = base64UrlEncode(json_encode([
        'iss' => $clientEmail,
        'aud' => $tokenUri,
        'iat' => $now,
        'exp' => $now + 3600,
    ]));

    $data   = "$header.$payload";
    $keyRes = openssl_pkey_get_private($privateKey);
    openssl_sign($data, $signature, $keyRes, OPENSSL_ALGO_SHA256);

    return "$data." . base64UrlEncode($signature);
}

function base64UrlEncode(string $data): string {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function getMasStackToken(array $config): array {
    $assertion = generateJwtAssertion(
        $config['private_key'],
        $config['private_key_id'],
        $config['client_email'],
        $config['token_uri']
    );

    $ch = curl_init($config['token_uri']);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/x-www-form-urlencoded'],
        CURLOPT_POSTFIELDS     => http_build_query([
            'assertion'  => $assertion,
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        ]),
    ]);

    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($status !== 200) {
        throw new RuntimeException("Error obteniendo token ($status): $body");
    }

    return json_decode($body, true);
    // Devuelve: ['access_token' => '...', 'expires_in' => 3600, 'token_type' => 'Bearer']
}

// --- Uso (PROD) ---
$config = [
    'private_key'    => file_get_contents('/ruta/private_key.pem'),
    'private_key_id' => '9fe70aff-...',
    'client_email'   => 'lemonvil-sa@XXXX.auth.masmovil.com',
    'token_uri'      => 'https://authn.masstack.com/v1/oauth/token',
];
$tokenData   = getMasStackToken($config);
$accessToken = $tokenData['access_token'];
<?php

/**
 * MasStack - Paso 2: Buscar edificio por direcciΓ³n postal
 *
 * Endpoint: POST /orgs/{orgId}/coverage/buildings/search
 *
 * Cabeceras requeridas:
 *   - Authorization: Bearer {token}
 *   - sf-check-id: UUID ΓΊnico por peticiΓ³n (para trazabilidad)
 *   - sf-channel: cΓ³digo de canal (te lo proporciona MasStack)
 *   - sf-segment: cΓ³digo de segmento (te lo proporciona MasStack)
 */

function searchBuilding(
    string $accessToken,
    string $orgId,
    string $apiBase,
    array  $address,
    string $sfChannel = '',
    string $sfSegment = ''
): array {
    $url     = "$apiBase/orgs/$orgId/coverage/buildings/search";
    $checkId = generateUuid();

    $headers = [
        "Authorization: Bearer $accessToken",
        'Content-Type: application/json',
        "sf-check-id: $checkId",
    ];
    if ($sfChannel) $headers[] = "sf-channel: $sfChannel";
    if ($sfSegment) $headers[] = "sf-segment: $sfSegment";

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_POSTFIELDS     => json_encode($address),
    ]);

    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($status !== 200) {
        throw new RuntimeException("Error buscando edificio ($status): $body");
    }

    return json_decode($body, true);
    // Devuelve un array de edificios con su 'technical_id'
}

function generateUuid(): string {
    return sprintf(
        '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
        mt_rand(0, 0xffff), mt_rand(0, 0xffff),
        mt_rand(0, 0xffff),
        mt_rand(0, 0x0fff) | 0x4000,
        mt_rand(0, 0x3fff) | 0x8000,
        mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
    );
}

// --- Uso (PROD) ---
$apiBase  = 'https://feasibility.masstack.com/v1';      // PROD
// $apiBase = 'https://feasibility.sta.masstack.com/v1'; // STA

$buildings = searchBuilding($accessToken, 'MARBLANCA', $apiBase, [
    'way_type'    => 'Calle',
    'way_name'    => 'Mayor',
    'number'      => '1',
    'town'        => 'Madrid',
    'postal_code' => '28001',
], 'LEMONVIL', 'RES');

$technicalId = $buildings[0]['technical_id'] ?? null;

if (!$technicalId) {
    echo "No se encontrΓ³ ningΓΊn edificio para esa direcciΓ³n.";
    exit;
}
<?php

/**
 * MasStack - Paso 3: Obtener cobertura del edificio
 *
 * Endpoint: GET /orgs/{orgId}/coverage/buildings/{technicalId}?unit_to_data=true
 */

function getBuildingCoverage(
    string $accessToken,
    string $orgId,
    string $apiBase,
    string $technicalId,
    string $sfChannel = '',
    string $sfSegment = ''
): array {
    $url     = "$apiBase/orgs/$orgId/coverage/buildings/$technicalId?unit_to_data=true";
    $checkId = generateUuid();

    $headers = [
        "Authorization: Bearer $accessToken",
        "sf-check-id: $checkId",
    ];
    if ($sfChannel) $headers[] = "sf-channel: $sfChannel";
    if ($sfSegment) $headers[] = "sf-segment: $sfSegment";

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => $headers,
    ]);

    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($status !== 200) {
        throw new RuntimeException("Error obteniendo cobertura ($status): $body");
    }

    return json_decode($body, true);
}

// --- Uso ---
$coverage = getBuildingCoverage($accessToken, 'MARBLANCA', $apiBase, $technicalId, 'LEMONVIL', 'RES');

$summary      = $coverage['_building_summary'] ?? $coverage;
$technologies = $summary['technologies']       ?? [];
$operators    = $summary['territory_owners']   ?? [];

echo "TecnologΓ­as disponibles: " . implode(', ', $technologies) . "\n";
$hasFTTH = in_array('FTTH', $technologies, true);
echo $hasFTTH ? "βœ… HAY COBERTURA FTTH/NEBA\n" : "❌ NO hay cobertura FTTH en esta direcciΓ³n\n";
<?php

class MasStackClient
{
    private string  $privateKey;
    private string  $privateKeyId;
    private string  $clientEmail;
    private string  $orgId;
    private string  $sfChannel;
    private string  $sfSegment;
    private string  $tokenUri;
    private string  $apiBase;
    private ?string $token    = null;
    private int     $tokenExp = 0;

    public function __construct(array $config)
    {
        $this->privateKey   = $config['private_key'];
        $this->privateKeyId = $config['private_key_id'];
        $this->clientEmail  = $config['client_email'];
        $this->orgId        = $config['org_id'];
        $this->sfChannel    = $config['sf_channel'] ?? '';
        $this->sfSegment    = $config['sf_segment'] ?? '';
        $this->tokenUri     = $config['token_uri'];
        $this->apiBase      = $config['api_base'];
    }

    /** Carga la configuraciΓ³n desde el JSON de service account */
    public static function fromServiceAccount(
        string $jsonPath,
        string $orgId,
        string $sfChannel = '',
        string $sfSegment = '',
        string $env = 'prod'   // 'prod' o 'sta'
    ): self {
        $data = json_decode(file_get_contents($jsonPath), true);
        return new self([
            'private_key'    => $data['private_key'],
            'private_key_id' => $data['private_key_id'],
            'client_email'   => $data['client_email'],
            'token_uri'      => $data['token_uri'],
            'org_id'         => $orgId,
            'sf_channel'     => $sfChannel,
            'sf_segment'     => $sfSegment,
            'api_base'       => $env === 'sta'
                ? 'https://feasibility.sta.masstack.com/v1'
                : 'https://feasibility.masstack.com/v1',
        ]);
    }

    public function checkCoverage(array $address): array
    {
        $buildings = $this->searchBuildings($address);
        if (empty($buildings)) {
            return ['found' => false, 'message' => 'DirecciΓ³n no encontrada en el catΓ‘logo'];
        }
        $building = $buildings[0];
        $coverage = $this->getBuildingCoverage($building['technical_id']);
        $summary      = $coverage['_building_summary'] ?? $coverage;
        $technologies = $summary['technologies']       ?? [];
        $operators    = $summary['territory_owners']   ?? [];
        return [
            'found'        => true,
            'ftth'         => in_array('FTTH', $technologies, true),
            'technologies' => $technologies,
            'operators'    => $operators,
            'building'     => $building,
            'raw'          => $coverage,
        ];
    }

    public function searchBuildings(array $address): array
    {
        return $this->request('POST', "/orgs/{$this->orgId}/coverage/buildings/search", $address);
    }

    public function getBuildingCoverage(string $technicalId): array
    {
        return $this->request('GET', "/orgs/{$this->orgId}/coverage/buildings/$technicalId", null, ['unit_to_data' => 'true']);
    }

    private function getToken(): string
    {
        if ($this->token && time() < $this->tokenExp - 60) return $this->token;
        $assertion = $this->buildJwt();
        $ch = curl_init($this->tokenUri);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => ['Content-Type: application/x-www-form-urlencoded'],
            CURLOPT_POSTFIELDS     => http_build_query([
                'assertion'  => $assertion,
                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            ]),
        ]);
        $body   = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($status !== 200) throw new RuntimeException("Auth error $status: $body");
        $data           = json_decode($body, true);
        $this->token    = $data['access_token'];
        $this->tokenExp = time() + $data['expires_in'];
        return $this->token;
    }

    private function buildJwt(): string
    {
        $now  = time();
        $head = $this->b64u(json_encode(['alg' => 'RS256', 'typ' => 'JWT', 'kid' => $this->privateKeyId]));
        $body = $this->b64u(json_encode(['iss' => $this->clientEmail, 'aud' => $this->tokenUri, 'iat' => $now, 'exp' => $now + 3600]));
        $data = "$head.$body";
        openssl_sign($data, $sig, openssl_pkey_get_private($this->privateKey), OPENSSL_ALGO_SHA256);
        return "$data." . $this->b64u($sig);
    }

    private function b64u(string $d): string { return rtrim(strtr(base64_encode($d), '+/', '-_'), '='); }

    private function request(string $method, string $path, ?array $body = null, array $query = []): array
    {
        $token   = $this->getToken();
        $url     = $this->apiBase . $path . ($query ? '?' . http_build_query($query) : '');
        $headers = [
            "Authorization: Bearer $token",
            'Content-Type: application/json',
            'sf-check-id: ' . $this->uuid(),
        ];
        if ($this->sfChannel) $headers[] = "sf-channel: {$this->sfChannel}";
        if ($this->sfSegment) $headers[] = "sf-segment: {$this->sfSegment}";
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST  => $method,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_POSTFIELDS     => $body ? json_encode($body) : null,
        ]);
        $resp   = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($status >= 400) throw new RuntimeException("API error $status on $method $path: $resp");
        return json_decode($resp, true);
    }

    private function uuid(): string
    {
        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff),
            mt_rand(0,0x0fff)|0x4000, mt_rand(0,0x3fff)|0x8000,
            mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff)
        );
    }
}

// ── Ejemplo de uso ───────────────────────────────────────────────────────────
$client = MasStackClient::fromServiceAccount(
    '/ruta/service_account.json',
    'MARBLANCA',
    'LEMONVIL',
    'RES',
    'prod'  // cambiar a 'sta' para staging
);

$result = $client->checkCoverage([
    'way_type'    => 'Calle',
    'way_name'    => 'Mayor',
    'number'      => '1',
    'town'        => 'Madrid',
    'postal_code' => '28001',
]);

if ($result['ftth']) {
    echo "βœ… HAY cobertura FTTH/NEBA\n";
    echo "TecnologΓ­as: " . implode(', ', $result['technologies']) . "\n";
} else {
    echo "❌ NO hay cobertura FTTH en esta dirección\n";
}
ℹ️
Dos entornos disponibles: PROD usa authn.masstack.com + feasibility.masstack.com Β· STA usa authn.sta.masstack.com + feasibility.sta.masstack.com. Cada entorno tiene su propio service account con claves RSA distintas.