*/ public static function sign_request( string $connection_id, string $seed_b64, string $method, string $host, string $path, string $query, string $body, int $now, string $plugin_version ): array { $ts = (string) $now; $nonce = MiPlata_Crypto::random_token( 18 ); $sig = MiPlata_Crypto::sign( $seed_b64, self::canonical( $method, $host, $path, $query, $ts, $nonce, $body ) ); return array( 'X-MP-Connection' => $connection_id, 'X-MP-Timestamp' => $ts, 'X-MP-Nonce' => $nonce, 'X-MP-Signature' => $sig, 'X-MP-Plugin' => $plugin_version, ); } /** Proof of possession for `POST /api/connect/redeem`. */ public static function redeem_proof( string $seed_b64, string $code, string $site_host ): string { return MiPlata_Crypto::sign( $seed_b64, "redeem\n" . self::hash_pairing_code( $code ) . "\n" . $site_host ); } /** Proof for `POST /api/connect/rotate`, signed by the *new* key. */ public static function rotate_proof( string $new_seed_b64, string $connection_id, string $old_public_key_b64 ): string { return MiPlata_Crypto::sign( $new_seed_b64, "rotate\n" . $connection_id . "\n" . MiPlata_Crypto::fingerprint( $old_public_key_b64 ) ); } /** * Verify an inbound webhook from miPlata: `v1\n{ts}\n{sha256hex(body)}` * signed with the tenant's webhook key (from capabilities). */ public static function verify_webhook( string $tenant_public_key_b64, string $ts, string $body, string $sig_b64, int $now ): bool { if ( ! preg_match( '/^\d{9,11}$/', $ts ) || abs( (int) $ts - $now ) > self::TS_SKEW_SECONDS ) { return false; } return MiPlata_Crypto::verify( $tenant_public_key_b64, "v1\n" . $ts . "\n" . MiPlata_Crypto::sha256_hex( $body ), $sig_b64 ); } /* ─────────────── pairing codes ─────────────── */ /** Same normalisation as the server (confusables O→0, I/L→1). */ public static function normalize_pairing_code( string $code ): string { $s = strtoupper( preg_replace( '/\s+/', '', trim( $code ) ) ); if ( ! preg_match( self::PAIRING_PATTERN, $s, $m ) ) { return ''; } $body = str_replace( array( 'O', 'I', 'L' ), array( '0', '1', '1' ), $m[2] . $m[3] . $m[4] ); for ( $i = 0, $n = strlen( $body ); $i < $n; $i++ ) { if ( false === strpos( self::CROCKFORD, $body[ $i ] ) ) { return ''; } } return 'MPC-' . strtolower( $m[1] ) . '-' . substr( $body, 0, 4 ) . '-' . substr( $body, 4, 4 ) . '-' . substr( $body, 8, 4 ); } public static function pairing_tenant( string $code ): string { $n = self::normalize_pairing_code( $code ); return '' === $n ? '' : explode( '-', $n )[1]; } public static function hash_pairing_code( string $code ): string { return MiPlata_Crypto::sha256_hex( self::normalize_pairing_code( $code ) ); } /** Tenant host the code belongs to; pinned to `*.miplata.co`. */ public static function host_for_code( string $code, string $root_domain = 'miplata.co' ): string { $t = self::pairing_tenant( $code ); if ( '' === $t ) { return ''; } $host = $t . '.' . $root_domain; return self::valid_host( $host ) ? $host : ''; } public static function valid_host( string $host ): bool { return 1 === preg_match( self::HOST_PATTERN, $host ); } /** Lower-cased host only, like `mpNormalizeSiteHost`. */ public static function normalize_site_host( string $raw ): string { $s = strtolower( trim( $raw ) ); $s = preg_replace( '#^https?://#', '', $s ); $s = explode( '?', explode( '/', $s )[0] )[0]; if ( '' === $s || false !== strpos( $s, ':' ) || strlen( $s ) > 253 ) { return ''; } if ( preg_match( '/^\d{1,3}(\.\d{1,3}){3}$/', $s ) ) { return ''; } if ( ! preg_match( '/^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/', $s ) ) { return ''; } if ( 'miplata.co' === $s || '.miplata.co' === substr( $s, -11 ) ) { return ''; } return $s; } }