 if one
// exists; otherwise at the very bottom). Self-contained, safe to append.

/* =============================================================
 * AUDIT IDENTITY TOKEN   (self-contained — safe to append)
 * -------------------------------------------------------------
 * GET /wp-json/basc/v1/audit-token
 *
 * Mints a short-lived, URL-safe HMAC token identifying the
 * logged-in portal user to the audit worker. The worker verifies
 * the signature with AUDIT_TOKEN_SECRET, decodes the email, resolves
 * it to a userId, then checks the per-audit access list.
 *
 * Token shape (identical to the MAB token, DIFFERENT secret):
 *     base64url(strtolower(email)) . "." . expEpoch . "." . hmacHex
 *     hmac = HMAC-SHA256( base64url(email) . "|" . exp , AUDIT_TOKEN_SECRET )
 *
 * The base64url encoder MUST match the worker's decoder exactly:
 * URL-safe ( +/ -> -_ ) and unpadded ( trailing = stripped ). This
 * mirrors the proven MAB PHP mint byte-for-byte.
 *
 * TTL is deliberately short (45 min) because revocation is bounded
 * by it — the audit page silently re-mints before expiry to keep
 * long editing sessions and the live socket alive.
 *
 * SECRET LIVES IN wp-config.php, NOT here (this file is versioned):
 *     define( 'AUDIT_TOKEN_SECRET', '<64-hex>' );   // != MAB_TOKEN_SECRET
 * ============================================================= */

if ( ! defined( 'BASC_AUDIT_TOKEN_TTL' ) ) {
    define( 'BASC_AUDIT_TOKEN_TTL', 2700 ); // 45 minutes
}

// Register the route on its own hook so this whole feature is one appendable
// block (no need to edit the existing basc_portal_rest_register_routes()).
// Cookie + nonce guarded: WP REST cookie auth only treats a request as
// logged-in when it carries a valid X-WP-Nonce (action 'wp_rest'), so
// is_user_logged_in() here IS the cookie+nonce gate — a logged-out or
// nonce-less request never reaches the callback, and the audit worker then
// default-denies.
add_action( 'rest_api_init', function () {
    register_rest_route( 'basc/v1', '/audit-token', array(
        'methods'             => 'GET',
        'callback'            => 'basc_portal_rest_audit_token',
        'permission_callback' => function () { return is_user_logged_in(); },
    ) );
} );

function basc_portal_rest_audit_token( $request ) {
    if ( ! defined( 'AUDIT_TOKEN_SECRET' ) || ! AUDIT_TOKEN_SECRET ) {
        return new WP_Error(
            'basc_audit_token_unconfigured',
            'Audit token signing is not configured (missing AUDIT_TOKEN_SECRET).',
            array( 'status' => 503 )
        );
    }
    $u = wp_get_current_user();
    if ( empty( $u ) || empty( $u->user_email ) ) {
        return new WP_Error( 'basc_audit_token_no_user', 'No authenticated user.', array( 'status' => 401 ) );
    }
    $email = strtolower( trim( $u->user_email ) );
    $e     = rtrim( strtr( base64_encode( $email ), '+/', '-_' ), '=' );
    $exp   = time() + BASC_AUDIT_TOKEN_TTL;
    $sig   = hash_hmac( 'sha256', $e . '|' . $exp, AUDIT_TOKEN_SECRET );
    $token = $e . '.' . $exp . '.' . $sig;

    return new WP_REST_Response( array(
        'ok'         => true,
        'token'      => $token,
        'expires_at' => $exp,   // epoch seconds — client should re-mint a little before this
        'ttl'        => BASC_AUDIT_TOKEN_TTL,
    ), 200 );
}

/* CORS for the audit-token endpoint. The audit page is served from the
 * worker origin (docket.basafetyconsortium.org), so the credentialed
 * cross-origin fetch needs an explicit origin + Allow-Credentials — the
 * wildcard ACAO WP sends by default is invalid with credentials. Scoped
 * to this one route so nothing else is loosened. If the audit page ever
 * moves to another origin, add it to $allowed. */
add_filter( 'rest_pre_serve_request', 'basc_portal_audit_token_cors', 15, 4 );
function basc_portal_audit_token_cors( $served, $result, $request, $server ) {
    if ( ! $request || strpos( (string) $request->get_route(), '/basc/v1/audit-token' ) === false ) {
        return $served;
    }
    $allowed = array( 'https://docket.basafetyconsortium.org' );
    $origin  = $request->get_header( 'origin' );
    if ( $origin && in_array( $origin, $allowed, true ) ) {
        header( 'Access-Control-Allow-Origin: ' . $origin );
        header( 'Access-Control-Allow-Credentials: true' );
        header( 'Access-Control-Allow-Methods: GET, OPTIONS' );
        header( 'Access-Control-Allow-Headers: X-WP-Nonce, Content-Type' );
        header( 'Vary: Origin' );
    }
    return $served;
}<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="https://basafetyconsortium.org/sitemaps_xsl.xsl"?>
<sitemapindex xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://basafetyconsortium.org/post-sitemap1.xml</loc>
<lastmod>2026-05-13T11:32:50-05:00</lastmod>
</sitemap>
<sitemap>
<loc>https://basafetyconsortium.org/page-sitemap1.xml</loc>
<lastmod>2026-07-12T12:57:16-05:00</lastmod>
</sitemap>
<sitemap>
<loc>https://basafetyconsortium.org/category-sitemap1.xml</loc>
</sitemap>
</sitemapindex>