SDKs oficiales · PietroCal API v1
PietroCal API v1 · JavaScript

OAuth 2.1 para JavaScript

Helper ES Module para PKCE S256, URL de autorización, intercambio, renovación y revocación. Revisá los requisitos, copiá el archivo completo o descargalo para incorporarlo a tu proyecto.

pietrocal-oauth.jsjavascript/pietrocal-oauth.js · 109 líneas
Descargar
export class PietroCalOAuth {
    constructor({
        clientId,
        redirectUri,
        clientSecret = null,
        baseUrl = 'https://pietrocal.com',
        fetchImpl = globalThis.fetch
    }) {
        if (!String(clientId || '').startsWith('pc_oauth_')) {
            throw new TypeError('El Client ID debe comenzar con pc_oauth_.');
        }
        this.clientId = clientId;
        this.redirectUri = redirectUri;
        this.clientSecret = clientSecret;
        this.baseUrl = String(baseUrl).replace(/\/+$/, '');
        this.fetchImpl = fetchImpl;
    }

    async createPkce() {
        const bytes = crypto.getRandomValues(new Uint8Array(48));
        const codeVerifier = base64url(bytes);
        const digest = await crypto.subtle.digest(
            'SHA-256',
            new TextEncoder().encode(codeVerifier)
        );
        return {
            codeVerifier,
            codeChallenge: base64url(new Uint8Array(digest))
        };
    }

    authorizationUrl({ scopes, state, codeChallenge }) {
        const url = new URL('/oauth/authorize.php', this.baseUrl);
        url.search = new URLSearchParams({
            response_type: 'code',
            client_id: this.clientId,
            redirect_uri: this.redirectUri,
            scope: [...new Set(scopes)].join(' '),
            state,
            code_challenge: codeChallenge,
            code_challenge_method: 'S256'
        });
        return url.toString();
    }

    exchangeCode({ code, codeVerifier }) {
        return this.postForm('/oauth/token.php', {
            grant_type: 'authorization_code',
            client_id: this.clientId,
            client_secret: this.clientSecret,
            code,
            redirect_uri: this.redirectUri,
            code_verifier: codeVerifier
        });
    }

    refresh(refreshToken) {
        return this.postForm('/oauth/token.php', {
            grant_type: 'refresh_token',
            client_id: this.clientId,
            client_secret: this.clientSecret,
            refresh_token: refreshToken
        });
    }

    async revoke(token, tokenTypeHint = null) {
        await this.postForm('/oauth/revoke.php', {
            client_id: this.clientId,
            client_secret: this.clientSecret,
            token,
            token_type_hint: tokenTypeHint
        }, true);
    }

    async postForm(path, fields, allowEmpty = false) {
        const body = new URLSearchParams();
        Object.entries(fields).forEach(([key, value]) => {
            if (value !== null && value !== undefined && value !== '') {
                body.set(key, String(value));
            }
        });

        const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
            method: 'POST',
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body
        });

        const raw = await response.text();
        if (!response.ok) {
            let data = {};
            try { data = JSON.parse(raw); } catch (_) {}
            throw new Error(data.error_description || 'La operación OAuth falló.');
        }

        if (allowEmpty && !raw.trim()) return {};
        return raw.trim() ? JSON.parse(raw) : {};
    }
}

function base64url(bytes) {
    let binary = '';
    bytes.forEach(byte => { binary += String.fromCharCode(byte); });
    return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '');
}